vector中一些需要注意的点

vector中一些需要注意的点

  在学完string后,我们就进入vector的学习。

知识点总结

1.typeid(a).name() 能够确定变量 a 的真实类型,注意包含头文件
2.emplace_back 和 push_back 功能相近,但是某些情况下 emplace_back 的效率更高,因为它支持传递构造A类型的参数。例如

 	struct A {
	A(int a1, int a2)
		: _a1(a1)
		,_a2(a2)
	{
		cout << "A(int a1, int a2)" << endl;
	}
	A(const A& aa)
		:_a1(aa._a1)
		,_a2(aa._a2)
	{
		cout << "A(const A& aa)" << endl;
	}
	int _a1;
	int _a2;
};
int main() {
	emplace 和 push 都是插入
	vector<int> v1 = { 10, 20, 30 };
	vector<A> v2;
	A aa1(1, 1);
	// 类类型的vector的push_back
	v2.push_back(aa1);		//有名对象
	v2.push_back(A(2,2));	//隐式类型转换
	v2.push_back({ 3,3 });	//多参数的隐式类型转换
	cout << "————————————————" << endl;
	v2.emplace_back(aa1);
	v2.emplace_back(A(2, 2));
	v2.emplace_back(3, 3);
	vector<A>::iterator it2 = v2.begin();
	while (it2 != v2.end()) {
		/*cout << (*it2)._a1 << ":" << (*it2)._a2 << " ";*/
		//对结构体A类型就可以使用 -> 访问其成员变量
		cout << it2->_a1 << ":" << it2->_a2 << " "; 
		it2++;
	}
	cout << endl;
	//类类型的vector的迭代器的使用,因为成员变量不再是内置类型,所以对结构体用箭头访问成员变量
	//相比之下范围for更香
	for (auto& aa : v2) {
		cout << aa._a1 << ":" << aa._a2 << endl;
	}
	//C++17 中的结构化绑定
	for (auto [x, y] : v2) {
		cout << x << ":" << y << endl;
	}
	return 0;
}

3.类类型的vector变量中使用迭代器访问他的成员变量的数据。使用范围for更好,但是也可以用(*it).成员变量 的形式访问(如上述代码的 while 部分)。
4.练习

136. 只出现一次的数字

题设:给你一个非空整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
复杂度要求:线性时间复杂度的算法,只使用常量额外空间
思路:目的是找到只出现一次的元素,其他的整数也都只出现两次。逻辑操作符异或 ^,正好能达到这样的效果。对一个初始值为0的变量, 0 ^ x = x,相同就“相消”。尝试{1,2,2},结果就是1。所以我们只需要遍历整个数组(线性复杂度),拿着初始值为0的变量(常量空间)与所有元素异或即可。

	class Solution {
		public:
	    int singleNumber(vector<int>& nums) {
	       int x = 0;
	       for(cosnt auto& e : nums){
	       		x = x ^ e;
	       }
	       return x;
	    }
};
118.

题设:给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
思路:用 vector 构建一个类似二维数组,底层结构类似下面这样,
vector中一些需要注意的点_第1张图片
最后完全可以看作二维数组来处理。

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv;
        //开竖向的空间(行标签 numRows),初始化都一样
        vv.resize(numRows, vector<int>());
        //为每一行开辟空间并初始化为1
        for(int i = 0; i < numRows; i++){
        	vv[i].resize(i + 1, 1);
        }
        //遍历每个元素计算,第一二行以及每行的头尾也不用算
        for(int i = 2; i < vv.size(); i++){
			for(int j = 1; i < vv[i].size() - 1; j++){
				//全当二位数组算的原因:
	//第一个是 vector> 的[],第二个是 vector 的[],所以最后返回的就是 int
				vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
			}
		}
        return vv;
    }
};

26.删除有序数组中的重复项
题设:给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
要求:元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数
思路:快慢指针法。一前一后,快指针遍历数组,慢指针指向数组中的唯一元素。当两个指针指向的数据不同时,慢指针往前一步,并把快指针指向的内容赋给慢指针保证慢指针指向数组中的唯一元素,最后返回 i + 1 就是新的不重复且相对顺序不变的数组大小。

	class Solution {
	public:
	    int removeDuplicates(vector<int>& nums) {
	        if(nums.empty())
	            return 0;
	        int i = 0, j = 1;
	        for(j = 1; j < nums.size(); j++){
	            if(nums[j] != nums[i]){
	                i++;
	                nums[i] = nums[j];
	            }
	        }
	        return i + 1;
	    }
};

