IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> C++要笑着学:vector 常用接口介绍 -> 正文阅读

[数据结构与算法]C++要笑着学:vector 常用接口介绍

?? ???????🤣?爆笑教程?👉??《C++要笑着学》?👈 火速订阅??🔥

💭 写在前面:

本章开始讲解 vector,首先对 vector 进行介绍,然后讲解 vector 常用的接口。像 emplace 等涉及右值引用的接口,我们等后期讲C++11的时候再作讲解。话不多说,直接开讲。


Ⅰ. vector 的介绍及使用

0x00 vector 的介绍

🔍 vector 文档介绍:vector - C++ Reference

vector 是表示可变大小数组的序列容器,我们说 vector 像数组,但是又不像数组。

说它像数组体现在:vector 就像是数组一样,它也是采用连续存储空间来存储元素的。这也就意味着我们可以用下标访问?vector 的元素,和数组一样的高效。

说它不像数组体现在:vector 的大小是可以动态改变的,而且它的大小会被容器自动处理。

② 本质上来说,vector 使用动态分配数组来存储它的元素

当新元素插入时,为了增加存储空间,这个数组就需要被重新分配大小。具体做法是分配一个新的数组,然后将全部元素转移到这个新的数组。就时间而言,这是一个相对代价较高的任务,因为每当一个新的元素加入后,vector 并不会每次都重新分配大小。

vector 分配空间策略:vector 会分配一些额外的空间以适应可能的增长,因此存储空间会比实际需要的存储空间更大。不同的库采用不同的策略权衡空间的使用和重新分配。但是无论如何,重新分配都应该是对数增长的间隔大小,以至于末尾插入一个元素时是在常数时间的复杂度完成的。

因此,vector 占用了更多的存储空间,为了获得管理存储空间的能力,并且由一种有效的方式去动态增长。

④ 与其它动态序列容器相比(deques, lists and forward_lists):

?vector 在访问元素的时候更加高效,在末尾添加和删除元素相对高效。

? 但是对于其它不在末尾的删除和插入操作,效率更低(这个我们下面会详细讲解)。比起 lists forward_lists 统一的迭代器和引用更好。

vector 的底层就是一个动态的顺序表,一个可改变 _size 的数组,不就是动态的顺序表吗,hhh。??

0x01 vector 的初始化

📜 头文件:<vector>

#include <vector>
using namespace std;   // 日常练习,我们可以无顾虑地展开

无参构造:一般用的最多的就是无参:

vector<int> v1;   // 无参构造,创建一个int类型的,空的vector对象

构造并初始化:用?n?个 value 初始化:

vector<int> v2(10, 3);   // 10个3

迭代器区间初始化:begin 👉?end

vector<int> v3(v2.begin(), v2.end());

?还用什么迭代器,直接用拷贝构造不香吗?

拷贝构造:

vector<int> v4(v2);

但是不乏有这种情况,我不要拷贝对象的头尾数据呢?即,不要它的 begin end

vector<int> v3(++v2.begin(), --v2.end());

?这样就少了 v2 的头尾数据了。看,还是有点用处的吧。

对于迭代器区间初始化,它这里的 InputInerator 不一定是 vector Inerator

它是一个模板,所以你传的是谁的迭代器,它就可以实例化出谁的迭代器,

去遍历 first?last?区间,进行初始化。

💬 所以有些场景我们可以这么初始化:

string s("hello world");
vector<char> v5(s.begin(), s.end());

0x02 调试观察 vector

?我们打个断点进入调试,打开监视窗口看看。

#include <iostream>
#include <vector>
#include <string>
using namespace std;

void test_vector1() {
	vector<int> v1;
	vector<int> v2(10, 3);
	vector<int> v3(++v2.begin(), --v2.end());
	vector<int> v4(v2);

	string s("hello world");
	vector<char> v5(s.begin(), s.end());
}

int main(void)
{
	test_vector1();

	return 0;
}

🐞 具体细节如下:

0x03 刍议?string 和 vector 的区别

string vector 有什么区别呢?刚才通过监视观察,感觉 vector char 已经很像 string 了。

? 我们可以思考一个问题:能不能让 vector char 去替代 string 呢?这合适吗?

?答案是否定的。因为 vector char 没有 \0,而 string 结尾是有 \0 的。

那我给 vector char 后面手动装一个 \0 行不?

你要硬是想这么玩,也不是不可以。但是 "术业有专攻" ,他们俩的体系是不一样的!

vector 是顺序表,存的是任意类型,是针对可动态增长的数组的。

string 就只是字符串,我随便举两个例子:

vector 支持正常的增删查改,但是不支持 += (本身也没必要+=),也不支持比较大小。

vector 也没有 c_str 这些东西,因为?string 作为字符串专用的类,能提供专有的接口(比如 +=,find),所以这就是 string 存在的意义。

