链表(linked list)
二、双向链表(Doubly Linked List )
双向链表基础
双向链表的每个结点都有两个指针域:一个指向前驱结点,一个指向后继结点。 因此它既能向后走,也能向前走,访问、插入、删除都更灵活。
双向链表的结点结构
一个典型双向链表结点包含三个部分:前驱指针域、数据域、后继指针域。 数据域可以有一个或多个,取决于实际要保存的数据;两个指针域负责建立前后连接。
双向链表如何连接
相邻两个结点之间不是只有一条连接,而是有两条方向相反的连接: 前一个结点的 `next` 指向后一个结点,后一个结点的 `prev` 指回前一个结点。 例如:A->next 指向结点 B,B->prev 指向结点 A。
为什么更方便
C++ 代码实现示例
下面代码演示了双向链表的基本操作:头插、尾插、指定结点后插入、删除结点和遍历输出。 重点观察每次插入或删除时,`prev` 和 `next` 两个方向的指针如何同时维护。
/****************************************************************
* 代码作者: Alex Li
* 创建时间: 2023-05-22 16:03:37
* 最后修改: 2025-01-10 22:11:00
* 文件描述: 双向链表(doubly linked list)
****************************************************************/
#include <iostream>
using namespace std;
// 节点结构定义
struct Node {
int data; // 数据域
struct Node* next; // 指向下一个节点的指针
struct Node* prev; // 指向前一个节点的指针
};
struct Node* head = NULL; // 全局头节点,初始化为空
// 在链表的开头插入节点
void insertFront(int data) {
// 分配新节点的内存
struct Node* newNode = new Node;
// 将数据赋值给新节点
newNode->data = data;
// 将新节点的 next 指向当前头节点
newNode->next = head;
// 将新节点的 prev 指向空
newNode->prev = NULL;
// 如果当前链表不为空,将原头节点的 prev 指向新节点
if (head != NULL)
head->prev = newNode;
// 更新头节点为新节点
head = newNode;
}
// 在指定节点之后插入新节点
void insertAfter(struct Node* prev_node, int data) {
// 检查给定的前节点是否为空
if (prev_node == NULL) {
cout << "前一个节点不能为空";
return;
}
// 分配新节点的内存
struct Node* newNode = new Node;
// 将数据赋值给新节点
newNode->data = data;
// 将新节点的 next 指向前节点的 next
newNode->next = prev_node->next;
// 将前节点的 next 指向新节点
prev_node->next = newNode;
// 将新节点的 prev 指向前节点
newNode->prev = prev_node;
// 如果新节点的 next 不为空,将其 next 节点的 prev 指向新节点
if (newNode->next != NULL)
newNode->next->prev = newNode;
}
// 在链表末尾插入新节点
void insertEnd(int data) {
// 分配新节点的内存
struct Node* newNode = new Node;
// 将数据赋值给新节点
newNode->data = data;
// 将新节点的 next 指向空
newNode->next = NULL;
// 临时存储头节点,用于遍历链表
struct Node* temp = head;
// 如果链表为空,将新节点作为头节点
if (head == NULL) {
newNode->prev = NULL;
head = newNode;
return;
}
// 如果链表不为空,遍历到链表末尾
while (temp->next != NULL)
temp = temp->next;
// 将最后一个节点的 next 指向新节点
temp->next = newNode;
// 将新节点的 prev 指向最后一个节点
newNode->prev = temp;
}
// 删除链表中的节点
void deleteNode(struct Node* del_node) {
// 如果头节点或待删除节点为空,则无法删除
if (head == NULL || del_node == NULL)
return;
// 如果待删除节点是头节点,将头节点指向其下一个节点
if (head == del_node)
head = del_node->next;
// 如果待删除节点不是最后一个节点,将其下一个节点的 prev 指向其前一个节点
if (del_node->next != NULL)
del_node->next->prev = del_node->prev;
// 如果待删除节点不是第一个节点,将其前一个节点的 next 指向其下一个节点
if (del_node->prev != NULL)
del_node->prev->next = del_node->next;
// 释放待删除节点的内存
delete del_node;
}
// 打印链表中的所有节点
void displayList() {
struct Node* last;
last = head;
while (last != NULL) { // 遍历链表并打印数据
cout << last->data << " ";
last = last->next;
}
if (head == NULL) // 如果链表为空,打印 NULL
cout << "NULL\n";
cout << endl;
}
int main() {
// 初始化空链表并插入节点
insertEnd(5);
insertFront(1);
insertFront(6);
insertEnd(9);
// 在头节点后插入 11
insertAfter(head, 11);
// 在第二个节点后插入 15
insertAfter(head->next, 15);
// 删除链表中的节点
deleteNode(head->next->next->next->next);
// 打印链表
displayList();
}
一句话记住双向链表
三、循环链表(Circular Linked List )
循环链表基础
循环链表把普通链表的尾部重新接回链表中的起点,让链表不再以空指针结束。 它可以是单向循环链表,也可以是双向循环链表。
单向循环链表
单向循环链表只有一个指针方向。它和单链表最关键的区别是: 最后一个结点的指针不再指向空,而是指向头结点。
双向循环链表
双向循环链表的每个数据结点都有两个指针,分别指向直接后继和直接前驱。 因此从任意一个结点出发,都可以方便地访问它的前驱结点和后继结点。
一句话记住循环链表
四、静态链表
用数组来表示链表,称为静态链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | /**************************************************************** * Description: 静态链表,用过的结点要回收 * Author: Alex Li * Date: 2022-08-04 11:22:42 * LastEditTime: 2024-08-05 19:39:49 ****************************************************************/ #include <iostream> using namespace std; const int N = 100010; // 定义常量N,表示数组的最大长度 int n = 0; // 当前链表节点数 int node[N], node_next[N]; // 分别存储节点的值和下一个节点的下标 int head, idx; // 头节点下标和当前可用的节点下标 int free_list_head; // 空闲链表的头节点 // 初始化链表 void init(){ head = -1; // 初始头节点下标设为-1 idx = 0; // 初始可用节点下标设为0 free_list_head = -1; // 初始空闲链表头节点设为-1 } // 从空闲链表中获取一个空闲下标 int get_free_index() { if (free_list_head == -1) { return idx++; // 如果空闲链表为空,返回当前可用下标并自增 } else { int free_idx = free_list_head; // 获取空闲链表的头节点 free_list_head = node_next[free_list_head]; // 更新空闲链表头节点 return free_idx; // 返回空闲下标 } } // 将x插入到头节点上 void firstNode(int x){ int new_idx = get_free_index(); // 获取一个空闲下标 node[new_idx] = x; // 在该下标处插入节点值x node_next[new_idx] = head; // 当前节点的next指向之前的头节点 head = new_idx; // 更新头节点下标为当前节点下标 n++; // 节点数加1 } // 将x插入到下标为k的点的后面 void add(int k, int x){ int new_idx = get_free_index(); // 获取一个空闲下标 node[new_idx] = x; // 在该下标处插入节点值x node_next[new_idx] = node_next[k]; // 当前节点的next指向下标为k节点的下一个节点 node_next[k] = new_idx; // 更新下标为k节点的next指向当前节点下标 n++; // 节点数加1 } // 删除下标为k的节点的后一个节点 void remove(int k){ if (node_next[k] != -1) { // 确保k节点后面有节点 int remove_idx = node_next[k]; // 获取要删除节点的下标 node_next[k] = node_next[remove_idx]; // 更新k节点的next指向被删除节点的next node_next[remove_idx] = free_list_head; // 将被删除节点插入到空闲链表的头部 free_list_head = remove_idx; // 更新空闲链表头节点 n--; // 节点数减1 } } // 打印链表中的所有节点 void printNode(){ int i = head; // 从头节点开始 while (i != -1) { // 遍历链表直到末尾 cout << node[i] << " "; // 打印当前节点值 i = node_next[i]; // 移动到下一个节点 } cout << endl; } int main(){ int k, option, position, value; init(); // 初始化链表 do { cout << "请选择要执行的操作,输入选项编号:" << endl; cout << "0. 退出" << endl; cout << "1. 插入头节点" << endl; cout << "2. 插入新节点" << endl; cout << "3. 删除节点" << endl; cout << "4. 打印链表" << endl; cin >> option; // 输入选项编号 switch (option) { case 0: break; case 1: cout << "输入头节点的值:" << endl; cin >> value; firstNode(value); break; case 2: cout << "输入新节点的位置(插入到k后面)和值:" << endl; cin >> k >> value; add(k, value); break; case 3: cout << "输入要删除的节点位置:" << endl; cin >> k; remove(k); break; case 4: printNode(); break; default: cout << "请输入正确的选项编号" << endl; } } while (option != 0); return 0; } |
习题:小熊的果篮
题目描述
小熊的水果店里摆放着一排 nn个水果。每个水果只可能是苹果或桔子,从左到右依次用正整数 1,2,…,n 编号。连续排在一起的同一种水果称为一个“块”。小熊要把这一排水果挑到若干个果篮里,具体方法是:每次都把每一个“块”中最左边的水果同时挑出,组成一个果篮。重复这一操作,直至水果用完。注意,每次挑完一个果篮后,“块”可能会发生变化。比如两个苹果“块”之间的唯一桔子被挑走后,两个苹果“块”就变成了一个“块”。请帮小熊计算每个果篮里包含的水果。
输入格式
第一行,包含一个正整数 n,表示水果的数量。
第二行,包含 n 个空格分隔的整数,其中第 i 个数表示编号为 i 的水果的种类,1 代表苹果,0 代表桔子。
输出格式
输出若干行。
第 i 行表示第 i 次挑出的水果组成的果篮。从小到大排序输出该果篮中所有水果的编号,每两个编号之间用一个空格分隔。
输入输出样例
输入 #1
12 1 1 0 0 1 1 1 0 1 1 0 0
输出 1
1 3 5 8 9 11
2 4 6 12
7
10
输入 2
20
1 1 1 1 0 0 0 1 1 1 0 0 1 0 1 1 0 0 0 0
输出2
21 5 8 11 13 14 15 17
2 6 9 12 16 18
3 7 10 19
4 20
说明/提示
【样例解释 #1】
这是第一组数据的样例说明。
所有水果一开始的情况是 [1,1,0,0,1,1,1,0,1,1,0,0],一共有 6 个块。
在第一次挑水果组成果篮的过程中,编号为 1,3,5,8,9,11的水果被挑了出来。
之后剩下的水果是 [1,0,1,1,1,0],一共 4 个块。
在第二次挑水果组成果篮的过程中,编号为 2,4,6,12 的水果被挑了出来。
之后剩下的水果是 [1,1],只有 1 个块。
在第三次挑水果组成果篮的过程中,编号为 7 的水果被挑了出来。
最后剩下的水果是 [1],只有 1 个块。
在第四次挑水果组成果篮的过程中,编号为10 的水果被挑了出来。
【数据范围】
对于 10%10% 的数据,n≤5n≤5。
对于 30%30% 的数据,n≤1000n≤1000。
对于 70%70% 的数据,n≤50000n≤50000。
对于 100%100% 的数据,1≤n≤2×1051≤n≤2×105。
[2021CSP-J-4][洛谷P7912] [4973]
