C++ | 容器vector

🦌云深麋鹿
专栏C++ | 用C语言学数据结构 | Java

回顾:上两篇我们结束了容器string,接下来这篇文章让我们进入到新的容器vector,体会新的设计思路吧~

放个目录

  • [一 vector的使用](#一 vector的使用)
    • [1.1 插入删除](#1.1 插入删除)
      • [1.1.1 尾插](#1.1.1 尾插)
      • [1.1.2 尾删](#1.1.2 尾删)
      • [1.1.3 头插](#1.1.3 头插)
      • [1.1.4 头删](#1.1.4 头删)
    • [1.2 构造函数](#1.2 构造函数)
      • [1.2.1 initializer_list](#1.2.1 initializer_list)
      • [1.2.2 n个val](#1.2.2 n个val)
      • [1.2.3 默认构造](#1.2.3 默认构造)
    • [1.3 vector空间接口](#1.3 vector空间接口)
      • [1.3.1 扩容](#1.3.1 扩容)
      • [1.3.2 resize](#1.3.2 resize)
    • [1.4 sort](#1.4 sort)
      • [1.4.1 升序](#1.4.1 升序)
      • [1.4.2 降序](#1.4.2 降序)
  • [二 题目](#二 题目)
    • [2.1 只出现一次的数字](#2.1 只出现一次的数字)
    • [2.2 杨辉三角](#2.2 杨辉三角)
  • [三 模拟实现](#三 模拟实现)
    • [3.1 成员变量](#3.1 成员变量)
      • [3.1.1 iterator](#3.1.1 iterator)
      • [3.1.2 T](#3.1.2 T)
    • [3.2 构造函数](#3.2 构造函数)
      • [3.2.1 无参](#3.2.1 无参)
      • [3.2.2 n个value](#3.2.2 n个value)
      • [3.2.3 花括号初始化](#3.2.3 花括号初始化)
      • [3.2.4 迭代器区间](#3.2.4 迭代器区间)
      • [3.2.5 拷贝构造](#3.2.5 拷贝构造)
      • [3.2.6 赋值](#3.2.6 赋值)
    • [3.3 析构函数](#3.3 析构函数)
    • [3.4 容量相关](#3.4 容量相关)
      • [3.4.1 size](#3.4.1 size)
      • [3.4.2 capacity](#3.4.2 capacity)
      • [3.4.3 reserve](#3.4.3 reserve)
      • [3.4.4 resize](#3.4.4 resize)
    • [3.5 改变vector](#3.5 改变vector)
    • [3.6 访问数据](#3.6 访问数据)
      • [3.6.1 operator[ ]](#3.6.1 operator[ ])
      • [3.6.2 迭代器](#3.6.2 迭代器)

一 vector的使用

1.1 插入删除

1.1.1 尾插

cpp 复制代码
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);

调试:

1.1.2 尾删

cpp 复制代码
v.pop_back();

调试:

1.1.3 头插

cpp 复制代码
v.insert(v.begin(), 0);

调试:

1.1.4 头删

cpp 复制代码
v.erase(v.begin());

调试:

1.2 构造函数

1.2.1 initializer_list

花括号初始化。

cpp 复制代码
vector<int> v1 = { 1,2,3,4,5 };

调试:

1.2.2 n个val

cpp 复制代码
vector<int> v2(5,1);

调试:

1.2.3 默认构造

cpp 复制代码
vector<int> v3;

调试:

1.3 vector空间接口

1.3.1 扩容

vs按照1.5倍方式扩容。

cpp 复制代码
vector<int> v;
size_t capacity = v.capacity();
for (size_t i = 0;i < 100;++i) {
    v.push_back(i);
    if (capacity != v.capacity()) {
        capacity = v.capacity();
        cout << "capacity:" << capacity << endl;
    }
}

运行:

不同平台实现不同,linux下使用的STL基本是按照2倍方式扩容。

1.3.2 resize

在开空间的同时还会进行初始化。

cpp 复制代码
vector<int> v1;
v1.resize(5);
vector<int> v2;
v2.resize(5,1);

调试:

1.4 sort

1.4.1 升序

cpp 复制代码
vector<int> v = {1,2,5,1,43,1,7,1};
sort(v.begin(), v.end());

调试:

1.4.2 降序

cpp 复制代码
sort(v.begin(), v.end(), greater<int>());

调试:

二 题目

2.1 只出现一次的数字

136. 只出现一次的数字 - 力扣(LeetCode)

思路:异或,最后剩下的就是只出现一次的数字。

cpp 复制代码
int singleNumber(vector<int>& nums) {
    int num = 0;
    for(int n:nums){
        num ^= n;
    }
    return num;
}

1.num即最后剩下的数字。

2.遍历nums,分别跟num异或。

3.一个数字跟0异或不变。

2.2 杨辉三角

118. 杨辉三角 - 力扣(LeetCode)

cpp 复制代码
vector<vector<int>> generate(int numRows) {
    vector<vector<int>> vv(numRows);
    for(size_t i = 0;i < numRows;++i){
        vv[i].resize(i + 1,1);
    }
    for(size_t i = 2;i < numRows;++i){
        for(size_t j = 1;j < i;++j){
            vv[i][j] = vv[i - 1][j - 1] + vv[i - 1][j];
        }
    }
    return vv;
}

1.第一个for循环给各行开辟不同大小的空间,都初始化为1。

2.第二个for循环给 值不是1的位置 根据公式算出它的值。

三 模拟实现

1.注意模板声明和定义不能分离到俩文件。

2.该用引用的时候用引用,深拷贝的代价很大。

3.1 成员变量

cpp 复制代码
iterator _first = nullptr;
iterator _last = nullptr;
iterator _end = nullptr;

先在这里给个默认值,以便无参构造。

3.1.1 iterator

这里的iterator,我们typedef一下,本质上是指针。

cpp 复制代码
typedef T* iterator;
typedef const T* const_iterator;

3.1.2 T

这里的T是我们的模板参数。

cpp 复制代码
template<class T>
class vector {
    //...
};

3.2 构造函数

3.2.1 无参

cpp 复制代码
vector() {}

因为我们已经在声明时给出缺省值,所以这个无参构造函数可以写空。

测试代码:

cpp 复制代码
vector<int> v;

调试:

3.2.2 n个value

cpp 复制代码
vector(size_t n,const T& val = T()) {
    if(n == 0){
        return;
    }
    reserve(n);
    for (size_t i = 0;i < n;++i) {
        _first[i] = val;
    }
    _last = _first + n;
}

1.若参数n为0,判定为无效构造,直接返回。

2.复用reserve,申请需要的空间。

3.依次赋值我们需要的n个数。

4.插入数据后,及时更新成员变量_last。

测试代码:

cpp 复制代码
vector<int> v(2,1);

调试:

查看内存空间:

3.2.3 花括号初始化

借助initializer_list初始化

cpp 复制代码
vector(std::initializer_list<T> il) {
    size_t n = il.size();
    reserve(n);
    for (auto e:il) {
        push_back(e);
    }
    _last = _first + n;
}

1.先存储il的元素个数。

2.拿这个n申请空间。

3.e遍历il,依次把e插入当前vector。

4.插入数据后,及时更新成员变量_last。

测试代码:

cpp 复制代码
vector<int> v = {1,2,3,5,6};

调试:

3.2.4 迭代器区间

cpp 复制代码
template<class InputIterator>
vector(InputIterator begin, InputIterator end) {
    if(begin == end){
        return;
    }
    size_t n = end - begin;
    reserve(n);
    while (begin != end) {
        push_back(*begin);
        ++begin;
    }
}

1.为了适配各种参数的类型,我们写一个模板函数。

2.排除无效迭代区间的情况。

3.存储元素个数n。

4.复用reserve申请空间。

5.参数begin依次往后。

6.复用push_back,把begin指向的值依次插入vector。

测试代码:

cpp 复制代码
vector<int> v1 = {1,2,3,5,6};
vector<int> v2(v1.begin(),v1.end());

调试:

问题:非法的间接寻址

编译报错:

实际上这里会有问题,不过不是当前我们写的测试代码有问题。

cpp 复制代码
vector<int> v(2,1);

是上面写的这句代码。

分析:

解决方案,写一个更匹配的构造:

cpp 复制代码
vector(int n, const T& val = T()) {
    if (n == 0) {
        return;
    }
    reserve(n);
    for (size_t i = 0;i < n;++i) {
        _first[i] = val;
    }
    _last = _first + n;
}

把size_t改成int,再写一个版本。

3.2.5 拷贝构造

问题:默认是浅拷贝,所以需要自己写。

cpp 复制代码
vector(const vector& v) { 
    reserve(v.capacity());
    for(auto e:v){
        push_back(e);
    }
}

1.先申请空间。

2.再插入数据。

测试代码:

cpp 复制代码
vector<int> v1 = {1,2,3,5,6};
vector<int> v2(v1);

调试:

3.2.6 赋值

复用swap(这个函数放到3.4.5了)。

cpp 复制代码
void operator=(vector v) { 
    swap(v);
}

1.这里的参数v是临时对象,出了这个函数就会被销毁。

2.我们和当前对象swap,销毁的就是swap后的旧对象。

3.3 析构函数

cpp 复制代码
~vector() {
    delete[] _first;
    _first = nullptr;
    _last = nullptr;
    _end = nullptr;
}

3.4 容量相关

3.4.1 size

返回元素个数。

cpp 复制代码
size_t size() const {
    return _last - _first;
}

3.4.2 capacity

返回容量大小。

cpp 复制代码
size_t capacity() const {
    return _end - _first;
}

3.4.3 reserve

错误代码:

cpp 复制代码
void reserve(size_t newCapacity) {
    if (newCapacity > capacity()) {
        T* tmp = new T[newCapacity];
        if (_first) {
            memcpy(tmp, _first, size() * sizeof(T));
            delete[] _first;
        }
        _first = tmp;
        _last = _first + size();
        _end = _first + newCapacity;
    }
}

1.这里的size函数内部通过计算_last - _first来得到返回值。

2.但是我们的_first已经更新过了,这个计算结果会失效。

所以我们提前存储size:

cpp 复制代码
void reserve(size_t newCapacity) {
    if (newCapacity > capacity()) {
        T* tmp = new T[newCapacity];
        size_t size = size();
        if (_first) {
            memcpy(tmp, _first, size * sizeof(T));
            delete[] _first;
        }
        _first = tmp;
        _last = _first + size;
        _end = _first + newCapacity;
    }
}

1.排除参数不合法情况

2.申请新空间。

3.在_first还没更新之前存储size。

4.把原数据拷贝到新空间,后释放旧空间。

5.空间改变,更新各成员变量。

3.4.4 resize

cpp 复制代码
void resize(size_t n = 0,T& val = T()) {
    if (n > size()) {
        reserve(n);
        while (_last != _first + n) {
            push_back(val);
        }
    }
    else {
        _last = _first + n;
    }
}

1.如果参数n比当前size大,则插入参数val,直到当前vector的size为n。

2.否则直接更新_last,表示后面的数据不要了。

测试代码:

cpp 复制代码
vector<int> v1 = { 1,2,3,5,6 };
v1.resize(10,1);

调试:

3.5 改变vector

3.5.1 push_back

cpp 复制代码
void push_back(cosnt T& val) {
    if (_last == _end) {
        size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
        reserve(newCapacity);
    }
    *_last = val;
    ++_last;
}

1.若vector满了(即_last == _end),按照2倍方式扩容。

2.尾插新数据。

3.更新_last。

问题:vector扩容后非法访问
cpp 复制代码
vector<string> v;
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");

调试:

我们原本存的"11111111111111111"可以看到乱码了,这恰巧发生在扩容之后,我们检查reserve。

cpp 复制代码
void reserve(size_t newCapacity) {
    if (newCapacity > capacity()) {
        T* tmp = new T[newCapacity];
        size_t mySize = size();
        if (_first) {
            memcpy(tmp, _first, mySize * sizeof(T));
            delete[] _first;
        }
        _first = tmp;
        _last = _first + mySize;
        _end = _first + newCapacity;
    }
}

解决方案:这里是浅拷贝问题,可以依次赋值。

cpp 复制代码
if (_first) {
    for (size_t i = 0;i < mySize;++i) {
        tmp[i] = _first[i];
    }
    delete[] _first;
}

把拷贝原数据的代码改巴改巴。

调试:

3.5.2 pop_back

cpp 复制代码
void pop_back() {
    if (_first != _last) {
        --_last;
    }
}

直接更新_last。

测试代码:

cpp 复制代码
vector<string> v;
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.pop_back();
for (auto e:v) {
    cout << e << endl;
}

运行:

3.5.3 insert

会返回新的pos。

cpp 复制代码
void insert(iterator pos,const T& val) {
    if (pos <= _last) {
        if (_last == _end) {
            size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
            reserve(newCapacity);
        }
        iterator it = _last;
        while (it != pos) {
            *(it) = *(it - 1);
            --it;
        }
        *(pos) = val;
        ++_last;
    }
}

测试一下:

cpp 复制代码
vector<string> v;
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.insert(v.begin() + 2, "222222222");

运行,依旧会在扩容的部分报错:

这里涉及到迭代器失效问题。

①迭代器失效问题

迭代器(这里指原生指针)指向的空间被销毁,类似于野指针。

我们调试一下刚才的测试代码:

pos:

扩容后:

可以看到pos位置已经被释放了。
解决方案:记录pos相对位置

cpp 复制代码
void insert(iterator pos,const T& val) {
    if (pos <= _last) {
        size_t myPos = pos - _first;
        if (_last == _end) {
            size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
            reserve(newCapacity);
        }
        iterator it = _last;
        while (it != _first + myPos) {
            *(it) = *(it - 1);
            --it;
        }
        _first[myPos] = val;
        ++_last;
    }
}

提前存了个pos的相对位置myPos,再次调试就没问题啦:

②iterator传参失效
cpp 复制代码
vector<int> v = {1,4,2,5,2,6,6};
int input = 4; 
auto p = std::find(v.begin(),v.end(),input);
if(p) {
    v.insert(p,input);
    cout << *p << endl;
}

1.使用find函数找到位置p。

2.如果找到了,则插入数据。

3.输出位置p上的数据。

调试:

观察到迭代器p失效,因为insert内部执行了扩容逻辑。
解决方案:给一个扩容后的pos作为返回值

cpp 复制代码
iterator insert(iterator pos,const T& val) {
    if (pos <= _last) {
        size_t len = pos - _first;
        if (_last == _end) {
            size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;
            reserve(newCapacity);
        }
        pos = _first + len;
        iterator it = _last;
        while (it != pos) {
            *(it) = *(it - 1);
            --it;
        }
        *(pos) = val;
        ++_last;
        return pos;
    }
    return nullptr;
}

需要改改测试代码:

cpp 复制代码
vector<int> v = {1,4,2,5};
int input = 4; 
auto p = std::find(v.begin(),v.end(),input);
if(p) {
	p = v.insert(p,input);
	cout << *p << endl;
}

调试:

3.5.4 erase

待完善代码:

cpp 复制代码
void erase(iterator pos) {
    assert(pos && pos >= _first && pos < _last);
    iterator it = pos;
    while (it != _last - 1) {
        *(it) = *(it + 1);
        ++it;
    }
    --_last;
}

1.检查参数。

2.pos后的数据往前覆盖。

3.更新_last。

测试代码:

cpp 复制代码
vector<int> v = {1,2,1,1,5,1};
auto it = v.begin();
while (it != v.end()) {
    if (*it == 1) {
        v.erase(it);
    }
    ++it;
}

it遍历v,依次erase掉我们不要的数据。

①迭代器失效问题

vs系统判断迭代器已失效,linux上则不会,但我们代码本身确实有问题,具体如图所示:

erase中后面的数据往前覆盖我们不要的值后,it再自增,明显这个2被我们漏下了。
解决方案:执行erase后就不需要执行++it了

cpp 复制代码
while (it != v.end()) {
    if (*it == input) {
        v.erase(it);
    }
    else {
        ++it;
    }
}
②缩容后迭代器失效问题

不排除其他平台erase中可能会执行缩容逻辑,这样就会造成迭代器失效。
解决方案:用erase返回值替代原迭代器。

cpp 复制代码
iterator erase(iterator pos) {
    assert(pos && pos >= _first && pos < _last);
    size_t len = pos - _first;
    if(capacity() == size() * 2){
        // shrink...
    }
    pos = _first + len;
    iterator it = pos;
    while (it != _last - 1) {
        *(it) = *(it + 1);
        ++it;
    }
    --_last;
    return pos;
}

中间加了缩容逻辑。
总结:迭代器失效了需要重新赋值才能访问。

cpp 复制代码
it = v.erase(it);

3.4.5 swap

不能直接调用算法库的swap,需要自己写。

cpp 复制代码
void swap(vector& v) {
	std::swap(_first, v._first);
    std::swap(_last, v._last);
    std::swap(_end, v._end);
}

但是函数内部可以直接调用算法库的swap实现交换,因为三个成员变量都是内置类型。

3.6 访问数据

3.6.1 operator[ ]

cpp 复制代码
T& operator[](size_t pos) {
    assert(pos < size());
    return _first[pos];
}

assert确保参数合法。

测试代码:

cpp 复制代码
vector<string> v;
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
for (size_t i = 0;i < v.size();++i) {
	cout << v[i] << endl;
}

运行:

3.6.2 迭代器

cpp 复制代码
iterator begin() {
    return _first;
}
iterator end() {
    return _last;
}
const_iterator begin() const {
    return _first;
}
const_iterator end() const {
    return _last;
}

俩版本,一个非const,一个const。

容器vector的学习就到这里啦,下一篇 容器list 不久后就会更出来啦~


相关推荐
格林威1 小时前
工业相机图像高速存储(C#版):直接IO存储方法,附海康相机C#实战代码!
开发语言·人工智能·数码相机·c#·工业相机·海康相机·堡盟相机
DynamicsAgg1 小时前
企业数字化底座-k8s企业实践系列第一篇初识
云原生·容器·kubernetes
下雨打伞干嘛2 小时前
手写Promise
开发语言·前端·javascript
Ronin3052 小时前
【Qt常用控件】输入类控件
开发语言·qt·常用控件·输入类控件
健康平安的活着2 小时前
java中事务@Transaction的正确使用和触发回滚机制【经典】
java·开发语言
全栈软件开发2 小时前
中小汽修门店汽修单管理系统PHP源码,数字化管理维修订单与客户信息
开发语言·php
Wenhao.2 小时前
Docker 安装 neo4j
docker·容器·neo4j
Barkamin2 小时前
使用PriorityQueue创建大小堆,解决TOPK问题
java·开发语言