0x04 关于 vector 的析构、拷贝构造和赋值构造

?至于析构函数,一般情况下我们不需要管,因为它会自动调用。

拷贝构造和赋值构造,vector 的拷贝构造和赋值其实就是深拷贝。

这些我们放在 vector 模拟实现的章节里详细探讨。

Ⅱ. vector 的遍历

0x00 引入

?我们先简单介绍 3 种 vector 的遍历方式,然后再介绍一些访问元素的接口。

其中为了讲解 "下标 + 方括号" 的遍历方式,我们先提前介绍一下 vector push_back

除此之外还有反向迭代器、const 迭代器……我们就不壹壹介绍了,具体可以看文档。

0x01 push_back()

? vector 不能用爽到飞起地 operator+=?

string 能用 += 主要是 string 不仅可以尾插一个字符还可以追加一个字符串。

但是 vector 就只支持一个一个数据的插入和删除,push_back pop_back

我们发现,vectorstring 一样,是没有提供?push_frontpop_front 的,因为挪动数据效率低。

?如果你硬是要去头插头删,也没人拦你,你可以用?inserterase 去操作。

所以我们用?push_back?尾插接口去操作!

vector.push_back(内容);

💬 举个例子:

void vector_Traversal_test() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
}

0x02?下标 + 方括号遍历

?vector 是连续的空间,又支持 operator[] size() ,所以可以用下标+方括号遍历。

💬 下标 + 方括号:遍历

void vector_Traversal_test() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	// 遍历
	for (size_t i = 0; i < v.size(); i++) {
		cout << v[i] << " ";
	}
	cout << endl;
}

🚩 运行结果如下:

💬 当然也是可以修改的:

void vector_Traversal_test() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	// 遍历
	for (size_t i = 0; i < v.size(); i++) {
		v[i] += 1;    // 令每个元素+1
		cout << v[i] << " ";
	}
	cout << endl;
}

🚩 运行结果如下:

0x03?访问 vector 的 at()?

顺便再讲一下 at() ,它和 operator[] 一样,也是用来访问 vector 的,但是 at() 会进行边界检查。

c.at(idx)传回索引 idx 所指的数据,如果 idx 越界,抛异常 out_of_range?

💬 at 用法演示:

void test_vector4() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	cout << v.at(0) << endl;
	cout << v.at(3) << endl;
}

🚩 运行结果如下:

📌 注意事项:operator[] 会用断言检查越界,而 at() 会抛异常。

void test_vector4() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	cout << v.at(5) << endl;  // 故意越界看看
}

0x04?用迭代器遍历vector

iterator 的使用接口说明
begin?+?end?(重点)

获取第一个数据位置的 iterator/const_iterator,

获取最后一个数据的下一个位置的 iterator/const_iterator

rbegin?+?rend

获取最后一个数据位置的 reverse_iterator,

获取第一个数据前一个位置的 reverse_iterator

💬 迭代器的读和写:

void vector_Traversal_test() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
    
    // 用迭代器遍历
	vector<int>::iterator it = v.begin();
	while (it != v.end()) {
		*it -= 1;             // 令每个元素-1
		cout << *it << " ";
		it++;
	}
	cout << endl;
}

🚩 运行结果如下:

0x05?范围 for

vector 支持迭代器,也就支持范围 for,这个我们在模拟实现 string 的时候已经验证过了。

范围 for 的本质就是编译器在编译时自动替换成迭代器,这里也一样。

💬 范围 for 的读和写:

void vector_Traversal_test() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	// 范围for
	for (auto e : v) {
		cout << e << " ";
	}
	cout << endl;

	for (auto& e : v) {
		e += 10;
		cout << e << " ";
	}
	cout << endl;
}

?🚩 运行结果如下:

记得范围 for 的写要加引用,这时老生常谈的问题了。

顺便说一点,原生指针就是天然的迭代器,数组支持范围 for,会被替换成指针:

for (int* p = arr; p < arr + sizeof(arr) / sizeof(int); p++) {}

Ⅲ. vector 空间

容量空间接口说明
size获取数据个数
capacity获取容量大小
empty判断是否为空
resize? ? ?(重点)改变 vector 的 size
reserve? ?(重点)

改变 vector 放入 capacity

0x00 获取数据个数的 size()

💬 和 string 里的一样,是用来获取数据的个数的。

void test_vector2() {
	vector<int> v(6, 6);        // 生成6个6
	cout << v.size() << endl;
}

🚩 运行结果如下:

0x01 获取 vector 最大存储的 max_size()

💬 我们来用 max_size 看看有多大:

void test_vector3() {
	vector<int> v(6, 6);   // 生成6个6
	cout << v.max_size() << endl;
}

🚩 运行结果如下:

0x02 改变 vector 容量的 reserve()

