数据结构初阶|链表:打破连续存储的“灵活王者”
本文详细介绍了链表这一基础数据结构。文章首先通过糖葫芦和钥匙串的生动比喻,解释了链表的非连续存储特性及其节点结构。与数组相比,链表在插入/删除操作上具有O(1)的时间复杂度优势,但查询效率较低(O(n))。作者重点讲解了单链表、双链表和循环链表三种基础类型,并提供了C/C++/Python三种语言的实现代码。通过对比数组,文章分析了链表的优缺点和适用场景,如频繁增删的聊天记录、动态扩容的任务队列等
文章目录
- 前言
- 一、先搞懂:链表到底是什么?
- 二、底层逻辑:为什么链表删改效率这么高?
- 三、链表的核心分类(初阶必掌握)
- 四、链表的优缺点:和数组对比,一目了然
- 五、链表的适用场景:什么时候该用链表?
- 六、新手必避的5个链表坑
- 七、新手实操:3个C/C++/Python单链表练习(附完整代码)
- 八、新手学习建议(附链表+数组学习路线)
- 最后:写给新手的一句话
前言
在上一篇博客中,我们聊了最基础的数据结构——数组,它凭借连续内存的优势,实现了高效的随机访问,但也有着插入、删除效率低、长度固定(静态数组)的致命缺点。那么问题来了:如果我们需要频繁添加、删除数据(比如聊天记录、购物车商品),数组显然不够用,这时候该用什么数据结构呢?
答案就是今天的主角——「链表(Linked List)」。
链表和数组是数据结构初阶的“黄金搭档”,它们互补长短:数组擅长“查询”,链表擅长“删改”。很多新手刚接触链表时,会被“指针”“节点”这些概念吓住,但其实只要用生活中的例子类比,再配合简单的实操,就能轻松吃透它。
今天这篇博客,依旧延续“通俗解读+实操落地”的风格,从“是什么、底层逻辑、优缺点、适用场景,到Python实操练习”,一步步拆解链表的核心知识点,哪怕是零基础,也能彻底搞懂链表,学会在合适的场景中使用它。
一、先搞懂:链表到底是什么?
数组是“一排整齐的抽屉”,所有元素连续存储,靠下标定位;而链表,更像是「一串糖葫芦」—— 每个山楂都是一个“数据节点”,山楂之间靠竹签连接,不用整齐排列,只要能通过竹签找到下一个山楂,就可以串成一串。
官方定义很简洁:链表是由若干个“节点”组成的线性表,每个节点包含“数据域”(存储数据)和“指针域”(存储下一个节点的地址),节点之间通过指针连接,无需占用连续的内存空间。
我们用一个生活中的例子再强化理解:假设你有一串钥匙,每把钥匙上都刻着下一把钥匙的存放位置——你要找最后一把钥匙,不需要把所有钥匙都摆出来,只要从第一把钥匙开始,顺着刻的位置一步步找,就能找到目标。这里的“每把钥匙”就是链表的“节点”,“刻的位置”就是“指针域”,“钥匙串”就是整个链表。
举一个C/C++/Python中的简单类比(后续会写完整实操代码):
C:
# 用结构体模拟链表节点(数据域存值,指针域存下一个节点的地址)
#include <stdio.h>
#include <stdlib.h> // 用于malloc函数分配内存
// 定义链表节点结构体(C语言无类,用结构体实现节点)
typedef struct ListNode {
int val; // 数据域:存储具体数据(与Python版本val对应)
struct ListNode* next; // 指针域:存储下一个节点的地址(默认NULL,代表没有下一个节点)
} ListNode;
// 创建3个节点,串联成一个简单链表:1 → 2 → 3
int main() {
// 1. 分配内存创建节点(C语言需手动为节点分配内存,替代Python的类实例化)
ListNode* node1 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node2 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node3 = (ListNode*)malloc(sizeof(ListNode));
// 2. 给每个节点的数据域赋值(对应Python中ListNode(1)的传参)
node1->val = 1;
node2->val = 2;
node3->val = 3;
// 3. 串联节点(修改指针指向,与Python中node1.next = node2逻辑一致)
node1->next = node2; // 节点1的指针指向节点2
node2->next = node3; // 节点2的指针指向节点3
node3->next = NULL; // 尾节点的指针置为NULL,明确无下一个节点
// 可选:释放内存(避免内存泄漏,新手可先了解)
free(node1);
free(node2);
free(node3);
node1 = NULL;
node2 = NULL;
node3 = NULL;
return 0;
}
C++:
# 用C++类模拟链表节点(替代C语言结构体,封装数据域和指针域)
#include <iostream> // C++标准输出头文件,替代C语言stdio.h
// 定义链表节点类(C++用类实现,比结构体更具封装性,对应原C语言结构体功能)
class ListNode {
public:
int val; // 数据域:存储具体数据(与原C语言val对应)
ListNode* next; // 指针域:存储下一个节点的地址(默认NULL,代表无下一个节点)
// 构造函数(替代C语言手动赋值,初始化节点,简化节点创建)
ListNode(int x) : val(x), next(NULL) {}
};
// 创建3个节点,串联成一个简单链表:1 → 2 → 3
int main() {
// 1. 用new创建节点(C++用new分配内存,替代C语言malloc,自动调用构造函数赋值)
ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(3);
// 2. 串联节点(修改指针指向,逻辑与原C语言、Python代码完全一致)
node1->next = node2; // 节点1的指针指向节点2
node2->next = node3; // 节点2的指针指向节点3
node3->next = NULL; // 尾节点的指针置为NULL,明确无下一个节点
// 可选:释放内存(用delete替代C语言free,避免内存泄漏,新手可先了解)
delete node1;
delete node2;
delete node3;
node1 = NULL;
node2 = NULL;
node3 = NULL;
return 0;
}
Python:
# 用类模拟链表节点(数据域存值,指针域存下一个节点)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val # 数据域:存储具体数据
self.next = next # 指针域:存储下一个节点的地址(默认None,代表没有下一个节点)
# 创建3个节点,串联成一个简单链表:1 → 2 → 3
node1 = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
node1.next = node2 # 节点1的指针指向节点2
node2.next = node3 # 节点2的指针指向节点3
这些段代码虽然简单,但已经包含了链表的核心:节点由“数据+指针”组成,通过指针串联,形成一个完整的链表。和数组不同,这三个节点在内存中可以是分散的,只要指针指向正确,就能找到所有节点。
二、底层逻辑:为什么链表删改效率这么高?
新手最关心的问题:同样是存数据,为什么链表的插入、删除比数组快?核心原因就在于「非连续存储」—— 链表不需要占用连续的内存空间,也不需要通过下标定位,删改数据时,只需要修改“指针的指向”,无需移动其他元素。
我们对比数组和链表的“插入操作”,一眼就能看懂差异:
-
数组插入:比如在数组[1,2,3,4]的中间插入5,需要把3、4往后移动一位,腾出位置给5,元素越多,移动的越多,效率越低(时间复杂度O(n));
-
链表插入:比如在链表1→2→3的中间插入5,只需要做两步:① 新建节点5;② 把节点1的指针从指向节点2,改成指向节点5,再把节点5的指针指向节点2,无需移动任何其他节点(时间复杂度O(1))。
用一张通俗的图理解(文字描述替代):
插入前:1 → 2 → 3
插入后:1 → 5 → 2 → 3
整个过程,只修改了两个指针的指向,和链表的长度无关——哪怕链表有10万个节点,插入一个新节点,也只需要修改两个指针,这就是链表删改效率高的底层逻辑。
同样,链表的删除操作也是如此:比如删除链表1→5→2→3中的节点5,只需要把节点1的指针,从指向节点5改成指向节点2,节点5就会被“遗弃”(后续由垃圾回收机制清理),同样无需移动其他节点。
这里要注意一个细节:链表的“查询效率低”。因为链表没有下标,要找某个节点,必须从“头节点”(第一个节点)开始,顺着指针一步步遍历,直到找到目标节点(时间复杂度O(n))。比如要找链表1→2→3→4中的节点4,必须依次经过1、2、3,才能找到4——这也是链表的致命缺点,和数组的“随机访问”形成鲜明对比。
三、链表的核心分类(初阶必掌握)
链表有很多种分类,但初阶阶段,我们只需要掌握「3种最基础的链表」,后续复杂链表(比如红黑树的底层链表),都是基于这3种扩展的。
1. 单链表(最常用,重点掌握)
我们前面举的例子,都是单链表——每个节点只有一个指针域,只能指向“下一个节点”,不能反向遍历。就像一串糖葫芦,只能从第一个山楂,顺着竹签吃到最后一个,不能倒着吃。
核心特点:
-
结构最简单,实现成本最低,日常开发中最常用;
-
只能单向遍历(从头节点到尾节点),尾节点的指针域为NULL(没有下一个节点);
-
插入、删除效率高(O(1)),查询效率低(O(n))。
适用场景:大多数需要频繁删改、很少随机查询的简单场景(比如简单的任务队列)。
2. 双链表
双链表解决了单链表“不能反向遍历”的问题——每个节点有两个指针域,一个指向“下一个节点”,一个指向“上一个节点”。就像一条双向车道,既能往前走,也能往回走。
核心特点:
-
可以双向遍历,查询某个节点的前驱节点(上一个节点)时,效率更高(O(1));
-
结构比单链表复杂,每个节点需要额外存储一个指针,内存开销略大;
-
删改效率依旧很高(O(1)),查询效率还是O(n)(整体遍历依旧需要逐个查找)。
适用场景:需要双向遍历的场景(比如浏览器的前进后退功能、双向队列)。
3. 循环链表
循环链表的特点是「尾节点的指针域,不指向None,而是指向头节点」,形成一个闭环。就像一个环形跑道,没有起点和终点,顺着指针能一直循环遍历。
核心特点:
-
可以循环遍历,适合需要“循环访问所有节点”的场景;
-
结构简单,基于单链表或双链表就能实现(单链表循环、双链表循环);
-
注意避免“死循环”(遍历时分清终止条件)。
适用场景:循环队列、约瑟夫环问题等需要循环访问的场景(初阶阶段了解即可)。
四、链表的优缺点:和数组对比,一目了然
链表和数组是初阶数据结构的“互补搭档”,单独说优缺点没有意义,结合数组对比,才能更清晰地知道什么时候该用链表、什么时候该用数组。
1. 链表的核心优点
-
插入、删除效率极高:无需移动元素,只修改指针指向,时间复杂度O(1)(初阶重点关注单链表的表头/表尾删改,中间删改需先遍历找到节点,整体还是O(n),但核心删改操作本身是O(1));
-
动态扩容,无需固定长度:链表的节点可以动态创建,只要有内存,就能一直添加节点,无需像静态数组那样提前定义长度,也不会出现内存溢出或浪费的情况;
-
内存利用率高:无需占用连续的内存空间,零散的内存碎片也能被利用(数组需要连续内存,哪怕有足够的总内存,但没有连续的空间,也无法创建数组)。
2. 链表的明显缺点
-
查询效率低:没有下标,无法随机访问,只能从头节点依次遍历,元素越多,查询越慢,时间复杂度O(n);
-
内存开销略大:每个节点除了存储数据,还需要额外存储指针(单链表一个指针,双链表两个指针),相比数组,多了一部分内存开销;
-
实现复杂:相比数组,链表需要手动定义节点、管理指针指向,容易出现“空指针”“野指针”“死循环”等问题,新手入门难度略高。
补充:数组 vs 链表(初阶核心对比表)
|
对比维度 |
数组 |
链表(单链表) |
|---|---|---|
|
存储方式 |
连续内存 |
非连续内存(节点+指针) |
|
查询效率 |
高(O(1),随机访问) |
低(O(n),只能遍历) |
|
插入/删除效率 |
低(O(n),需移动元素) |
高(O(1),仅修改指针) |
|
长度特性 |
静态数组固定,动态数组自动扩容 |
动态扩容,无需固定长度 |
|
内存开销 |
小(仅存储数据) |
大(数据+指针) |
五、链表的适用场景:什么时候该用链表?
结合链表的优缺点和与数组的对比,链表的适用场景核心原则是:数据量不固定、需要频繁插入/删除、很少随机查询。
举几个日常开发和学习中的常见场景:
-
频繁删改的线性数据:比如聊天记录(随时发送消息、删除消息,不需要随机查询某条消息的位置,只要按顺序查看)、购物车商品(添加、删除商品频繁,查询时按顺序浏览即可);
-
动态扩容需求强烈的场景:比如日志记录(日志数量不确定,会不断新增,不需要频繁查询历史日志)、任务队列(随时添加任务、执行任务(删除),无需固定队列长度);
-
内存碎片化严重的场景:比如嵌入式开发(内存空间有限,碎片化严重,无法分配连续内存给数组,链表的非连续存储特性更适配);
-
复杂数据结构的基础:比如栈、队列的底层实现(可以用链表实现,解决数组长度固定的问题)、哈希表的冲突解决(链地址法)、树和图的底层连接(节点之间的关联的本质是链表指针)。
反例:如果需要频繁随机查询(比如排行榜,经常查询第n名的分数),就不适合用链表,应该选择数组。
六、新手必避的5个链表坑
链表的入门难度比数组略高,新手很容易在“指针”“节点”上踩坑,总结了5个最常见的坑,避开这些,你的链表学习会更顺畅,写代码时也能减少报错。
-
空指针异常(最常见):访问了“空节点”的指针域或数据域。比如链表的尾节点next是NULL,你却试图访问tail->next->val,就会报错(C++中会触发空指针访问错误);解决方法:操作指针前,先判断节点是否为NULL。
-
忘记修改指针指向:插入、删除节点时,只修改了一个指针,导致链表“断裂”。比如插入节点时,只改了前一个节点的next,没改新节点的next,就会导致后续节点丢失。
-
混淆单链表和双链表的遍历逻辑:单链表只能单向遍历(从头到尾),双链表可以双向遍历,但新手容易在双链表中写错指针指向(比如把prev写成next)。
-
遍历链表时死循环:循环链表中,没有设置正确的终止条件;或者修改指针时,不小心让某个节点的next指向了前面的节点(比如让尾节点next指向头节点,遍历时分不清终止条件)。
-
误以为链表所有删改都是O(1)效率:只有“已知前驱节点”的删改,效率才是O(1);如果需要先遍历找到目标节点(比如删除链表中间某个指定值的节点),整体效率还是O(n)——因为遍历占了主要时间。
七、新手实操:3个C/C++/Python单链表练习(附完整代码)
学链表,光看概念和理论没用,必须动手写代码——实操一遍,比背10遍指针逻辑都管用。下面3个练习,从简单到复杂,覆盖单链表的核心操作(遍历、插入、删除),新手可以直接复制代码运行,跟着理解每一步的逻辑。
提示:
C语言中没有原生的链表结构,我们用「结构体」模拟节点,手动管理指针指向和内存分配(malloc分配、free释放),贴合初阶学习的核心(理解节点和指针)。
C++/Python中没有原生的链表结构,我们用「类」模拟节点,手动管理指针指向和内存分配,贴合初阶学习的核心(理解节点和指针)。
练习1:单链表遍历(打印所有节点的值)
核心逻辑:从头节点开始,顺着next指针,逐个访问节点,打印数据,直到节点为NULL/None(尾节点之后)。
C:
# 1. 定义链表节点结构体(C语言无类,用结构体模拟节点,替代Python的类)
#include <stdio.h>
#include <stdlib.h> // 用于malloc分配内存、free释放内存
// 链表节点结构体:包含数据域和指针域,与Python的ListNode类功能一致
typedef struct ListNode {
int val; // 数据域:存储具体数据(对应Python中的self.val)
struct ListNode* next; // 指针域:存储下一个节点的地址(对应Python中的self.next)
} ListNode;
// 2. 定义单链表遍历函数(对应Python的traverse_linkedlist函数)
void traverse_linkedlist(ListNode* head) {
ListNode* current = head; // 从头部节点开始遍历(对应Python中的current = head)
// 终止条件:当前节点为空(遍历到尾节点之后),C语言用!=NULL替代Python的is not None
while (current != NULL) {
// C语言用printf输出,替代Python的print,保持输出格式一致
printf("%d → ", current->val); // 指针操作需用->,替代Python的.
current = current->next; // 移动到下一个节点,指针赋值
}
printf("NULL\n"); // 打印尾节点之后的空指针,更直观,与原输出格式一致
}
// 3. 测试代码(创建链表+遍历,与Python测试场景完全一致,确保功能可验证)
int main() {
// 创建节点,串联成链表:1 → 2 → 3 → 4(C语言需用malloc手动分配内存)
ListNode* node1 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node2 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node3 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node4 = (ListNode*)malloc(sizeof(ListNode));
// 给每个节点的数据域赋值(对应Python中ListNode(1)的传参初始化)
node1->val = 1;
node2->val = 2;
node3->val = 3;
node4->val = 4;
// 串联节点(修改指针指向,对应Python中的node1.next = node2)
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL; // 尾节点指针置为NULL,明确无下一个节点
// 调用遍历函数,输出:1 → 2 → 3 → 4 → NULL(与原Python代码输出一致)
traverse_linkedlist(node1);
// 释放内存,避免内存泄漏(C语言关键步骤,Python自动垃圾回收,无需手动操作)
free(node1);
free(node2);
free(node3);
free(node4);
// 置空指针,避免野指针(C语言安全规范,新手需重点注意)
node1 = NULL;
node2 = NULL;
node3 = NULL;
node4 = NULL;
return 0;
}
C++:
# 1. 复用链表节点类(与前文示例一致)
#include <iostream>
using namespace std;
class ListNode {
public:
int val;
ListNode* next;
ListNode(int x) : val(x), next(NULL) {}
};
// 2. 定义单链表遍历函数
void traverse_linkedlist(ListNode* head) {
ListNode* current = head; // 从头部节点开始遍历
while (current != NULL) { // 终止条件:当前节点为空(遍历到尾节点之后)
cout << current->val << " → "; // 打印当前节点的值,C++用cout输出
current = current->next; // 移动到下一个节点
}
cout << "NULL" << endl; // 打印尾节点之后的空指针,更直观
}
// 3. 测试代码(创建链表+遍历)
int main() {
// 创建节点,串联成链表:1 → 2 → 3 → 4(用new创建节点,调用构造函数赋值)
ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(3);
ListNode* node4 = new ListNode(4);
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
// 调用遍历函数,输出:1 → 2 → 3 → 4 → NULL
traverse_linkedlist(node1);
// 释放内存,避免内存泄漏(C++需手动释放new分配的内存)
delete node1;
delete node2;
delete node3;
delete node4;
node1 = NULL;
node2 = NULL;
node3 = NULL;
node4 = NULL;
return 0;
}
Python:
# 1. 定义链表节点类
class ListNode:
def __init__(self, val=0, next=None):
self.val = val # 数据域
self.next = next # 指针域
# 2. 定义单链表遍历函数
def traverse_linkedlist(head):
current = head # 从头部节点开始遍历
while current is not None: # 终止条件:当前节点为空(遍历到尾节点之后)
print(current.val, end=" → ") # 打印当前节点的值
current = current.next # 移动到下一个节点
print("None") # 打印尾节点之后的空指针,更直观
# 3. 测试代码(创建链表+遍历)
if __name__ == "__main__":
# 创建节点,串联成链表:1 → 2 → 3 → 4
node1 = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
node4 = ListNode(4)
node1.next = node2
node2.next = node3
node3.next = node4
# 调用遍历函数,输出:1 → 2 → 3 → 4 → None
traverse_linkedlist(node1)
练习2:单链表插入(在指定下标插入节点)
核心逻辑:找到插入位置的前驱节点(上一个节点),修改前驱节点的next指向新节点,新节点的next指向原前驱节点的下一个节点;注意处理“插入到表头”“插入到表尾”“下标越界”三种特殊情况。
C:
# 1. 复用链表节点结构体(和练习1一致,C语言用结构体模拟节点)
#include <stdio.h>
#include <stdlib.h> // 用于malloc分配内存、free释放内存
// 链表节点结构体:与练习1完全一致,可直接复用
typedef struct ListNode {
int val; // 数据域:存储具体数据
struct ListNode* next; // 指针域:存储下一个节点的地址
} ListNode;
// 2. 单链表插入函数(在指定下标index插入值为val的节点)
// 返回值为ListNode*,用于处理插入表头后头节点变化的情况
ListNode* insert_node(ListNode* head, int index, int val) {
// 特殊情况1:插入到表头(index=0),新节点成为新的头节点
// C语言用malloc手动分配节点内存,替代C++的new
ListNode* new_node = (ListNode*)malloc(sizeof(ListNode));
if (new_node == NULL) { // 额外增加内存分配失败判断,C语言安全规范
printf("内存分配失败,插入失败\n");
return head;
}
new_node->val = val; // 给新节点数据域赋值,替代C++构造函数
new_node->next = NULL; // 初始化新节点指针域为NULL,避免野指针
if (index == 0) {
new_node->next = head; // 新节点的next指向原头节点,与原逻辑一致
return new_node; // 返回新的头节点,更新链表头部
}
// 找到插入位置的前驱节点(index-1位置的节点)
ListNode* current = head; // 当前遍历节点,从表头开始
ListNode* prev = NULL; // 前驱节点,初始为空(指向当前节点的上一个节点)
int count = 0; // 记录当前遍历到的下标,用于定位插入位置
// 遍历找到index-1位置的节点,同时判断下标是否越界
while (current != NULL && count < index) {
prev = current; // 先记录当前节点为前驱节点
current = current->next;// 移动到下一个节点,指针操作需用->
count++; // 下标计数自增
}
// 特殊情况2:下标越界(count < index,说明遍历到尾节点仍未到指定下标)
if (count < index) {
printf("下标越界,插入失败\n"); // C语言用printf输出,替代C++的cout
free(new_node); // 释放未使用的新节点,避免内存泄漏
new_node = NULL; // 置空指针,避免野指针
return head; // 插入失败,返回原头节点,链表不变
}
// 正常插入:修改指针指向,完成节点插入(核心逻辑与原代码一致)
prev->next = new_node; // 前驱节点的next指向新节点
new_node->next = current; // 新节点的next指向原前驱节点的下一个节点
return head; // 表头未变,返回原头节点
}
// 3. 测试代码(与原测试场景一致,确保功能可验证,适配C语言语法)
int main() {
// 原链表:1 → 2 → 3 → 4,用malloc创建节点并串联(替代C++的new)
ListNode* node1 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node2 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node3 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node4 = (ListNode*)malloc(sizeof(ListNode));
// 给每个节点赋值,初始化指针域,替代C++构造函数
node1->val = 1;
node1->next = node2;
node2->val = 2;
node2->next = node3;
node3->val = 3;
node3->next = node4;
node4->val = 4;
node4->next = NULL; // 尾节点指针置为NULL,明确无下一个节点
// 在index=2插入值为5的节点,插入后:1 → 2 → 5 → 3 → 4
ListNode* new_head = insert_node(node1, 2, 5);
// 遍历验证(复用练习1的遍历逻辑,简化实现,便于快速验证结果)
void traverse(ListNode* head) {
ListNode* current = head;
while (current != NULL) {
printf("%d → ", current->val);
current = current->next;
}
printf("NULL\n");
}
traverse(new_head); // 调用遍历函数,打印插入后的链表
// 释放内存,避免内存泄漏(C语言关键步骤,逐节点释放)
ListNode* temp;
current = new_head; // 从新头节点开始释放
while (current != NULL) {
temp = current; // 先保存当前节点
current = current->next; // 移动到下一个节点
free(temp); // 释放当前节点内存,替代C++的delete
temp = NULL; // 置空临时指针,避免野指针
}
new_head = NULL; // 置空头指针,避免野指针
return 0;
}
C++:
# 1. 复用节点类(和练习1一致,C++用类模拟链表节点,封装数据域和指针域)
#include <iostream>
using namespace std;
// 链表节点类(与练习1完全一致,可直接复用)
class ListNode {
public:
int val; // 数据域:存储具体数据
ListNode* next; // 指针域:存储下一个节点的地址
// 构造函数:初始化节点,替代Python中__init__方法
ListNode(int x) : val(x), next(NULL) {}
};
// 2. 单链表插入函数(在指定下标index插入值为val的节点)
// 返回值为ListNode*,用于处理插入表头后头节点变化的情况
ListNode* insert_node(ListNode* head, int index, int val) {
// 特殊情况1:插入到表头(index=0),新节点成为新的头节点
ListNode* new_node = new ListNode(val); // C++用new创建节点,自动调用构造函数
if (index == 0) {
new_node->next = head; // 新节点的next指向原头节点,与Python逻辑一致
return new_node; // 返回新的头节点,更新链表头部
}
// 找到插入位置的前驱节点(index-1位置的节点)
ListNode* current = head; // 当前遍历节点,从表头开始
ListNode* prev = NULL; // 前驱节点,初始为空(指向当前节点的上一个节点)
int count = 0; // 记录当前遍历到的下标,用于定位插入位置
// 遍历找到index-1位置的节点,同时判断下标是否越界
// C++中判断节点非空用!=NULL,替代Python中的is not None
while (current != NULL && count < index) {
prev = current; // 先记录当前节点为前驱节点
current = current->next;// 移动到下一个节点,指针操作需用->
count++; // 下标计数自增
}
// 特殊情况2:下标越界(count < index,说明遍历到尾节点仍未到指定下标)
if (count < index) {
cout << "下标越界,插入失败" << endl; // C++用cout输出,替代Python的print
delete new_node; // 释放未使用的新节点,避免内存泄漏(C++需手动管理内存)
return head; // 插入失败,返回原头节点,链表不变
}
// 正常插入:修改指针指向,完成节点插入(核心逻辑与Python一致)
prev->next = new_node; // 前驱节点的next指向新节点
new_node->next = current; // 新节点的next指向原前驱节点的下一个节点
return head; // 表头未变,返回原头节点
}
// 3. 测试代码(与Python测试场景一致,确保功能可验证)
int main() {
// 原链表:1 → 2 → 3 → 4,用new创建节点并串联
ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(3);
ListNode* node4 = new ListNode(4);
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL; // 尾节点指针置为NULL,明确无下一个节点
// 在index=2插入值为5的节点,插入后:1 → 2 → 5 → 3 → 4
ListNode* new_head = insert_node(node1, 2, 5);
// 遍历验证(复用练习1的遍历逻辑,简化实现,便于快速验证结果)
void traverse(ListNode* head) {
ListNode* current = head;
while (current != NULL) {
cout << current->val << " → ";
current = current->next;
}
cout << "NULL" << endl;
}
traverse(new_head); // 调用遍历函数,打印插入后的链表
// 释放内存,避免内存泄漏(C++关键步骤,新手需重点注意)
ListNode* temp;
current = new_head; // 从新头节点开始释放
while (current != NULL) {
temp = current; // 先保存当前节点
current = current->next; // 移动到下一个节点
delete temp; // 释放当前节点内存
}
new_head = NULL; // 置空指针,避免野指针
temp = NULL; // 置空临时指针
return 0;
}
Python:
# 1. 复用节点类(和练习1一致)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# 2. 单链表插入函数(在指定下标index插入值为val的节点)
def insert_node(head, index, val):
# 特殊情况1:插入到表头(index=0),新节点成为新的头节点
new_node = ListNode(val)
if index == 0:
new_node.next = head # 新节点的next指向原头节点
return new_node # 返回新的头节点
# 找到插入位置的前驱节点(index-1位置的节点)
current = head
prev = None # 前驱节点,初始为空
count = 0 # 记录当前遍历到的下标
# 遍历找到index-1位置的节点,同时判断下标是否越界
while current is not None and count < index:
prev = current
current = current.next
count += 1
# 特殊情况2:下标越界(count < index,说明遍历到尾节点仍未到指定下标)
if count < index:
print("下标越界,插入失败")
return head
# 正常插入:修改指针指向
prev.next = new_node # 前驱节点的next指向新节点
new_node.next = current # 新节点的next指向原前驱节点的下一个节点
return head # 返回原头节点(表头未变)
# 3. 测试代码
if __name__ == "__main__":
# 原链表:1 → 2 → 3 → 4
node1 = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
node4 = ListNode(4)
node1.next = node2
node2.next = node3
node3.next = node4
# 在index=2插入值为5的节点,插入后:1 → 2 → 5 → 3 → 4
new_head = insert_node(node1, 2, 5)
# 遍历验证
def traverse(head):
current = head
while current:
print(current.val, end=" → ")
current = current.next
print("None")
traverse(new_head)
练习3:单链表删除(删除指定下标的节点)
核心逻辑:找到删除节点的前驱节点,将前驱节点的next指向删除节点的下一个节点,手动释放删除节点的内存(避免内存泄漏);同样需要处理“删除表头”“删除表尾”“下标越界”三种特殊情况。
C:
# 1. 复用链表节点结构体(和练习1、2一致,C语言用结构体模拟节点)
#include <stdio.h>
#include <stdlib.h> // 用于malloc分配内存、free释放内存
// 链表节点结构体:与练习1、2完全一致,可直接复用
typedef struct ListNode {
int val; // 数据域:存储具体数据
struct ListNode* next; // 指针域:存储下一个节点的地址
} ListNode;
// 2. 单链表删除函数(删除指定下标index的节点)
// 返回值为ListNode*,用于处理删除表头后头节点变化的情况
ListNode* delete_node(ListNode* head, int index) {
// 特殊情况1:链表为空,无法删除
if (head == NULL) {
printf("链表为空,删除失败\n"); // C语言用printf输出,替代Python的print
return NULL;
}
// 特殊情况2:删除表头(index=0),新头节点为原头节点的next
if (index == 0) {
ListNode* temp = head; // 保存原头节点,用于后续释放内存(C语言关键步骤)
ListNode* new_head = head->next; // 新头节点指向原头节点的下一个节点
free(temp); // 释放原头节点内存,避免内存泄漏(替代Python自动垃圾回收)
temp = NULL; // 置空指针,避免野指针(C语言安全规范)
return new_head; // 返回新的头节点,更新链表头部
}
// 找到删除节点的前驱节点(index-1位置的节点)
ListNode* current = head; // 当前遍历节点,从表头开始
ListNode* prev = NULL; // 前驱节点,初始为空(指向当前节点的上一个节点)
int count = 0; // 记录当前遍历到的下标,用于定位删除节点
// 遍历找到index-1位置的节点,同时判断下标是否越界
// C语言用!=NULL替代Python的is not None,指针操作需用->
while (current != NULL && count < index) {
prev = current; // 先记录当前节点为前驱节点
current = current->next;// 移动到下一个节点
count++; // 下标计数自增
}
// 特殊情况3:下标越界(current为空,说明没有该下标对应的节点)
if (current == NULL) {
printf("下标越界,删除失败\n"); // C语言用printf输出,替代Python的print
return head; // 删除失败,返回原头节点,链表不变
}
// 正常删除:修改前驱节点的指针,跳过删除节点(核心逻辑与原代码一致)
prev->next = current->next; // 前驱节点的next指向删除节点的下一个节点
free(current); // 释放删除节点内存,避免内存泄漏(C语言关键步骤)
current = NULL; // 置空指针,避免野指针
return head; // 表头未变,返回原头节点
}
// 3. 测试代码(与原Python测试场景完全一致,适配C语言语法,可直接运行)
int main() {
// 原链表:1 → 2 → 5 → 3 → 4(复用练习2插入后的链表)
// C语言用malloc手动分配节点内存,替代Python的类实例化
ListNode* node1 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node2 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node5 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node3 = (ListNode*)malloc(sizeof(ListNode));
ListNode* node4 = (ListNode*)malloc(sizeof(ListNode));
// 给每个节点赋值,初始化指针域,替代Python的ListNode传参
node1->val = 1;
node1->next = node2;
node2->val = 2;
node2->next = node5;
node5->val = 5;
node5->next = node3;
node3->val = 3;
node3->next = node4;
node4->val = 4;
node4->next = NULL; // 尾节点指针置为NULL,明确无下一个节点
// 删除index=2的节点(值为5),删除后:1 → 2 → 3 → 4
ListNode* new_head = delete_node(node1, 2);
// 遍历验证(复用练习1、2的遍历逻辑,简化实现,便于快速验证结果)
void traverse(ListNode* head) {
ListNode* current = head;
while (current != NULL) {
printf("%d → ", current->val);
current = current->next;
}
printf("NULL\n");
}
traverse(new_head); // 调用遍历函数,打印删除后的链表
// 释放内存,避免内存泄漏(C语言关键步骤,逐节点释放,替代Python自动回收)
ListNode* temp;
current = new_head; // 从新头节点开始释放
while (current != NULL) {
temp = current; // 先保存当前节点
current = current->next; // 移动到下一个节点
free(temp); // 释放当前节点内存
temp = NULL; // 置空临时指针,避免野指针
}
new_head = NULL; // 置空头指针,避免野指针
return 0;
}
C++:
# 1. 复用节点类(和练习1一致)
#include <iostream>
using namespace std;
class ListNode {
public:
int val;
ListNode* next;
ListNode(int x) : val(x), next(NULL) {}
};
// 2. 单链表插入函数(在指定下标index插入值为val的节点)
ListNode* insert_node(ListNode* head, int index, int val) {
// 特殊情况1:插入到表头(index=0),新节点成为新的头节点
ListNode* new_node = new ListNode(val); // 用new创建新节点
if (index == 0) {
new_node->next = head; // 新节点的next指向原头节点
return new_node; // 返回新的头节点
}
// 找到插入位置的前驱节点(index-1位置的节点)
ListNode* current = head;
ListNode* prev = NULL; // 前驱节点,初始为空
int count = 0; // 记录当前遍历到的下标
// 遍历找到index-1位置的节点,同时判断下标是否越界
while (current != NULL && count < index) {
prev = current;
current = current->next;
count++;
}
// 特殊情况2:下标越界(count < index,说明遍历到尾节点仍未到指定下标)
if (count < index) {
cout << "下标越界,插入失败" << endl; // C++用cout输出
delete new_node; // 释放未使用的新节点,避免内存泄漏
return head;
}
// 正常插入:修改指针指向
prev->next = new_node; // 前驱节点的next指向新节点
new_node->next = current; // 新节点的next指向原前驱节点的下一个节点
return head; // 返回原头节点(表头未变)
}
// 3. 测试代码
int main() {
// 原链表:1 → 2 → 3 → 4
ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(3);
ListNode* node4 = new ListNode(4);
node1->next = node2;
node2->next = node3;
node3->next = node4;
node4->next = NULL;
// 在index=2插入值为5的节点,插入后:1 → 2 → 5 → 3 → 4
ListNode* new_head = insert_node(node1, 2, 5);
// 遍历验证(复用遍历函数)
void traverse(ListNode* head) {
ListNode* current = head;
while (current != NULL) {
cout << current->val << " → ";
current = current->next;
}
cout << "NULL" << endl;
}
traverse(new_head);
// 释放内存
ListNode* temp;
current = new_head;
while (current != NULL) {
temp = current;
current = current->next;
delete temp;
}
new_head = NULL;
temp = NULL;
return 0;
}
Python:
# 1. 复用节点类
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# 2. 单链表删除函数(删除指定下标index的节点)
def delete_node(head, index):
# 特殊情况1:链表为空,无法删除
if head is None:
print("链表为空,删除失败")
return None
# 特殊情况2:删除表头(index=0),新头节点为原头节点的next
if index == 0:
new_head = head.next
return new_head
# 找到删除节点的前驱节点(index-1位置的节点)
current = head
prev = None
count = 0
while current is not None and count < index:
prev = current
current = current.next
count += 1
# 特殊情况3:下标越界(current为空,说明没有该下标对应的节点)
if current is None:
print("下标越界,删除失败")
return head
# 正常删除:修改前驱节点的指针,跳过删除节点
prev.next = current.next
return head # 返回原头节点(表头未变)
# 3. 测试代码
if __name__ == "__main__":
# 原链表:1 → 2 → 5 → 3 → 4(复用练习2插入后的链表)
node1 = ListNode(1)
node2 = ListNode(2)
node5 = ListNode(5)
node3 = ListNode(3)
node4 = ListNode(4)
node1.next = node2
node2.next = node5
node5.next = node3
node3.next = node4
# 删除index=2的节点(值为5),删除后:1 → 2 → 3 → 4
new_head = delete_node(node1, 2)
# 遍历验证
def traverse(head):
current = head
while current:
print(current.val, end=" → ")
current = current.next
print("None")
traverse(new_head)
八、新手学习建议(附链表+数组学习路线)
链表的核心不是“指针”,而是“灵活”——它打破了数组连续存储的限制,用指针串联起零散的节点,解决了数组删改效率低的问题。初阶阶段,不用追求“精通所有链表类型”,只要吃透单链表的核心操作、分清它和数组的适用场景,就是胜利。
数组和链表,是数据结构的“入门基石”,后续我们会聊栈、队列、哈希表等进阶结构,而这些结构,本质上都是基于数组和链表扩展的——把基础吃透,后续的学习会轻松很多。
如果这篇博客对你有帮助,欢迎点赞收藏~ 也可以在评论区留言,说说你学习链表时遇到的困惑(比如指针总是写错、容易出现空指针),我们一起交流、一起进步!
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)