④ 数据结构与算法 — 第 3 课
val + next,会用头插法和遍历。p 不动。head 变成 nullptr。头插法会把顺序反过来。尾插法把新节点放到最后,输入和输出顺序就一样了。
void insertTail(Node*& head, int x) {
Node* newNode = new Node{x, nullptr};
if (head == nullptr) { // 链表是空的:新节点就是头
head = newNode;
return;
}
Node* p = head;
while (p->next != nullptr) { // 走到最后一个节点
p = p->next;
}
p->next = newNode; // 最后节点接上新节点
}
调用:
Node* head = nullptr;
insertTail(head, 10);
insertTail(head, 20);
insertTail(head, 30);
// 链表:10 -> 20 -> 30
指针是怎么动的?拿「往 10 → 20 后面接 30」这一趟来看:
| 步骤 | 代码 | 谁指向谁 | 链表 |
|---|---|---|---|
| 初始 | p = head | p → 10 | 10 → 20 |
| 1 | while (p->next != nullptr) p = p->next; | 10->next 是 20,p 走到 20;20->next 是 nullptr,停 | 10 → 20 |
| 2 | newNode = new Node{30, nullptr} | newNode → 30,30 的 next 暂时为空 | 10 → 20 30 |
| 3 | p->next = newNode | 20 的 next 从 nullptr 改成 30 | 10 → 20 → 30 |
尾插唯一容易错的地方是空链表:这时 p = head 就是 nullptr,再去读 p->next 会直接崩。所以循环之前必须先判 head == nullptr。
头插是「每次把新节点塞到队首」,所以先来的反而被挤到后面。用上节课的例子(依次插入 30、20、10):
| 步骤 | 操作 | head 指向 | 链表 |
|---|---|---|---|
| 初始 | head = nullptr | 空 | 空 |
| 1 | insertHead(head, 30) | head → 30 | 30 |
| 2 | insertHead(head, 20) | head → 20 → 30 | 20 → 30 |
| 3 | insertHead(head, 10) | head → 10 → 20 → 30 | 10 → 20 → 30 |
输入 30 20 10,输出 10 20 30:头插就是原地把顺序倒过来。尾插之所以能保持顺序,是因为它每次都老老实实走到队尾,让先来的留在前面。
tail 指针,直接记住尾巴。我们先理解这个简单版,课后练习再升级。
Node* find(Node* head, int x) {
for (Node* p = head; p != nullptr; p = p->next) {
if (p->val == x) return p; // 找到就返回这个节点
}
return nullptr; // 没找到
}
如果返回 nullptr,说明链表中没有这个值。调用方拿到的如果是 nullptr,就不能再写 result->val——先判空再用。
删除分两步处理:先处理头部连续等于 x 的情况,再处理中间节点。
void deleteValue(Node*& head, int x) {
// 1. 如果头节点就是要删的值,把头指针后移
while (head != nullptr && head->val == x) {
Node* tmp = head;
head = head->next;
delete tmp;
}
// 2. 处理中间节点
Node* p = head;
while (p != nullptr && p->next != nullptr) {
if (p->next->val == x) {
Node* tmp = p->next;
p->next = tmp->next; // 跳过 tmp
delete tmp;
} else {
p = p->next; // 只有没删时才往后走
}
}
}
重点:删除成功后 p 不前进,因为新的 p->next 可能还是 x。
拿本课完整示例的链表 10 → 20 → 30 → 20、执行 deleteValue(head, 20),把「谁指向谁」一步步画出来:
| 步骤 | 发生了什么 | 谁指向谁 | 链表 |
|---|---|---|---|
| 0 | 检查头节点 | head->val 是 10,不等于 20,头不动;p 从 10 出发 | 10 → 20 → 30 → 20 |
| 1 | p->next 是 20,命中 | tmp 记住 20;p->next 改成 tmp->next(30) | 10 → 30 → 20 20 已脱链 |
| 2 | 删完 p 不前进 | 新的 p->next 是 30,不等于 20,p 才移到 30 | 10 → 30 → 20 |
| 3 | p->next 是 20,又命中 | tmp 记住 20;p->next 改成 tmp->next(nullptr) | 10 → 30 |
| 4 | p->next 是 nullptr | 循环结束,head 仍指向 10 | 10 → 30 |
第 2 步是整段代码的灵魂:如果删完立刻 p = p->next,就会跳过后一个 20,最后链表中还剩一个。所以删完必须让 p 留在原地,重新检查新的 p->next。
void freeList(Node*& head) {
while (head != nullptr) {
Node* tmp = head;
head = head->next;
delete tmp;
}
}
每次先记住当前节点,头指针后移,再删掉刚记住的节点。顺序是记 → 移 → 删:先 delete head 再读 head->next,读的就是已经释放的内存。最后 head 自动变成 nullptr。
#include <iostream>
using namespace std;
struct Node {
int val;
Node* next;
};
// ==== 建表:尾插 ====
void insertTail(Node*& head, int x) {
Node* newNode = new Node{x, nullptr};
if (head == nullptr) {
head = newNode;
return;
}
Node* p = head;
while (p->next != nullptr) p = p->next;
p->next = newNode;
}
// ==== 查找 ====
Node* find(Node* head, int x) {
for (Node* p = head; p != nullptr; p = p->next) {
if (p->val == x) return p;
}
return nullptr;
}
// ==== 按值删除 ====
void deleteValue(Node*& head, int x) {
while (head != nullptr && head->val == x) {
Node* tmp = head;
head = head->next;
delete tmp;
}
Node* p = head;
while (p != nullptr && p->next != nullptr) {
if (p->next->val == x) {
Node* tmp = p->next;
p->next = tmp->next;
delete tmp;
} else {
p = p->next;
}
}
}
// ==== 打印 ====
void printList(Node* head) {
for (Node* p = head; p != nullptr; p = p->next) {
cout << p->val << " ";
}
cout << endl;
}
// ==== 释放 ====
void freeList(Node*& head) {
while (head != nullptr) {
Node* tmp = head;
head = head->next;
delete tmp;
}
}
// ==== main ====
int main() {
Node* head = nullptr;
insertTail(head, 10);
insertTail(head, 20);
insertTail(head, 30);
insertTail(head, 20);
printList(head); // 10 20 30 20
cout << (find(head, 30) != nullptr) << endl; // 1
deleteValue(head, 20);
printList(head); // 10 30
freeList(head);
return 0;
}
p = nullptr,再读 p->next 直接崩。代码里的 if (head == nullptr) 不能省。tail 指针。p 前进了:连续两个相同的 x 只会删掉一个,后面的被跳过。delete 再取 next:该先 Node* nxt = tmp->next; 记下来,再释放。find 的结果不判空:找不到返回 nullptr,直接 ->val 就是空指针解引用。tail 指针维护链表尾部,让每次 insertTail 都是 O(1),不用从头走到尾。
head == nullptr;循环是不是停在最后一个节点。p->next」而不是 p;删完 p 有没有乱动。nullptr。p 不动,防止漏删。head = nullptr。