💬 reserve:

void test_vector3() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.reserve(100);
}

🐞 调试结果如下:

reserve 会扩容,但是不会影响数据个数。[capacity] 4? → [capacity]100

0x03 改变 vector 大小的 resize()

?" reserve 和 resize 都是卖房子的(开空间的),reserve 只是把房子卖给你(开空间),而 resize 是一条龙服务,房子卖给你(开空间)之后还帮你搞装修(初始化),你还可以指定装修风格(初始化内容),如果不指定会按默认的简约风格装修(按类型的缺省值初始化)"

💬 resize

void test_vector4() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.resize(100);
}

string resize 如果不指定 "填充值" ,默认给的是 \0

vector resize 如果不指定,默认给的是其对应类型的缺省值作为 "填充值"

这里是 int 就是 0,如果是指针,对应的缺省值就是空指针。

💬 我们来试着给 resize 提供指定 "填充值":

void test_vector5() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.resize(100, 6);   // 开100个空间,用6补
}

📌 注意事项:如果开的数据比之前更小,还会删除数据!

void test_vector4() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.resize(2);
}

当然,正如我们 string 章节所说,它的容量并不会因此改变。

这里虽然大小变成 2,数据也只有 [0]1 和 [1]2 了,但是容量仍然为 4。

0x04?vector 空间增长问题

capacity 的代码在 VS 和 g++下分别运行会发现:VS下 capacity 是按 1.5 倍增长的,而 g++ 下 capacity 是按 2 倍增长的。 这个问题经常会考察,不要固化的认为,顺序表增容都是2倍,具体增长多少是根据具体的需求定义的。VS 是 PJ 版本 STL,g++ 是 SGI 版本 STL。

reserve 只负责开辟空间,如果确定知道需要用多少空间,reserve 可以缓解 vector 增容的代价缺陷问题。

resize 在开空间的同时还会进行初始化,影响 size

💬 测试:

// vector::capacity
#include <iostream>
#include <vector>

int main(void)
{
	size_t sz;
	std::vector<int> foo;
	sz = foo.capacity();
	std::cout << "making foo grow:\n";
	for (int i = 0; i < 100; ++i) {
		foo.push_back(i);
		if (sz != foo.capacity()) {
			sz = foo.capacity();
			std::cout << "capacity changed: " << sz << '\n';
		}
	}
}

🚩 VS运行结果如下:

making foo grow :
capacity changed : 1
capacity changed : 2
capacity changed : 3
capacity changed : 4
capacity changed : 6
capacity changed : 9
capacity changed : 13
capacity changed : 19
capacity changed : 28
capacity changed : 42
capacity changed : 63
capacity changed : 94
capacity changed : 141

🚩 g++ 运行结果如下:

making foo grow :
capacity changed : 1
capacity changed : 2
capacity changed : 4
capacity changed : 8
capacity changed : 16
capacity changed : 32
capacity changed : 64
capacity changed : 128

Ⅳ. vector 增删查改

vector 增删查改接口说明
push_back(重点)尾插
pop_back? (重点)尾删
find? ? ? ? ? ? (#include algorithm)查找(注意这个是算法模块实现,不是 vector 的成员接口)
insert在 pos 之前插入 val
erase删除 pos 位置的数据
swap交换两个 vector 的数据空间
operator[]? (重点)像数组一样访问

??刚才讲 "下标 + 方括号"?的遍历方式的时候已经讲过?push_back 了,

也顺便解释了为什么不提供 push_front pop_front ,所以这里就不多bb了。

0x00?pop_back() 尾删

现在我们把 pop_back 简单介绍一下,pop_back 就是尾删,可用来删除 vector 中的数据。

💬 pop_back

void test_vector5() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	for (auto e : v) cout << e << " "; cout << endl;

	v.pop_back();   // 尾删:3
	for (auto e : v) cout << e << " "; cout << endl;
	
	v.pop_back();   // 尾删:2
	for (auto e : v) cout << e << " "; cout << endl;
}

🚩 运行结果如下:

0x01? assign() 赋值

assign 可以把 vector 的内容覆盖掉。允许给一段区间覆盖,也可以给 n?个 value 去覆盖。

💬 用 n?个 value 覆盖:

void test_vector6() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	v.assign(10, 5);   // 原来是1到4,现在改成10个5
}

🐞 调试结果如下:

0x02?探讨:为什么?vector 不提供 find 接口

stringmapset 都有 find() 用,凭什么?vector list 没有?

太不公平了吧!歧视这是赤裸裸的容器歧视??

其实,我们应该考虑的是 —— 为什么 stringmapset 能有 find 操作。

vector 之所以不提供 find ,是因为如果去查找元素效率就会是?O(n)?……?

?呵呵,凭什么不让我 vector find() ,我偏要用!