137 只出现一次的数字 II
题设:给你一个整数数组 nums ,除某个元素仅出现 一次 外,其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素
要求:线性时间复杂度的算法且使用常数级空间
思路:考虑状态机的方法,
每个数字在计算机中以二进制形式存储。我们可以单独考虑每一位(bit)的情况。对于某一位(比如最低位),统计所有数字在这一位上 1 的个数。由于其他数字都出现三次,唯一数字出现一次,因此:
如果唯一数字在这一位是 0,那么该位上 1 的出现的次数是 3 的倍数。
如果唯一数字在这一位是 1,那么该位上 1 的出现的次数是 3 的倍数加 1。
因此,我们可以通过统计每一位上 1 的个数,然后模 3,得到唯一数字在该位的值(0 或 1)。

	class Solution {
	public:
    int singleNumber(vector<int>& nums) {
        //分别记录某一位中 1 出现的次数 模 3 为 1 或 2 的情况
        int ones = 0, twos = 0;
        //遍历数组
        for (auto num : nums) {
            //更新
            //当前数字的此位为 1 且 此为 1 已经出现过一次,则 twos 置为 1
            twos |= ones & num;
            //此为出现 1 的次数与当前数字在此位是否为 1 不同步,异或
            ones ^= num;
            //3 = 1 + 2
            int threes = ones & twos;
            //将 ones 和 twos 中已经出现三次的位清零。
            ones &= ~threes;
            twos &= ~threes;
        }
        return ones;
    }
};

5.vector 中的迭代器失效
 失效即指,迭代器不能在后续代码中正常使用了。当 vector 中发生扩容导致开辟新空间,或者出现一些逻辑上的问题(如 erase 删除某些位置的数据)时,都容易发生迭代器失效的问题。
 对第一种情况,考虑下面的场景,

	my_vector::vector<int> v;
	//初始空间为 4
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	//在某个值的位置插入
	int x;
	cin >> x;
	auto it = find(v.begin(), v.end(), x);
	if (it != v.end()) {
		v.insert(it, x * 10);
		//这里我们认为 it 失效了。
		//因为插入时发生了扩容,内存空间位置可能发生改变,
		// 但是 it 只是找到的 pos 的浅拷贝,it 指向的是一个未知的位置
	}

 在考虑第二种情况,如下,

	//擦除
	void erase(iterator pos) {
		assert(pos >= _start && pos < _finish);
		iterator it = pos + 1;
		//数据前移
		while (it != _finish) {
			*(it - 1) = *it;
			it++;
		}
		_finish--;
	}
	//返回迭代器版本
	iterator erase(iterator pos) {
		assert(pos >= _start && pos < _finish);
		iterator it = pos + 1;
		//数据前移
		while (it != _finish) {
			*(it - 1) = *it;
			it++;
		}
		_finish--;
		return pos;
	}
	//要求删除所有的偶数
	auto it = v.begin();
	while (it != v.end()) {
		if (it != v.end()) {
			if (*it % 2 == 0) {
				v.erase(it);
				//it = v.erase(it);	//返回迭代器版本
				//运行时发现,it erase 后失效了! 不同编译器对迭代器失效的处理方式不同
			}
			it++;
		}
	}

以上代码会出现 assert 报错,
erase 迭代器失效中 assert 报错
原因是 erase 实现时是直接将 pos 后面的数据往前覆盖的,导致擦除数据 2 之后, _finish–,指向的也是一个 4,v中实际的数据为 1, 3, 4, 4,最后一个 4 就是 _finish 指向的。 it 继续擦除第一个 4 进而会 ++ 到比 _finish 更大导致越界,从而引发 assert 报错。究其原因,在于擦除时的判断语句it != v.end()给了 it 越界的机会,我们设置it < v.end(),结果就正常了。
 但是为了和库中的 erase匹配,我们同样可以设置重新返回一个迭代器来更新 it
