④ 数据结构与算法 — 第 1 课
a + b、cout << a 认识自己的类。数据结构 = 数据的组织方式 + 对这些数据能做什么操作。
同一个「学生名单」,你可以:
选择不同的结构,同样操作的速度可能天差地别。数据结构课就是教你:什么场景选什么结构。
线性表是最简单的结构:元素一个接一个,有顺序。比如:
[10] -> [20] -> [30] -> [40]
它需要支持这些操作:
线性表有两种经典实现:顺序表(数组/vector) 和 链表。这节课先学顺序表。
顺序表就是一块连续内存,像一排连号的柜子。你早就在用了:
int a[5] = {10, 20, 30, 40, 50};
vector<int> v = {10, 20, 30, 40, 50};
「顺序」的意思是:第 i 个元素的地址 = 首地址 + i × 元素大小。所以按下标访问非常快。
v[2] // 直接算出地址,一步拿到
*(v.data() + 2) // 本质上是这个
这就是它能做到 O(1) 按下标访问的全部原因:地址是算出来的,不是走过去的。链表的第 i 个元素可没法这么算,下一课会看到代价。
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3, 4};
// 在下标 2 的位置插入 99
v.insert(v.begin() + 2, 99);
// v 变成:1 2 99 3 4
for (int x : v) cout << x << " ";
return 0;
}
为什么插入后下标 2 后面的元素都往后挪?因为顺序表必须保持连续,所以要先腾位置。
注意 insert 的第一个参数是迭代器,不是下标:要写 v.begin() + 2。
vector<int> v = {1, 2, 3, 4, 5};
// 删除下标 2 的元素(值为 3)
v.erase(v.begin() + 2);
// v 变成:1 2 4 5
同样,删除中间元素后,后面的元素要往前挪,补上空位。
O(n)。
| 操作 | vector 写法 | 复杂度 | 原因 |
|---|---|---|---|
| 按下标访问 | v[i] | O(1) | 直接算地址 |
| 末尾插入 | push_back | O(1) | 通常在末尾有空位 |
| 末尾删除 | pop_back | O(1) | 直接缩短 |
| 中间插入 | insert | O(n) | 后面元素要后移 |
| 中间删除 | erase | O(n) | 后面元素要前移 |
| 查找某个值 | find | O(n) | 必须一个一个扫 |
v[i] 不做检查,i 超出 [0, size()) 就是未定义行为,可能读到垃圾值、也可能直接崩。at() 才会抛异常。v.insert(2, 99) 编译不过,要写 v.insert(v.begin() + 2, 99);erase 同理。erase 之后后面元素整体前移,再 i++ 就跳过了一个。删完不要急着自增。push_back 永远 O(1):它是均摊 O(1),扩容那一次要把旧数据全搬走,偶尔会慢一下——但整体仍然划算。pos name,把 name 插到下标 pos 处;最后按顺序输出整个名单。3 Alice Bob Cindy 1 David,输出 Alice David Bob Cindy。
n 对得上吗?先打印一遍原名单。v.insert(v.begin() + pos, name),还是把下标直接传了进去?pos 会不会等于 v.size()(插到末尾)?这个位置是合法的。erase 之后有没有马上 i++?有就多半跳元素了。Alice David Bob Cindy 对上了吗?没对上就单步打印每一步的 vector。push_back / pop_back 是最快的 vector 操作。