目录
[二、vector搜索:find 的使用](#二、vector搜索:find 的使用)
[2.1 find函数的作用](#2.1 find函数的作用)
[2.2 使用find要包含哪个头文件](#2.2 使用find要包含哪个头文件)
[2.3 find的参数怎么理解](#2.3 find的参数怎么理解)
[2.4 find返回的是什么](#2.4 find返回的是什么)
[2.5 *f 是什么意思](#2.5 *f 是什么意思)
[2.6 f - datas.begin() 为什么能得到位置](#2.6 f - datas.begin() 为什么能得到位置)
[2.7 find默认只找第一个匹配项](#2.7 find默认只找第一个匹配项)
[三、vector删除:erase 的使用](#三、vector删除:erase 的使用)
[3.1 erase删除单个元素](#3.1 erase删除单个元素)
[3.2 erase删除后,为什么后面元素会前移](#3.2 erase删除后,为什么后面元素会前移)
[3.3 删除后为什么位置会变化](#3.3 删除后为什么位置会变化)
[3.4 为什么删除多个元素常常从后往前删](#3.4 为什么删除多个元素常常从后往前删)
[3.5 从前往后删为什么容易出问题](#3.5 从前往后删为什么容易出问题)
[3.6 从后往前删为什么更安全](#3.6 从后往前删为什么更安全)
[3.7 删除多个指定值的完整逻辑](#3.7 删除多个指定值的完整逻辑)
[3.8 erase后原迭代器要小心](#3.8 erase后原迭代器要小心)
[四、vector插入:insert 的使用](#四、vector插入:insert 的使用)
[4.1 insert的基本作用](#4.1 insert的基本作用)
[4.2 在指定元素前插入数据](#4.2 在指定元素前插入数据)
[4.3 datas.begin() + 2 是什么意思](#4.3 datas.begin() + 2 是什么意思)
[4.4 插入后为什么后面的元素会后移](#4.4 插入后为什么后面的元素会后移)
[4.5 insert和erase的共同特点](#4.5 insert和erase的共同特点)
[五、vector排序:sort 的使用](#五、vector排序:sort 的使用)
[5.1 排序为什么也要用algorithm头文件](#5.1 排序为什么也要用algorithm头文件)
[5.2 正序排序](#5.2 正序排序)
[5.3 倒序排序](#5.3 倒序排序)
[5.4 greater () 是什么意思](#5.4 greater () 是什么意思)
[5.5 关于 greater () 再补充一点](#5.5 关于 greater () 再补充一点)
[7.1 find的核心要点](#7.1 find的核心要点)
[7.2 erase的核心要点](#7.2 erase的核心要点)
[7.3 insert的核心要点](#7.3 insert的核心要点)
[7.4 sort的核心要点](#7.4 sort的核心要点)
一、前言
在上一篇 4.3.3 中,我们已经把 vector 的基础部分讲清楚了,包括:
vector的定义与初始化push_back()增加元素size()获取大小- 下标访问
- 三种遍历方式
- 迭代器
iterator begin()和end()的含义
如果说上一节解决的是:
"vector 怎么创建、怎么访问、怎么遍历"
那么这一节解决的就是:
"vector 里面的数据怎么查、怎么删、怎么插、怎么排"
这几个操作在实际开发中非常常见。例如:
- 在一组数据中查找某个值
- 删除某个元素
- 在指定位置插入新数据
- 对数据进行升序或降序排序
而 vector 作为连续空间容器,在做这些操作时有一个很重要的特点:
一旦发生删除或插入,后面的元素位置可能整体发生变化。
所以这一节除了讲函数怎么用,更重要的是讲清楚它背后的逻辑。
二、vector搜索:find 的使用
2.1 find函数的作用
在 vector 中,如果我们想查找某个元素,可以使用标准库算法 find。
例如:
cpp
auto f = find(datas.begin(), datas.end(), 7);
它的意思是:
在 datas 这个 vector 中,从开头到结尾,顺序查找值为 7 的元素。
这里的查找方式不是二分,不是哈希,而是最基础的:
从前往后一个一个比。
所以**find**可以理解成:
顺序查找。
2.2 使用find要包含哪个头文件
find 和 sort 一样,都属于标准算法库中的内容,所以要包含头文件:
cpp
#include <algorithm>
如果没有这个头文件,find 和 sort 都无法正常使用。
2.3 find的参数怎么理解
find 的基本格式是:
cpp
find(开始位置, 结束位置, 要查找的值)
例如:
cpp
find(datas.begin(), datas.end(), 7);
这里三个参数分别表示:
(1)开始位置
cpp
datas.begin()
表示从 vector 的第一个元素开始查找。
(2)结束位置
cpp
datas.end()
表示查找到尾后位置为止。
注意:
end() 不是最后一个元素,而是 最后一个元素后面的位置。
(3)目标值
bash
7
表示要找的内容是值为 7 的元素。
所以整句意思就是:
在 datas 的全部范围内,查找值为 7 的元素。
2.4 find返回的是什么
这一点非常重要。
find 返回的不是下标,不是布尔值,而是:迭代器。
也就是说:
- 找到了,就返回指向该元素的迭代器
- 没找到,就返回
datas.end()
2.5 *f 是什么意思
因为 f 是迭代器,它表示一个位置。
而:
cpp
*f
表示取出这个位置上的元素值。
例如如果 f 指向的是元素 7,那么:
cpp
*f
结果就是:
cpp
7
所以:
f是位置*f是位置上的值
这一点和指针很像。
2.6 f - datas.begin() 为什么能得到位置
代码:
cpp
f - datas.begin()
这里表示:
当前找到的位置,和起始位置之间相差了多少个元素。
由于 vector 是连续空间,迭代器支持随机访问,所以可以直接相减。
例如:
datas.begin()指向第 0 个元素f指向第 3 个元素
那么:
cpp
f - datas.begin()
结果就是:
bash
3
这也就相当于该元素的下标位置。
所以这句代码:
cpp
cout << "find " << *f << " in " << f - datas.begin() << endl;
可以理解为:
找到了这个值,它在第几个位置。
2.7 find默认只找第一个匹配项
这个也要特别说明。
例如:
cpp
vector<int> datas{7,2,3,6,5,6,7,8};
auto f = find(datas.begin(), datas.end(), 7);
这里虽然有两个 7,但 find 只会返回:
第一个 7 的位置。
也就是说,它找到第一个匹配项后就停止了,不会继续帮你找后面的所有结果。
如果要找所有匹配位置,就需要自己写循环。
三、vector删除:erase 的使用
3.1 erase删除单个元素
代码:
cpp
datas.erase(f);
这里表示:
删除迭代器 f 所指向的那个元素。
例如:
cpp
auto f = find(datas.begin(), datas.end(), 7);
if (f != datas.end())
{
datas.erase(f);
}
意思就是:
先找到第一个 7,然后把这个 7 删掉。
3.2 erase删除后,为什么后面元素会前移
这一点是 vector 很核心的特性。
因为 vector 底层本质上是:
连续空间。
例如原来数据是:
bash
7 2 3 6 5
如果删掉第一个元素 7,为了保证仍然连续,后面的元素就要整体往前移动:
cpp
2 3 6 5
所以你可以理解成:
删除 vector 中某个位置的元素时,后面的数据会自动"补位"。
这也正因为如此,删除操作相比单纯访问,会有额外开销。
3.3 删除后为什么位置会变化
因为元素前移了。
举个例子:
原数组:
bash
0 1 2 3 4 5
如果删掉下标为 2 的元素,那么原来下标 3、4、5 的元素,删除后都会变成 2、3、4。
也就是说:
删除会改变后续元素的位置。
所以一旦在循环中一边遍历一边删除,就要格外小心。
3.4 为什么删除多个元素常常从后往前删
这是非常重要的工程经验。
cpp
int fdata = 6;
vector<int> datapos;
for (int i = datas.size() - 1; i >= 0; i--)
{
if (datas[i] == fdata)
{
datapos.push_back(i);
}
}
这里是从后往前找所有值为 6 的位置。
为什么这么做?
因为如果你从前往后删,前面删掉一个元素后,后面的下标全部变化,可能会导致:
- 漏删
- 下标错位
- 删除错误元素
而从后往前删,就不会影响前面还没处理的位置。
3.5 从前往后删为什么容易出问题
例如数据是:
bash
1 6 6 8
如果你从前往后删:
- 先删第一个
6- 原来后面的那个
6下标已经变了
删完后变成:
bash
1 6 8
如果循环变量继续正常往后加,就容易跳过它。
所以,删除多个元素时,从前往后直接删,风险很大。
3.6 从后往前删为什么更安全
如果先删后面的元素,前面的元素位置不受影响。
这就是你代码里这一段的意义:
cpp
vector<int> datas{7,2,3,6,5,6,7,8};
int fdata = 6;
vector<int> datapos;
for (int i = datas.size() - 1; i >= 0; i--)
{
if (datas[i] == fdata)
{
datapos.push_back(i);
}
}
先把所有目标位置按"从后往前"的顺序记录下来。
然后再删除:
cpp
for (auto p : datapos)
{
datas.erase(datas.begin() + p);
}
因为删的是后面的元素,所以前面的下标不会被破坏。
3.7 删除多个指定值的完整逻辑
你这段代码完整逻辑是:
(1)先定义要找的值
cpp
int fdata = 6;
表示我们要处理所有值为 6 的元素。
(2)准备一个数组存位置
cpp
vector<int> datapos;
用来保存所有找到的下标。
(3)从后往前找所有位置
cpp
for (int i = datas.size() - 1; i >= 0; i--)
{
if (datas[i] == fdata)
{
datapos.push_back(i);
}
}
如果某个位置的值等于 6,就把这个位置保存下来。
(4)输出这些位置
cpp
for (auto p : datapos)
cout << p << " ";
方便观察哪些位置将要被删除。
(5)按记录的位置逐个删除
cpp
for (auto p : datapos)
{
datas.erase(datas.begin() + p);
}
这就是一种很典型的:
先找位置,再从后往前删。
3.8 erase后原迭代器要小心
这里再补一个工程中很重要的点。
vector 删除元素后,后续元素会移动,所以某些原来的迭代器可能失效。
也就是说:
erase之后,不要随便继续使用原来已经保存的迭代器。
你当前这份代码里,因为删除时重新用的是:
cpp
datas.begin() + p
所以逻辑是安全的。
但如果你把一个老迭代器留着,删除后还继续乱用,就容易出问题。
四、vector插入:insert 的使用
4.1 insert的基本作用
insert 用来在指定位置插入元素。
例如:
cpp
datas.insert(f, 3311);
表示在迭代器 f指向的位置前面,插入一个新元素 3311。
注意是:
插入到 f 的前面。
4.2 在指定元素前插入数据
代码:
cpp
f = find(datas.begin(), datas.end(), 3312);
datas.insert(f, 3311);
这段逻辑是:
先找到值为
3312的位置在这个位置前面插入
3311
假设原来是:
bash
12 3312 334
插入后就变成:
bash
12 3311 3312 334
所以这里本质上是在做:
按目标值定位,再在该位置前插入。
4.3 datas.begin() + 2 是什么意思
代码:
cpp
datas.insert(datas.begin() + 2, 99);
这里的:
cpp
datas.begin() + 2
表示从起始位置往后移动 2 个位置。
也就是:
begin() + 0:第 1 个元素位置begin() + 1:第 2 个元素位置begin() + 2:第 3 个元素位置
所以这句代码的意思是:
在第 3 个位置前插入元素 99。
4.4 插入后为什么后面的元素会后移
和删除一样,因为 vector 是连续空间。
如果你在中间插入一个元素,那么后面的元素必须整体向后移动一个位置,为新元素腾出空间。
例如:
原来:
bash
10 20 30 40
在位置 2 插入 99 后:
bash
10 20 99 30 40
可以看到,原来的 30 和 40 都往后移动了。
4.5 insert和erase的共同特点
无论是 insert 还是 erase,本质上都会影响后续元素位置。
所以你可以把这两个操作统一理解为:
erase:删除当前位置,后面元素前移insert:在当前位置前插入,后面元素后移
这也是为什么它们虽然好用,但做得多时开销会比单纯访问更大。
五、vector排序:sort 的使用
5.1 排序为什么也要用algorithm头文件
sort ****和 find 同样来自标准算法库,所以也要:
cpp
#include <algorithm>
它不是 vector 自己的成员函数,而是通用算法。
这也是 STL 的一个很重要思想:
容器负责存数据,算法负责处理数据。
5.2 正序排序
代码:
cpp
std::sort(datas.begin(), datas.end());
这表示对 datas 中的元素进行排序,默认是:
从小到大。
5.3 倒序排序
代码:
cpp
std::sort(datas.begin(), datas.end(), greater<int>());
这表示:
按从大到小进行排序。
5.4 greater<int>() 是什么意思
greater<int>() 可以理解为:
"按大的在前,小的在后" 的比较规则。
默认 sort 没写第三个参数时,使用的是升序规则。
加上 greater<int>() 之后,就变成降序规则。
5.5 关于 greater<int>() 再补充一点
更规范一点的话,可以额外包含:
cpp
#include <functional>
因为**greater<int> 属于函数对象**相关内容。
有些环境可能不写也能过,但从工程习惯看,写上更严谨。
六、完整代码实战分析
下面把你这一段代码完整整理一下,并做适度规范化。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
int main()
{
std::vector<int> datas{
7,2,3,6,5,6,7,8,0,10,
5,12,3312,334,11
};
// 1. 查找元素7
auto f = find(datas.begin(), datas.end(), 7);
if (f != datas.end())
{
cout << "find " << *f << " in "
<< f - datas.begin() << endl;
datas.erase(f); // 删除找到的第一个7
}
// 2. 在3312前插入3311
f = find(datas.begin(), datas.end(), 3312);
if (f != datas.end())
{
datas.insert(f, 3311);
}
// 3. 在第三个位置插入99
datas.insert(datas.begin() + 2, 99);
// 4. 查找所有值为6的位置
int fdata = 6;
vector<int> datapos;
for (int i = datas.size() - 1; i >= 0; i--)
{
if (datas[i] == fdata)
{
datapos.push_back(i);
}
}
// 输出所有找到的位置
for (auto p : datapos)
cout << p << " ";
cout << "\n";
// 5. 删除所有值为6的元素
for (auto p : datapos)
{
datas.erase(datas.begin() + p);
}
// 输出删除后的结果
for (auto d : datas)
{
cout << d << "-";
}
cout << endl;
// 6. 正序排序
std::sort(datas.begin(), datas.end());
for (auto d : datas)
cout << d << "-";
cout << endl;
// 7. 倒序排序
std::sort(datas.begin(), datas.end(), greater<int>());
for (auto d : datas)
cout << d << "-";
cout << endl;
return 0;
}
七、本节重点总结
7.1 find的核心要点
(1)find 需要 #include <algorithm>
(2)格式是 find(开始, 结束, 目标值)
(3)返回值是迭代器,不是下标
(4)找到则返回目标位置,找不到返回 end()
(5)*f 表示取出找到的值
(6)f - begin() 可以求出位置
7.2 erase的核心要点
(1)erase 可以删除指定位置元素
(2)删除后后续元素会前移
(3)删除会导致后面元素位置变化
(4)删除多个元素时通常从后往前更安全
(5)删除后原有迭代器可能失效,要小心
7.3 insert的核心要点
(1)insert 用于在指定位置前插入元素
(2)可以配合 find 先定位再插入
(3)可以用 begin() + 偏移量 指定位置
(4)插入后后面元素会整体后移
7.4 sort的核心要点
(1)sort 也需要 #include <algorithm>
(2)默认从小到大排序
(3)加 greater<int>() 可以从大到小排序
(4)sort 操作的是一个区间,不局限于 vector