.在这里插入图片描述
6.int 和 string 类型的拷贝 对 int 类型的 vector,发生扩容时,我们使用 memcpy 完成的按字节拷贝就是深拷贝,但对string类型恰恰相反。

	//扩容
	void reserve(size_t n) {
		if (n > capacity()) { //确保不缩容
			//1.开辟 n 个 T 类型的空间
			T* tmp = new T[n];
			//注意!!记录旧数据的大小,因为size()的返回值期间会变化
			size_t old_size = size();
			//拷贝旧数据
			if (_start) {
				//memcpy(tmp, _start, sizeof(T) * old_size);
				for (size_t i = 0; i < old_size; i++) {
					tmp[i] = _start[i];
				}
				//释放旧空间
				delete[] _start;
			}
			_start = tmp;
			_finish = _start + old_size;
			_endofstorage = _start + n;
		}
	}
	
	//尾插
	void push_back(const T& x) {
		//判断是否需要扩容
		if (_endofstorage == _finish) {
			reserve(capacity() == 0 ? 4 : capacity() * 2);
		}
		//插入数据
		*_finish = x;
		_finish++;
	}

	my_vector::vector<string> v1;
	v1.push_back("111111111111111");
	v1.push_back("111111111111111");
	v1.push_back("111111111111111");
	v1.push_back("111111111111111");
	//当超过初始内存时,出现乱码,在扩容部分
	//delete[] _start; 时影响到了 tmp。
	//原因是,对 char 类型使用 memcpy 是按字节拷贝成了浅拷贝
	//(会发现 _start 和 tmp 指向同一块空间,导致析构时 tmp 受影响),
	//所以我们最后用赋值的形式进行内容的拷贝,保存在新开辟的 tmp 指向的新空间
	v1.push_back("111111111111111");
	Print(v1);

以上代码会崩溃,在这里插入图片描述
因为使用 memcpy 完成的按字节拷贝对 string 是浅拷贝,
vector中一些需要注意的点_第2张图片
这就导致释放 _start 时会影响新开辟的部分,使程序出错。
解决方案:赋值重载(代码未注释部分)

	void swap(vector<T>& v) {
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
	}

	//赋值	v1 = v2,最后 v1 = b, v2 = a;(v2会被析构)
	vector<T>& operator=(vector<T> v) {
		swap(v);
		return *this;
	}

值得注意的是,对 int 类型,我们使用赋值完成是浅拷贝,但对string类型恰恰相反。
7.构造函数
 调用默认拷贝就是浅拷贝,它们会共用同一块空间。所以我们可以自己写一个复用数据的拷贝构造实现深拷贝,

	//拷贝构造
	vector(const vector<T>& v)
		:_start(nullptr)
		, _finish(nullptr)
		, _endofstorage(nullptr) {
		reserve(v.capacity());
		//复用待拷贝的数据值
		for (auto& e : v) {
			push_back(e);
		}
	}

同时,我们还可以实现一种多参数的构造函数,

	#include
	//让编译器支持多参数的构造
	vector(initializer_list<T> il) {
		reserve(il.size());
		//复用待拷贝的数据值
		for (auto& e : il) {
			push_back(e);
		}
	}
	my_vector::vector<int> v3 = { 1, 2, 3, 4 };

还可以实现对已知 vector 的指定区间的构造与多个 value 的构造,

	//迭代器区间构造 —— 模板形式,能在类之间使用。可以用任意类型的容器迭代器初始化
	template <class InputIterator>
	vector(InputIterator first, InputIterator last) {
		while (first != last) {
			push_back(*first);
			first++;
		}
	}
	//n个 value 的构造 —— 可实现 vector> vv(num); 的构造
	vector(int n, T val = T()) {
		resize(n, val);
	}
	//迭代器区间构造
	my_vector::vector<int> v4(v3.begin() + 1, v3.end() - 1);
	Print(v4);
	
	string s1("hello world!");
	my_vector::vector<int> v5(s1.begin() + 1, s1.end() - 1);
	Print(v5);
	//n个 value 的构造
	my_vector::vector<int> v6(10, 1);
	Print(v6);

不同类类型之间的构造一般会议 ASCII 码的形式进行切换。此外,由于编译器的最佳匹配原则,在进行多个 value 的构造时,如果 n 的类型是 size_t ,由于多个 value 的构造和迭代器区间构造的参数列表一样,而输入的又是个 1,默认为有符号整型,所以会匹配到 vector(InputIterator first, InputIterator last) 上去,导致报错。(出现这种问题就需要一步步屏蔽代码调试出来了)

你可能感兴趣的:(c++)