GESP 6级
链表概念
6级 · 指针/STL/复杂度
💡 为什么重要
链表是学习所有高级数据结构(树、图)的基础。与数组相比,链表在插入和删除时不需要移动元素,效率更高。GESP 6级考试常考链表的创建、遍历、插入、删除操作,以及链表与数组的对比分析。理解链表也是理解"指针如何连接数据"的关键。
📖 前置知识
- 指针基础(指针声明、& 取地址、* 解引用、-> 成员访问)
- 结构体(struct)的定义和使用
- new/delete 动态内存分配
- while 循环
🔍 链表是什么?
想象一列火车:每节车厢装着货物(数据),车厢之间通过挂钩连接(指针)。你不需要所有车厢紧挨着停在站台上,它们可以散落在不同地方,只要挂钩连着就行。
链表由节点(Node)组成,每个节点包含两部分:
- 数据域:存数据(如一个 int)
- 指针域:存下一个节点的地址(像火车的挂钩)
最后一个节点的指针域为 nullptr(空),表示链表结束。
📐 链表结构
单链表:
[1|→] → [2|→] → [3|→] → NULL
每个节点:[数据 | 指向下一个的指针]
节点定义:
struct ListNode {
int val; // 数据
ListNode* next; // 指向下一个节点
};
数组 vs 链表:
数组:连续内存,按下标O(1)访问,中间插入/删除O(n)
链表:不连续内存,必须从头遍历O(n)访问,中间插入/删除O(1)
[1|→] → [2|→] → [3|→] → NULL
每个节点:[数据 | 指向下一个的指针]
节点定义:
struct ListNode {
int val; // 数据
ListNode* next; // 指向下一个节点
};
数组 vs 链表:
数组:连续内存,按下标O(1)访问,中间插入/删除O(n)
链表:不连续内存,必须从头遍历O(n)访问,中间插入/删除O(1)
💻 代码详解
📝 示例1:链表的创建和遍历
#include <iostream>
using namespace std;
// 定义链表节点
struct ListNode {
int val; // 数据域:存值
ListNode *next; // 指针域:指向下一个节点
// 构造函数:方便创建节点
ListNode(int x) : val(x), next(nullptr) {
// val 初始化为 x
// next 初始化为 nullptr(不指向任何节点)
}
};
int main() {
// 手动创建 1 → 2 → 3 → NULL
ListNode *head = new ListNode(1); // 创建节点1
head->next = new ListNode(2); // 节点1指向节点2
head->next->next = new ListNode(3); // 节点2指向节点3
// 此时:1 → 2 → 3 → NULL
// 遍历链表:从头开始,沿着next一直走到nullptr
ListNode *cur = head; // cur 从头开始
while (cur != nullptr) { // 当前节点不是空
cout << cur->val; // 打印当前节点的值
if (cur->next != nullptr) {
cout << " → "; // 如果后面还有,打印箭头
}
cur = cur->next; // 移动到下一个节点
}
cout << " → NULL" << endl;
// 输出: 1 → 2 → 3 → NULL
// 释放内存(逐个delete)
cur = head;
while (cur != nullptr) {
ListNode *temp = cur; // 保存当前节点
cur = cur->next; // 先移到下一个
delete temp; // 再释放当前节点
}
return 0;
}
📝 示例2:头插法和尾插法
#include <iostream>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
// ★ 头插法:每次在链表头部插入(顺序会反转)
// 插入 1, 2, 3 后链表变成 3 → 2 → 1 → NULL
ListNode* buildByHeadInsert(int arr[], int n) {
ListNode *head = nullptr; // 初始为空链表
for (int i = 0; i < n; i++) {
ListNode *node = new ListNode(arr[i]);
// 步骤:
// 1. 新节点的next指向当前头
node->next = head;
// 2. 头指针指向新节点
head = node;
// 新节点成为了新的头部
}
return head;
}
// ★ 尾插法:每次在链表尾部插入(保持原始顺序)
// 插入 1, 2, 3 后链表变成 1 → 2 → 3 → NULL
ListNode* buildByTailInsert(int arr[], int n) {
ListNode *head = nullptr; // 头指针
ListNode *tail = nullptr; // 尾指针
for (int i = 0; i < n; i++) {
ListNode *node = new ListNode(arr[i]);
if (head == nullptr) {
// 链表为空,新节点既是头也是尾
head = node;
tail = node;
} else {
// 把新节点接到尾部
tail->next = node;
// 更新尾指针
tail = node;
}
}
return head;
}
// 打印链表的辅助函数
void printList(ListNode *head) {
ListNode *cur = head;
while (cur != nullptr) {
cout << cur->val;
if (cur->next) cout << " → ";
cur = cur->next;
}
cout << " → NULL" << endl;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
cout << "头插法: ";
ListNode *list1 = buildByHeadInsert(arr, 5);
printList(list1);
// 输出: 5 → 4 → 3 → 2 → 1 → NULL(反转了!)
cout << "尾插法: ";
ListNode *list2 = buildByTailInsert(arr, 5);
printList(list2);
// 输出: 1 → 2 → 3 → 4 → 5 → NULL(保持顺序)
return 0;
}
📝 示例3:链表的插入和删除
#include <iostream>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
void printList(ListNode *head) {
ListNode *cur = head;
while (cur) {
cout << cur->val << " → ";
cur = cur->next;
}
cout << "NULL" << endl;
}
// 在节点 prev 后面插入值为 x 的新节点
void insertAfter(ListNode *prev, int x) {
ListNode *node = new ListNode(x);
// 步骤1: 新节点的next指向prev的下一个
node->next = prev->next;
// 步骤2: prev的next指向新节点
prev->next = node;
// 顺序不能反!否则会丢失原后面的节点
}
// 删除节点 prev 后面的那个节点
void deleteAfter(ListNode *prev) {
if (prev->next == nullptr) return; // 后面没节点,不操作
ListNode *target = prev->next; // 要删除的节点
prev->next = target->next; // 跳过target
delete target; // 释放target的内存
}
int main() {
// 创建链表: 1 → 3 → 5 → NULL
ListNode *head = new ListNode(1);
head->next = new ListNode(3);
head->next->next = new ListNode(5);
cout << "原始: ";
printList(head);
// 在1后面插入2: 1 → 2 → 3 → 5
insertAfter(head, 2);
cout << "插入2后: ";
printList(head);
// 在2后面插入4: 1 → 2 → 4 → 3 → 5
insertAfter(head->next, 4);
cout << "插入4后: ";
printList(head);
// 删除2后面的4: 1 → 2 → 3 → 5
deleteAfter(head->next);
cout << "删除4后: ";
printList(head);
return 0;
}
📝 示例4:查找链表长度和搜索
#include <iostream>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
// 求链表长度
int getLength(ListNode *head) {
int count = 0; // 计数器
ListNode *cur = head; // 从头开始
while (cur != nullptr) { // 遍历到末尾
count++; // 每经过一个节点+1
cur = cur->next; // 移到下一个
}
return count; // 返回总数
}
// 在链表中查找值 x,返回节点指针
ListNode* find(ListNode *head, int x) {
ListNode *cur = head;
while (cur != nullptr) {
if (cur->val == x) {
return cur; // 找到了,返回节点
}
cur = cur->next; // 没找到,继续
}
return nullptr; // 遍历完都没找到
}
int main() {
ListNode *head = new ListNode(10);
head->next = new ListNode(20);
head->next->next = new ListNode(30);
cout << "长度: " << getLength(head) << endl;
// 输出: 长度: 3
ListNode *found = find(head, 20);
if (found) {
cout << "找到了: " << found->val << endl;
// 输出: 找到了: 20
} else {
cout << "没找到" << endl;
}
return 0;
}
⚠️ 易错点
- 插入/删除时顺序搞反:插入新节点时,必须先让新节点指向后面,再让前面指向新节点。否则后面的节点就丢失了
- 丢失头指针:如果直接把 head 移动了(如
head = head->next;),就找不到链表开头了。需要用临时变量保存 - 遍历时忘记移动指针:如果 while 循环中没有
cur = cur->next;,会死循环 - 访问 nullptr 的成员:在 cur == nullptr 时访问 cur->val 会崩溃。每次解引用前检查是否为空
- 内存泄漏:new 创建的节点如果不 delete,内存永远无法回收。程序结束前应遍历释放
- 不能用下标访问:链表不像数组,不能用
head[2],必须从头遍历到第2个
🎯 练习建议
- 入门:手动创建 3 个节点的链表,用 while 循环遍历并打印
- 进阶:实现头插法和尾插法,分别构建链表并对比结果
- 挑战:反转一个链表(1→2→3→NULL 变成 3→2→1→NULL)
- 综合:合并两个有序链表,结果仍然有序
- 调试:用纸笔画出每一步操作后链表的状态,这是理解链表最有效的方法