链表进阶
一、课上练习
编程练习
二、知识总结
✨ 核心思想
双向链表是单向链表的一个扩展,允许节点间双向遍历。这意味着每个节点不仅保存有指向下一个节点的指针,还保存有指向前一个节点的指针。双向链表的这种结构增加了灵活性,使得许多操作更加高效。
✨ 结构和组成
双向链表的每个节点通常包括三部分:
- 数据域:存储实际的数据。
- 前向指针(prev):指向链表中前一个节点。
- 后向指针(next):指向链表中后一个节点。
链表的起始节点(称为头节点)的前向指针通常指向 nullptr,而结束节点(称为尾节点)的后向指针也指向 nullptr,标志着链表的开始和结束。
✨ 基本操作
双向链表支持的基本操作包括:
- 插入:可以在链表的头部、尾部或两个节点之间插入新节点。与单向链表相比,双向链表在插入时更为高效,因为可以直接访问任一节点的前一个节点。
- 删除:删除操作同样更简单,因为每个节点都知道自己的前一个节点,所以可以直接删除而不必先搜索前一个节点。
- 搜索:可以从头节点或尾节点开始,向前或向后搜索,这在某些情况下可以减少搜索时间。
- 遍历:双向链表可以从头到尾或从尾到头进行遍历,提供了更大的灵活性。
下面的代码展示了双向链表的完整实现,包括插入、删除、更新和打印操作:
1#include<iostream>
2using namespace std;
3
4// 定义双向链表的节点结构
5struct Node {
6 int value; // 节点存储的值
7 Node *next; // 指向下一个节点的指针
8 Node *prev; // 指向上一个节点的指针
9};
10
11// 打印链表的函数
12void print(Node *head) {
13 Node *current = head->next; // 从头节点的下一个节点开始遍历,跳过头节点
14 while (current) { // 当当前节点非空时继续遍历
15 cout << current->value; // 输出当前节点的值
16 current = current->next; // 移动到下一个节点
17 if (current) { // 如果下一个节点存在
18 cout << " "; // 在值之间输出空格
19 }
20 else {
21 cout << endl; // 在链表末尾输出换行符
22 }
23 }
24}
25
26// 向链表中插入节点的函数
27void insert(Node *head, int i, int j) {
28 Node *current = head;
29 int count = 0;
30 // 寻找插入位置的前一个节点
31 while (count < i && current->next) {
32 current = current->next;
33 count++;
34 }
35 // 如果找到了正确的插入位置
36 if (count == i) {
37 Node *node = new Node; // 创建新节点
38 node->value = j; // 设置新节点的值
39 node->next = current->next; // 新节点指向当前节点的下一个节点
40 node->prev = current; // 设置新节点的前向指针
41 if (current->next) { // 如果当前节点后面有节点
42 current->next->prev = node; // 更新后续节点的前向指针
43 }
44 current->next = node; // 当前节点指向新节点,完成插入
45 }
46}
47
48// 删除链表中的节点的函数
49void deleteNode(Node *head, int i) {
50 Node *current = head;
51 int count = 0;
52 // 寻找要删除的节点的前一个节点
53 while (count < i - 1 && current->next) {
54 current = current->next;
55 count++;
56 }
57 // 如果找到了正确的位置,并且确实有节点可以删除
58 if (count == i - 1 && current->next) {
59 Node *dn = current->next; // 暂存要删除的节点
60 current->next = dn->next; // 将当前节点指向要删除节点的下一个节点
61 if (dn->next) { // 如果删除节点后面有节点
62 dn->next->prev = current; // 更新后续节点的前向指针
63 }
64 delete dn; // 释放要删除节点的内存
65 }
66}
67
68// 更新链表中节点的值的函数
69void update(Node *head, int i, int j) {
70 Node *current = head;
71 int count = 0;
72 // 寻找要更新的节点
73 while (count < i && current->next) {
74 current = current->next;
75 count++;
76 }
77 // 如果找到了正确的位置
78 if (count == i) {
79 current->value = j; // 更新节点的值
80 }
81}
82
83int main() {
84 freopen("list_oeprations1.in", "r", stdin); // 重定向标准输入
85 freopen("list_oeprations.out", "w", stdout); // 重定向标准输出
86 Node *head = new Node; // 创建头节点
87 head->next = NULL; // 初始化头节点的next为NULL
88 head->prev = NULL; // 初始化头节点的prev为NULL
89
90 int n = 0;
91 string op;
92 int i = 0;
93 int j = 0;
94 cin >> n; // 读取操作数量
95 for (int k = 0; k < n; k++) {
96 cin >> op; // 读取操作类型
97 if (op == "insert") {
98 cin >> i >> j; // 读取插入操作的参数
99 insert(head, i, j); // 执行插入操作
100 }
101 else if (op == "delete") {
102 cin >> i; // 读取删除操作的参数
103 deleteNode(head, i); // 执行删除操作
104 }
105 else { // "update"操作
106 cin >> i >> j; // 读取更新操作的参数
107 update(head, i, j); // 执行更新操作
108 }
109 }
110 print(head); // 打印最终链表的状态
111 return 0;
112}✨ 执行示例
下面通过一个具体的例子,展示双向链表的插入和删除操作的完整执行过程。
初始状态: 空链表,只有一个头节点 head(head.prev = NULL, head.next = NULL)
操作序列: 依次在位置 0 插入 10,位置 1 插入 20,位置 1 插入 15,然后删除位置 2 的节点。
第 1 步:insert(head, 0, 10) —— 在位置 0 插入值 10
- current 指向 head,count = 0,满足 count == 0
- 创建新节点 node(value=10)
- node->next = head->next(即 NULL)
- node->prev = head
- head->next = node
- 链表状态:head <-> [10] -> NULL
第 2 步:insert(head, 1, 20) —— 在位置 1 插入值 20
- current 从 head 出发,移动 1 步到节点 [10],count = 1
- 创建新节点 node(value=20)
- node->next = [10]->next(即 NULL)
- node->prev = [10]
- [10]->next = node
- 链表状态:head <-> [10] <-> [20] -> NULL
第 3 步:insert(head, 1, 15) —— 在位置 1 插入值 15
- current 从 head 出发,移动 1 步到节点 [10],count = 1
- 创建新节点 node(value=15)
- node->next = [10]->next(即 [20])
- node->prev = [10]
- [20]->prev = node(更新后继节点的前向指针)
- [10]->next = node
- 链表状态:head <-> [10] <-> [15] <-> [20] -> NULL
第 4 步:deleteNode(head, 2) —— 删除位置 2 的节点
- current 从 head 出发,需要找到位置 2 的前一个节点(位置 1)
- current 移动到 [10](count=1),再移动到 [15](count=1,满足 i-1=1)
- dn = [15]->next = [20](要删除的节点)
- [15]->next = [20]->next(即 NULL)
- [20]->next 为 NULL,不需要更新后继节点
- delete [20]
- 链表状态:head <-> [10] <-> [15] -> NULL
✨ 算法评价
优点
- 双向遍历:可以向前或向后遍历链表,增加了遍历的灵活性。
- 高效的插入和删除操作:在任何位置的插入和删除操作都比单向链表更高效,因为不需要搜索前一个节点。
- 灵活性:可以从链表的任一端开始操作,便于实现如双端队列这样的数据结构。
缺点
- 更高的内存消耗:每个节点需要额外的空间来存储另一个指针。
- 更复杂的实现:在处理节点的添加和删除时,必须小心更新两个指针,这使得实现比单向链表更复杂。
✨ 应用场景
双向链表在需要双向遍历或频繁在列表中间进行插入和删除操作的场景中非常有用。例如,它们经常用于实现各种高级数据结构,如双端队列(deque)、链表实现的栈和队列,以及在某些缓存策略(如LRU缓存)中用来维护最近使用的元素。
总的来说,双向链表因其增强的遍历和修改能力,在需要这些特性的多种算法和应用中被广泛使用。
✨ 解题步骤详解
以"合并两个有序链表"为例,介绍如何用双向链表思维解决问题:
问题: 给定两个升序排列的链表,将它们合并为一个新的升序链表。
解题步骤:
- 创建结果链表的头节点:新建一个哨兵头节点,作为合并后链表的起点。
- 设置两个指针:分别指向两个链表的第一个有效节点(跳过各自的头节点)。
- 逐一比较并插入:比较两个指针所指节点的值,将较小的节点链接到结果链表的尾部,并移动对应指针。
- 处理剩余节点:当一个链表遍历完毕后,将另一个链表的剩余部分直接连接到结果链表尾部。
- 输出结果:从结果链表的头节点的 next 开始遍历输出。
关键点: 每次将节点接入结果链表时,不要忘记维护 prev 指针(如果使用双向链表),否则会导致反向遍历出错。
✨ 常见错误
- 插入时忘记更新后继节点的 prev 指针:插入新节点后,如果新节点后面还有节点,必须将后继节点的 prev 指向新节点。遗漏这一步会导致反向遍历时跳过新插入的节点。
- 删除时忘记更新后继节点的 prev 指针:删除节点后,被删除节点的下一个节点的 prev 应该指向被删除节点的前一个节点。
- 指针操作顺序错误:插入时应先设置新节点的 next 和 prev,再修改周围节点的指针。如果先修改了 current->next,就丢失了对原后继节点的引用。
- 忘记释放内存:使用
new创建的节点在删除时必须用delete释放,否则会造成内存泄漏。 - 边界情况处理不当:在链表头部或尾部操作时,某些指针可能为 NULL,需要额外判断,避免对 NULL 指针解引用。
三、课后练习
编程练习
- 字典排序:L3193