C++零基础到工程实战(4.3.4):vector数组搜索、删除、插入与排序

目录

一、前言

[二、vector搜索:find 的使用](#二、vector搜索:find 的使用)

[2.1 find函数的作用](#2.1 find函数的作用)

[2.2 使用find要包含哪个头文件](#2.2 使用find要包含哪个头文件)

[2.3 find的参数怎么理解](#2.3 find的参数怎么理解)

(1)开始位置

(2)结束位置

(3)目标值

[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 删除多个指定值的完整逻辑)

(1)先定义要找的值

(2)准备一个数组存位置

(3)从后往前找所有位置

(4)输出这些位置

(5)按记录的位置逐个删除

[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要包含哪个头文件

findsort 一样,都属于标准算法库中的内容,所以要包含头文件:

cpp 复制代码
#include <algorithm>

如果没有这个头文件,findsort 都无法正常使用。


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

如果你从前往后删:

  1. 先删第一个 6
  2. 原来后面的那个 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);

这段逻辑是:

  1. 先找到值为 3312 的位置

  2. 在这个位置前面插入 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

可以看到,原来的 3040 都往后移动了。


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

相关推荐
Fuyo_11192 小时前
带你了解C++的类与对象(中)
c++
小苗卷不动2 小时前
UDP服务端收发流程
linux·c++·udp
Xiu Yan3 小时前
Java 转 C++ 系列:函数模板
java·开发语言·c++
小苗卷不动3 小时前
OJ练习之加减(中等偏难)
c++
我能坚持多久3 小时前
String类常用接口的实现
c语言·开发语言·c++
智者知已应修善业3 小时前
【数字稳压控制DAC/TLC5615驱动】2023-5-27
c++·经验分享·笔记·算法·51单片机
t***5443 小时前
Orwell Dev-C++和Embarcadero Dev-C++哪个更稳定
开发语言·c++
代码中介商3 小时前
C++运行时多态深度解析:从原理到实践
开发语言·c++·多态·虚函数
代码中介商4 小时前
C++ 继承与派生深度解析:存储布局、构造析构与高级特性
开发语言·c++·继承·派生