Initial commit: C++ courseware

This commit is contained in:
xiaolinquan committed 2026-11-05 22:51:02 +08:00
commit 4a4ee7c287
1789 files changed
+373489

No files matched your search

@@ -0,0 +1,186 @@
### **C++算法将以 链表(二)**
#### **一、 引言**
##### **1.1 问题引入:静态链表的局限性**
前面我们学习了用数组来模拟链表(静态链表),比如开一个 `Next[1005]` 和 `Prev[1005]` 数组。这种方法在算法竞赛中很常见,因为它写起来快。但是它有两个缺点:
1. **空间浪费或溢出**:你必须提前估计好元素的最大数量(比如开 1000 万个大小的数组)。如果最终只有 10 个人排队,剩下的 999 万多个位置就白白浪费了;如果来了 1001 万个人,程序就会因为数组越界而崩溃。
2. **值与下标的绑定**:如果我们要存的数据本身非常大(比如学生的学号是 `202300001`),我们显然不能开一个大小为 2亿 的数组来把学号当作下标。
##### **1.2 价值阐述**
有没有一种办法:来一个人,我就找计算机要一个刚好的位置;走一个人,我就把这个位置还给计算机?完全**按需分配,一点都不浪费**?
这就是我们今天要学习的——**动态内存分配**与**指针**。基于此构建的**动态链表**,突破了数组容量的限制,是计算机底层系统中极为常见的数据结构。同时,我们还将学习 C++ 已经为我们造好的双向链表工具——**`list`**。
---
#### **二、 动态链表的核心:指针与 `new/delete`**
##### **2.1 定义节点(`struct Node`)**
在静态链表中,用数组的**下标**来表示位置(`Next[i] = j`)。
在动态链表中,直接用内存的**真实地址**来表示位置。存放地址的变量,叫做**指针**。
```cpp
struct Node {
int value; // 数据域:节点存储的值
Node* prev; // 指针域:指向前驱节点的地址
Node* next; // 指针域:指向后继节点的地址
};
```
**注意**:`Node*` 中的星号 `*`,代表这是一个**指针类型**。`prev` 和 `next` 里存的不是一个数字,而是一个内存地址。如果一个节点没有后继(比如它是尾节点),我们让指针指向一个特殊的空地址:**`nullptr`**。
##### **2.2 向操作系统 要 和 还 内存**
* **要内存 (`new`)**:
当一个新元素加入时,我们需要在内存中为他申请一块空间。
```cpp
Node* p = new Node;
```
这句代码做了三件事:
1. `new Node`:计算机在堆区找到一块刚好能放下 `Node` 结构体的内存空间。
2. 返回这块内存空间的**内存地址**。
3. `Node* p = ...`:把这个地址保存在指针变量 `p` 中。以后我们就可以通过 `p` 找到这个新元素了。
* **访问指针指向的内容 (`->`)**:
因为 `p` 存的是地址,我们不能用普通的 `.` 来访问数据,必须用**箭头符号 `->`**(代表顺着地址走过去看)。
```cpp
p->value = 10; // 把这块内存里的 value 设置为 10
p->next = nullptr; // 把这块内存里的 next 设置为空
```
* **还内存 (`delete`)**:
当再也不需要使用该元素时,就必须把这块空间还给操作系统,否则会导致**内存泄漏**。
```cpp
delete p; // 释放 p 指向的那块内存空间
```
---
#### **三、 动态双向链表的实战操作**
掌握了 `new` 和 `->`,那么动态链表的操作逻辑和静态链表**完全一模一样**,只是写法换成了指针!
##### **3.1 查找节点 (O(n))**
在动态链表中,我们只有头节点 `head` 的地址,要找某个值,必须从头挨个顺着 `next` 指针往下问。
```cpp
Node* findNode(Node* head, int val) {
Node* current = head; // 从起点出发
while (current != nullptr) { // 只要还没走到空(尽头)
if (current->value == val) {
return current; // 找到了,返回这个节点的地址
}
current = current->next; // 没找到,顺着指针去下一个节点
}
return nullptr; // 找遍了也没找到,返回空地址
}
```
##### **3.2 插入节点:`ins_back(x, p)`**
场景:找到节点 `x`,把新创建的节点 `p` 插在 `x` 和它的后继 `z` 之间。
```cpp
void ins_back(Node* x, Node* p) {
Node* z = x->next; // 提前找到 x 的后继 z
// 设置新节点 p 的前后驱
p->next = z; // p 的后继连向 z
p->prev = x; // p 的前驱连向 x
// 更新原有的 x 和 z
x->next = p; // x 的后继改为 p
if (z != nullptr) { // 如果 z 存在(x 不是尾节点)
z->prev = p; // z 的前驱改为 p
}
}
```
##### **3.3 删除节点:`del(x)`**
场景:将节点 `x` 从链表中删除,并释放内存。
```cpp
// 全局变量有 Node* head 指向开始元素的头指针; 和指向结束位置的尾指针 Node* tail;
void del(Node* x) {
Node* L = x->prev; // x 的前驱
Node* R = x->next; // x 的后继
// 处理前驱的连接
if (L != nullptr) {
L->next = R; // 前驱直接连向后继
} else {
head = R; // 如果没有前驱,说明 x 是头节点,删除后头节点变更为 R
}
// 处理后继的连接
if (R != nullptr) {
R->prev = L; // 后继直接连向前驱
} else {
tail = L; // 如果没有后继,说明 x 是尾节点,更新尾节点为 L
}
delete x; // 手动释放 x 占用的内存空间
}
```
---
#### **四、 C++ 标准库:`list`**
在日常开发和算法竞赛中,C++ 标准库(STL)已经为我们写好了一个方便、极其强大的双向链表工具:**`list`**。而使用前需要包含头文件:`#include <list>`
##### **4.1 创建与初始化**
```cpp
#include <list>
using namespace std;
list<int> a; // 创建一个空的存放 int 的双向链表
list<int> l = {1, 2, 3}; // 创建并初始化为 1, 2, 3
// 获取元素数量
long long count = l.size();
```
##### **4.2 特定增删操作 (O(1))**
* **首尾操作**:
* **在头部/尾部插入元素**:`l.push_front(x);` / `l.push_back(x);`
* **删除头部/尾部元素**:`l.pop_front();` / `l.pop_back();`
* **如何表示每个元素的位置?**:
STL 提供了**迭代器**,用于表示链表中的位置。
* **指向第一个有效元素:**`auto b_it = l.begin();`
* **指向最后一个元素的**下一个**位置(没有实际元素,仅作为结束标志)**:`auto e_it = l.end();`
* **移动**:`it++` (向后移),`it--` (向前移)。
* **访问迭代器指向的元素(解引用):** `cout << *b_it;` ,其中`b_it` 是指向第一个有效元素的迭代器,那么本代码会输出第一个元素的值。
* **指定位置增删 (O(1))**:
* **在迭代器 `it` 指向的位置前插入 `x`:**`l.insert(it, x);`
* **删除 `it` 指向的元素:**`l.erase(it);`
##### **4.3 遍历链表**
遍历链表更加推荐使用基于范围的 `for` 循环:
```cpp
list<int> a = {10, 20, 30};
// val 依次代表链表 a 中的每一个元素
for (int val : a) {
cout << val << " ";
}
// 输出: 10 20 30
// 也可以使用传统的 for 循环方式
for (auto it = a.begin(); it != a.end(); it++) {
// 使用 *it 解引用来获取迭代器指向的元素值
cout << *it << " ";
}
```