一、为什么需要单链表?
在学习单链表之前,我们先思考一个问题:为什么不能只用数组存储数据?数组作为Python中常见的线性存储结构,虽然支持随机访问,但存在两个明显的局限性:
- 内存连续性要求高:数组初始化时需要申请一块连续的内存空间,若后续数据量超过初始容量,就需要进行“扩容”——重新申请更大的连续内存,将原数据复制过去,这个过程会消耗额外的时间成本(时间复杂度通常为O(n));
- 中间增删效率低:由于数组元素的索引与内存地址直接关联,若要在数组中间插入或删除元素,需要移动该位置之后的所有元素(例如在数组第3位插入元素,第3位及以后的元素都要向后挪一位),操作时间复杂度为O(n)。
而单链表恰好能解决这些问题:它不需要连续的内存空间,通过“指针”(Python中表现为对象引用)将分散的节点串联起来;在链表头部或尾部增删元素时,只需修改指针指向,时间复杂度可达到O(1)(仅需处理相邻节点的关联,无需移动其他元素)。虽然单链表的随机访问效率较低(需从表头依次遍历,时间复杂度O(n)),但在“数据频繁增删、无需频繁随机访问”的场景(如实现栈、队列、链表式哈希表)中,单链表成为更优选择。
二、单链表的核心概念:
单链表其两个核心组成部分:节点(Node) 和链表的边界标识(表头Head、表尾Tail、空指针None)。
1. 节点(Node)
单链表的每个节点都包含两部分,缺一不可:
- 值域(data):存储实际的数据,例如整数、字符串、自定义对象等,是节点的核心信息;
- 指针域(next):存储下一个节点的引用(地址),相当于“连接绳”,将当前节点与下一个节点串联起来。
可以类比生活中的“火车车厢”:每个车厢(节点)的“车厢本体”是值域(装着乘客/货物),而“车厢之间的连接挂钩”就是指针域,通过挂钩将所有车厢连成长长的火车(单链表)。
在Python中,由于没有“指针”的概念,我们通过类(class) 来定义节点——用类的属性存储值域(data)和指针域(next),每个节点就是一个类的实例对象。例如,一个存储整数的节点可以表示为:
class Node:
def __init__(self, data):
self.data = data # 值域:存储节点数据
self.next = None # 指针域:初始化为None(表示暂无后续节点)
创建节点时,只需传入数据即可,next默认指向None(表示该节点暂时是“孤立节点”,未接入链表):
# 创建3个孤立节点,分别存储10、20、30
node1 = Node(10)
node2 = Node(20)
node3 = Node(30)
2. 单链表的边界标识
单个节点无法构成链表,需要通过指针域将节点串联,同时明确链表的“边界”,避免遍历或操作时出现混乱。单链表的边界由以下三个标识确定:
- 表头(Head):永远指向链表的第一个节点,是遍历链表的“入口”——所有对链表的操作(如遍历、插入、删除),几乎都需要从Head开始;
- 表尾(Tail):永远指向链表的最后一个节点,其特点是“表尾节点的next指针必为None”——这是判断链表是否到达终点的关键标志;
- 空指针(None):表示“没有下一个节点”,是单链表的“终止信号”,除了表尾节点的next指向None,若链表为空(无任何节点),Head也会指向None。
以“节点10→节点20→节点30”的单链表为例,其结构可表示为:
Head → node1(data=10, next=node2)→ node2(data=20, next=node3)→ node3(data=30, next=None)→ None
此时,Head指向node1,Tail指向node3,node3的next为None,清晰地界定了链表的起点、节点顺序和终点。
3. 空链表与非空链表的区别
- 空链表:链表中没有任何节点,此时Head和Tail都指向None,链表的长度(节点总数)为0;
- 非空链表:至少包含一个节点,Head指向第一个节点,Tail指向最后一个节点,且Tail的next为None。
空链表是单链表的“初始状态”,后续所有增删操作都是基于空链表或非空链表进行的。
三、单链表的核心操作
单链表的操作围绕“节点的管理”展开,核心操作包括:判断链表是否为空、获取链表长度、遍历链表、在指定位置增删节点、查找节点等。每个操作都需要严格处理“指针指向”和“边界条件”(如空链表、操作位置在表头/表尾),否则容易出现“链表断裂”或“死循环”。
下面我们将通过Python类(SinglyLinkedList)封装单链表的所有操作,先定义类的初始化方法,再逐一实现核心功能。
1. 单链表类的初始化
首先,我们定义单链表类,初始化时创建一个空链表——Head和Tail都指向None,同时用length属性记录链表长度(初始为0):
class SinglyLinkedList:
def __init__(self):
self.head = None # 表头:初始指向None(空链表)
self.tail = None # 表尾:初始指向None(空链表)
self.length = 0 # 链表长度:初始为0(无节点)
2. 判断链表是否为空(is_empty)
判断标准:若self.head为None(或self.length为0),则链表为空。该操作主要用于后续增删操作的“前置校验”(例如,删除节点前需先判断链表是否为空,避免报错)。
实现代码:
def is_empty(self):
# 两种判断方式等价,任选其一即可
return self.head is None
# return self.length == 0
使用示例:
# 创建空链表
link_list = SinglyLinkedList()
print(link_list.is_empty()) # 输出:True(空链表)
3. 获取链表长度(get_length)
直接返回self.length属性即可——因为我们在增删节点时会同步更新length,避免每次获取长度都需要遍历链表(遍历获取长度的时间复杂度为O(n),而直接返回属性为O(1),效率更高)。
实现代码:
def get_length(self):
return self.length
使用示例:
# 后续添加节点后,可直接获取长度
link_list.append(10) # 向链表尾部添加节点10
print(link_list.get_length()) # 输出:1(链表包含1个节点)
4. 遍历链表(travel)
遍历逻辑:从表头(self.head)开始,依次访问每个节点的data,直到遇到next为None的节点(表尾)。遍历的核心是“用一个临时指针(如current)跟踪当前节点,避免修改表头self.head”(若直接移动self.head,会导致后续无法再访问链表)。
实现代码:
def travel(self):
if self.is_empty():
print("链表为空,无节点可遍历")
return
current = self.head # 临时指针:从表头开始
while current is not None: # 未到达表尾(next为None),继续遍历
print(current.data, end=" → ") # 输出当前节点数据
current = current.next # 指针移动到下一个节点
print("None") # 遍历结束,输出终止符(表示链表终点)
使用示例:
# 向链表添加3个节点
link_list.append(10)
link_list.append(20)
link_list.append(30)
link_list.travel() # 输出:10 → 20 → 30 → None
5. 头部添加节点(add)
头部添加节点(“头插法”)是单链表中效率最高的添加操作之一(时间复杂度O(1)),核心是“将新节点的next指向原表头,再更新新表头为新节点”。需要分两种情况处理:
- 情况1:链表为空:新节点既是表头也是表尾,
self.head和self.tail都指向新节点; - 情况2:链表非空:新节点的next指向原表头(
self.head),再将self.head更新为新节点。
实现代码:
def add(self, data):
# 1. 创建新节点
new_node = Node(data)
# 2. 处理空链表场景
if self.is_empty():
self.head = new_node # 新节点成为表头
self.tail = new_node # 新节点成为表尾
else:
# 3. 处理非空链表场景:新节点的next指向原表头
new_node.next = self.head
self.head = new_node # 更新表头为新节点
# 4. 同步更新链表长度
self.length += 1
使用示例:
# 头部添加节点0
link_list.add(0)
link_list.travel() # 输出:0 → 10 → 20 → 30 → None
print(link_list.get_length()) # 输出:4(长度从3变为4)
6. 尾部添加节点(append)
尾部添加节点(“尾插法”)也是常用操作(时间复杂度O(1)),核心是“将原表尾的next指向新节点,再更新新表尾为新节点”。同样分两种情况:
- 情况1:链表为空:与头部添加逻辑一致,新节点既是表头也是表尾;
- 情况2:链表非空:原表尾的next指向新节点,再将
self.tail更新为新节点。
实现代码:
def append(self, data):
# 1. 创建新节点
new_node = Node(data)
# 2. 处理空链表场景
if self.is_empty():
self.head = new_node
self.tail = new_node
else:
# 3. 处理非空链表场景:原表尾的next指向新节点
self.tail.next = new_node
self.tail = new_node # 更新表尾为新节点
# 4. 同步更新链表长度
self.length += 1
使用示例:
# 尾部添加节点40
link_list.append(40)
link_list.travel() # 输出:0 → 10 → 20 → 30 → 40 → None
print(link_list.get_length()) # 输出:5(长度从4变为5)
7. 指定位置添加节点(insert)
指定位置添加节点(“中间插法”)相对复杂(时间复杂度O(n)),需要先找到“插入位置的前一个节点”,再调整指针指向。核心步骤如下:
- 位置合法性校验:插入位置
pos需满足0 ≤ pos ≤ self.length(pos=0等价于头部添加,pos=self.length等价于尾部添加),若超出范围则抛出异常; - 特殊位置处理:若pos=0,直接调用
add()方法;若pos=self.length,直接调用append()方法; - 中间位置处理:用临时指针
current遍历到“pos-1”位置的节点(插入位置的前一个节点),然后:- 新节点的next指向
current的next(原pos位置的节点); current的next指向新节点;
- 新节点的next指向
- 同步更新链表长度。
实现代码:
def insert(self, pos, data):
# 1. 位置合法性校验
if not (0 <= pos <= self.length):
raise ValueError(f"插入位置非法!合法范围为0~{self.length}")
# 2. 特殊位置:pos=0(头部添加)
if pos == 0:
self.add(data)
return
# 3. 特殊位置:pos=self.length(尾部添加)
if pos == self.length:
self.append(data)
return
# 4. 中间位置:遍历找到pos-1位置的节点
new_node = Node(data)
current = self.head # 临时指针:从表头开始
count = 0 # 计数器:记录当前指针位置
# 遍历到pos-1位置(例如pos=2,需找到第1个节点)
while count < pos - 1:
current = current.next
count += 1
# 5. 调整指针指向:插入新节点
new_node.next = current.next # 新节点指向原pos位置的节点
current.next = new_node # pos-1位置的节点指向新节点
# 6. 同步更新链表长度
self.length += 1
使用示例:
# 在pos=2的位置插入节点15(原pos=2的节点是20)
link_list.insert(2, 15)
link_list.travel() # 输出:0 → 10 → 15 → 20 → 30 → 40 → None
print(link_list.get_length()) # 输出:6(长度从5变为6)
8. 删除指定数据的节点(remove)
删除节点是单链表中最容易出错的操作之一(时间复杂度O(n)),核心是“找到待删除节点的前一个节点,断开待删除节点的指针连接”。需要处理三种关键场景:
- 场景1:链表为空:直接抛出异常或提示,无节点可删除;
- 场景2:待删除节点是表头:将
self.head更新为原表头的next(跳过待删除节点),若删除后链表为空(原链表只有1个节点),需同步将self.tail设为None; - 场景3:待删除节点是中间节点或表尾:用临时指针
current遍历找到“待删除节点的前一个节点”,然后将current.next指向“待删除节点的next”(跳过待删除节点);若待删除节点是表尾,需同步将self.tail更新为current; - 同步更新链表长度。
此外,若链表中存在多个相同数据的节点(如两个节点数据都是20),默认删除“第一个匹配的节点”(从表头开始遍历的第一个)。
实现代码:
def remove(self, data):
# 1. 场景1:链表为空
if self.is_empty():
raise ValueError("链表为空,无节点可删除")
current = self.head # 临时指针:从表头开始
prev = None # 记录当前节点的前一个节点(初始为None,因为表头无前节点)
# 2. 遍历链表,寻找待删除节点
while current is not None:
if current.data == data:
# 场景2:待删除节点是表头(prev为None)
if prev is None:
self.head = current.next # 表头更新为下一个节点
# 若删除后链表为空(原链表只有1个节点),同步更新表尾
if self.head is None:
self.tail = None
else:
# 场景3:待删除节点是中间节点或表尾
prev.next = current.next # 前节点跳过当前节点(删除当前节点)
# 若待删除节点是表尾,更新表尾为前节点
if current.next is None:
self.tail = prev
# 同步更新链表长度
self.length -= 1
return # 只删除第一个匹配节点,直接返回
# 未找到待删除节点,继续遍历
prev = current
current = current.next
# 遍历结束仍未找到,抛出异常
raise ValueError(f"链表中不存在数据为{data}的节点")
使用示例:
# 删除数据为15的节点(中间节点)
link_list.remove(15)
link_list.travel() # 输出:0 → 10 → 20 → 30 → 40 → None
print(link_list.get_length()) # 输出:5(长度从6变为5)
# 删除数据为0的节点(表头)
link_list.remove(0)
link_list.travel() # 输出:10 → 20 → 30 → 40 → None
print(link_list.get_length()) # 输出:4(长度从5变为4)
# 删除数据为40的节点(表尾)
link_list.remove(40)
link_list.travel() # 输出:10 → 20 → 30 → None
print(link_list.get_length()) # 输出:3(长度从4变为3)
9. 查找指定数据的节点(search)
查找逻辑与遍历类似:从表头开始,用临时指针current依次判断每个节点的data是否等于目标数据,若找到则返回True,遍历结束仍未找到则返回False。
实现代码:
def search(self, data):
if self.is_empty():
print("链表为空,无法查找节点")
return False
current = self.head
while current is not None:
if current.data == data:
return True # 找到目标节点,返回True
current = current.next
return False # 遍历结束未找到,返回False
使用示例:
print(link_list.search(20)) # 输出:True(存在数据为20的节点)
print(link_list.search(100)) # 输出:False(不存在数据为100的节点)
四、单链表的完整测试:验证所有操作的正确性
为了确保单链表的实现无误,我们可以编写一个完整的测试代码,依次调用上述所有操作,验证结果是否符合预期:
class Node:
"""单链表节点类"""
def __init__(self, data):
self.data = data # 存储节点数据
self.next = None # 指向下一个节点的引用,初始为None
class SinglyLinkedList:
"""单链表类,实现单链表的各种操作"""
def __init__(self):
self.head = None # 头指针,指向链表第一个节点
self.tail = None # 尾指针,指向链表最后一个节点
self.length = 0 # 链表长度(节点数量)
def is_empty(self):
"""判断链表是否为空"""
return self.head is None
def get_length(self):
"""获取链表长度"""
return self.length
def travel(self):
"""遍历链表并打印所有节点数据"""
if self.is_empty():
print("链表为空,无节点可遍历")
return
current = self.head # 从头部开始遍历
while current is not None:
print(current.data, end=" → ")
current = current.next # 移动到下一个节点
print("None") # 标记链表结束
def add(self, data):
"""在链表头部添加节点(头插法)"""
new_node = Node(data) # 创建新节点
if self.is_empty():
# 空链表时,新节点既是头也是尾
self.head = new_node
self.tail = new_node
else:
new_node.next = self.head # 新节点指向原头节点
self.head = new_node # 更新头指针为新节点
self.length += 1 # 长度加1
def append(self, data):
"""在链表尾部添加节点(尾插法)"""
new_node = Node(data) # 创建新节点
if self.is_empty():
# 空链表时,新节点既是头也是尾
self.head = new_node
self.tail = new_node
else:
self.tail.next = new_node # 原尾节点指向新节点
self.tail = new_node # 更新尾指针为新节点
self.length += 1 # 长度加1
def insert(self, pos, data):
"""在指定位置pos插入节点(pos范围:0~length)"""
# 验证插入位置合法性
if not (0 <= pos <= self.length):
raise ValueError(f"插入位置非法!合法范围为0~{self.length}")
# 特殊位置处理
if pos == 0:
self.add(data)
return
if pos == self.length:
self.append(data)
return
# 中间位置插入
new_node = Node(data)
current = self.head # 查找pos-1位置的节点
count = 0
while count < pos - 1:
current = current.next
count += 1
new_node.next = current.next # 新节点指向插入位置的原节点
current.next = new_node # 前节点指向新节点
self.length += 1 # 长度加1
def remove(self, data):
"""删除第一个值为data的节点"""
if self.is_empty():
raise ValueError("链表为空,无节点可删除")
current = self.head # 当前节点指针
prev = None # 前一个节点指针
while current is not None:
if current.data == data:
# 找到目标节点,执行删除
if prev is None:
# 删除头节点
self.head = current.next
# 若删除后链表为空,同步更新尾指针
if self.head is None:
self.tail = None
else:
# 删除中间或尾节点
prev.next = current.next
# 若删除的是尾节点,更新尾指针
if current.next is None:
self.tail = prev
self.length -= 1 # 长度减1
return # 只删除第一个匹配节点
# 未找到目标节点,继续遍历
prev = current
current = current.next
# 遍历结束未找到目标节点
raise ValueError(f"链表中不存在数据为{data}的节点")
def search(self, data):
"""查找链表中是否存在值为data的节点"""
if self.is_empty():
print("链表为空,无法查找节点")
return False
current = self.head
while current is not None:
if current.data == data:
return True # 找到节点
current = current.next
return False # 未找到节点
# 测试单链表功能
if __name__ == "__main__":
# 初始化空链表
link_list = SinglyLinkedList()
print("=== 初始状态 ===")
print("链表是否为空:", link_list.is_empty()) # 预期:True
print("链表长度:", link_list.get_length()) # 预期:0
link_list.travel() # 预期:链表为空,无节点可遍历
# 头部添加节点
link_list.add(10)
link_list.add(0)
print("\n=== 头部添加0、10后 ===")
link_list.travel() # 预期:0 → 10 → None
print("链表长度:", link_list.get_length()) # 预期:2
# 尾部添加节点
link_list.append(20)
link_list.append(30)
print("\n=== 尾部添加20、30后 ===")
link_list.travel() # 预期:0 → 10 → 20 → 30 → None
print("链表长度:", link_list.get_length()) # 预期:4
# 指定位置添加节点
link_list.insert(2, 15)
print("\n=== 在pos=2添加15后 ===")
link_list.travel() # 预期:0 → 10 → 15 → 20 → 30 → None
print("链表长度:", link_list.get_length()) # 预期:5
# 查找节点
print("\n=== 查找节点 ===")
print("是否存在15:", link_list.search(15)) # 预期:True
print("是否存在100:", link_list.search(100)) # 预期:False
# 删除节点
link_list.remove(15)
print("\n=== 删除15后 ===")
link_list.travel() # 预期:0 → 10 → 20 → 30 → None
print("链表长度:", link_list.get_length()) # 预期:4
link_list.remove(0)
print("\n=== 删除表头0后 ===")
link_list.travel() # 预期:10 → 20 → 30 → None
print("链表长度:", link_list.get_length()) # 预期:3
link_list.remove(30)
print("\n=== 删除表尾30后 ===")
link_list.travel() # 预期:10 → 20 → None
print("链表长度:", link_list.get_length()) # 预期:2
输出结果为:
=== 初始状态 ===
链表是否为空: True
链表长度: 0
链表为空,无节点可遍历
=== 头部添加0、10后 ===
0 → 10 → None
链表长度: 2
=== 尾部添加20、30后 ===
0 → 10 → 20 → 30 → None
链表长度: 4
=== 在pos=2添加15后 ===
0 → 10 → 15 → 20 → 30 → None
链表长度: 5
=== 查找节点 ===
是否存在15: True
是否存在100: False
=== 删除15后 ===
0 → 10 → 20 → 30 → None
链表长度: 4
=== 删除表头0后 ===
10 → 20 → 30 → None
链表长度: 3
=== 删除表尾30后 ===
10 → 20 → None
链表长度: 2
五、单链表的优缺点与应用场景
1. 单链表的优点
- 动态内存分配:无需提前申请连续内存,节点可根据需要动态创建,避免内存浪费;
- 头部/尾部增删效率高:头部添加(add)、尾部添加(append)、头部删除的时间复杂度均为O(1),无需移动其他节点;
- 内存利用率高:节点分散存储在内存中,可利用碎片化内存(数组需要连续内存,碎片化内存无法利用)。
2. 单链表的缺点
- 随机访问效率低:无法像数组一样通过索引直接访问节点,需从表头依次遍历,时间复杂度O(n);
- 只能单向遍历:节点只有指向后一个节点的指针(next),若需访问前一个节点,需重新从表头遍历(这也是双向链表存在的原因);
- 额外内存开销:每个节点除了存储数据,还需存储next指针,增加了内存开销(数组只需存储数据本身)。
3. 单链表的应用场景
单链表适合“频繁增删、无需频繁随机访问”的场景,典型应用包括:
- 实现栈(Stack):栈的核心操作是“先进后出(LIFO)”,用单链表的头部作为栈顶,
add()(头插)对应栈的push(),remove()(删除表头)对应栈的pop(),操作效率均为O(1); - 实现队列(Queue):队列的核心操作是“先进先出(FIFO)”,用单链表的头部作为队头(
remove()对应dequeue),尾部作为队尾(append()对应enqueue),操作效率均为O(1); - 链表式哈希表:当哈希表发生“哈希冲突”时,常用“链地址法”解决——每个哈希桶存储一个单链表,冲突的元素通过单链表串联,避免冲突导致的数据丢失;
- 动态数据集合:如日志记录、消息队列等,数据量不确定且需要频繁添加/删除的场景,单链表可灵活应对数据规模的变化。
七、总结与拓展
单链表作为链表家族的入门成员,其核心是“节点通过指针串联,动态管理数据”。本文从概念入手,详细讲解了单链表的结构、核心操作的实现逻辑,并通过完整测试验证了代码正确性,最后总结了优缺点与应用场景。掌握单链表后,读者可以进一步学习更复杂的链表结构:
- 双向链表:每个节点增加
prev指针(指向前一个节点),支持双向遍历,解决单链表无法反向访问的问题; - 单向循环链表:表尾节点的
next指向表头,形成循环结构,适合需要“循环访问”的场景(如约瑟夫环问题); - 双向循环链表:结合双向链表和循环链表的特点,支持双向循环遍历,功能更强大但实现更复杂。
489

被折叠的 条评论
为什么被折叠?