可以的,"algorithm库" 里有通用的 find 操作,我们来一睹其芳容 ——

#include <algorithm>

find 内部是从 beginend 进行一次遍历,其复杂度是 O(n)?

值得一提的是,在C++中,凡是使用迭代器区间,都是左闭右开的 ——??(\, first, \, last\, ]?

(map 和 set 接下来的章节会讲,以下部分可先作了解)

再去思考?mapset 为什么有 find()?通过迭代器从头到尾遍历 map 与?set 时,

得到的结果是按 key 排序的结果,而不是插入时的顺序,所以这两个容器没有 push_back 操作。

其实,插入到?map set 中的元素会被组织到一颗红黑树上,红黑树是一颗平衡二叉树,

平衡二叉树是一颗二叉排序树,对一颗二叉排序树的查找有点像二分查找,复杂度是 O(log(n))

由于 map set 的数据结构能有更快的查找方法,所以在容器内提供了 find 方法。

0x03?通用查找?find()

?如果非要在 vector 里用 find() ,我们可以用通用 find

找到了:返回一个迭代器区间那个值的位置;

没找到:返回的是?last?,即右边开区间的位置。

💬 使用方法演示:

void test_vector7() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	vector<int>::iterator ret = find(v.begin(), v.end(), 3);
    // auto ret = find(v.begin(), v.end(), 3);

	if (ret != v.end()) {
		cout << "找到了" << endl;
	}
}

0x04?insert() 插入

?比如我们刚才用通用 find 找到了 3 的位置,

我们想在这个位置前面插入一个数据,就可以使用 insert() 插入。

💬 在 3 前面用 insert 插入一个 30:

void test_vector7() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);

	vector<int>::iterator ret = find(v.begin(), v.end(), 3);
	if (ret != v.end()) {
		cout << "找到了" << endl;
		v.insert(ret, 30);         // 在ret位置前面插入一个30
	}

	for (auto e : v) cout << e << " "; cout << endl;
}

🚩 运行结果如下:

我们刚才说过,虽然没有 vector 没有提供 push_front,但是我们也可以用 insert 去头插。

💬 用 insert 去头插:

void test_vector8() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
    for (auto e : v) cout << e << " "; cout << endl;

	v.insert(v.begin(), -1);   // 在起始位置插入一个-1

	for (auto e : v) cout << e << " "; cout << endl;
}

🚩 运行结果如下:

0x05?erase() 删除

?我们我们想删除数据,我们就可以用 erase 去删除。

💬 使用 erase 的时要判断一下有没有找到要删除的目标:

void test_vector9() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	for (auto e : v) cout << e << " "; cout << endl;

	vector<int>::iterator pos = find(v.begin(), v.end(), 5);
	if (pos != v.end()) {   // 判断pos是否存在
		v.erase(pos);       // 删除pos
	}

	for (auto e : v) cout << e << " "; cout << endl;
}

🚩 运行结果如下:

? 如果没有判断会怎么样?

void test_vector9() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	for (auto e : v) cout << e << " "; cout << endl;

	vector<int>::iterator pos = find(v.begin(), v.end(), 3);
	v.erase(pos);

	for (auto e : v) cout << e << " "; cout << endl;
}

🚩 运行结果如下:

如果要删除的目标存在,不会怎么样。

怕的就是要删的目标不存在!比如我要删个 5,但是 vector 里只有 1,2,3,4 。

vector<int>::iterator pos = find(v.begin(), v.end(), 5);
v.erase(pos);

? (待删目标不存在导致)

? 如果有了判断,就不会翻车了,如果待删目标不存在,就不会去走 erase()? 。?

因为 pos 如果找不到就会等于 end() 上的值,我们利用这一点进行 if 判断,

vector<int>::iterator pos = find(v.begin(), v.end(), 5);
if (pos != v.end()) {   // 检查!
	v.erase(pos);   
}

0x04 clear() 清空数据

💬 clear:

void test_vector10() {
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	
	printf("清空前:");
	for (auto e : v) cout << e << " "; cout << endl;
	
	v.clear();
	printf("清空后:");
	for (auto e : v) cout << e << " "; cout << endl;
}

🚩 运行结果如下:


📜?参考资料

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

. C++reference[EB/OL]. []. http://www.cplusplus.com/reference/.

百度百科[EB/OL]. []. https://baike.baidu.com/.

比特科技. C++[EB/OL]. 2021[2021.8.31]. .

📌 [ 笔者 ]? ?王亦优
📃 [ 更新 ]? ?2022.5.8
? [ 勘误 ]?? /* 暂无 */
📜 [ 声明 ]? ?由于作者水平有限,本文有错误和不准确之处在所难免,
              本人也很想知道这些错误,恳望读者批评指正!

本章完。

  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2022-05-12 16:37:25  更:2022-05-12 16:37:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 3:39:34-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码