Python数据结构(二): Python中的单链表

部署运行你感兴趣的模型镜像

一、为什么需要单链表?

在学习单链表之前,我们先思考一个问题:为什么不能只用数组存储数据?数组作为Python中常见的线性存储结构,虽然支持随机访问,但存在两个明显的局限性:

  1. 内存连续性要求高:数组初始化时需要申请一块连续的内存空间,若后续数据量超过初始容量,就需要进行“扩容”——重新申请更大的连续内存,将原数据复制过去,这个过程会消耗额外的时间成本(时间复杂度通常为O(n));
  2. 中间增删效率低:由于数组元素的索引与内存地址直接关联,若要在数组中间插入或删除元素,需要移动该位置之后的所有元素(例如在数组第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.headself.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)),需要先找到“插入位置的前一个节点”,再调整指针指向。核心步骤如下:

  1. 位置合法性校验:插入位置pos需满足0 ≤ pos ≤ self.length(pos=0等价于头部添加,pos=self.length等价于尾部添加),若超出范围则抛出异常;
  2. 特殊位置处理:若pos=0,直接调用add()方法;若pos=self.length,直接调用append()方法;
  3. 中间位置处理:用临时指针current遍历到“pos-1”位置的节点(插入位置的前一个节点),然后:
    • 新节点的next指向current的next(原pos位置的节点);
    • current的next指向新节点;
  4. 同步更新链表长度。

实现代码:

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. 场景1:链表为空:直接抛出异常或提示,无节点可删除;
  2. 场景2:待删除节点是表头:将self.head更新为原表头的next(跳过待删除节点),若删除后链表为空(原链表只有1个节点),需同步将self.tail设为None;
  3. 场景3:待删除节点是中间节点或表尾:用临时指针current遍历找到“待删除节点的前一个节点”,然后将current.next指向“待删除节点的next”(跳过待删除节点);若待删除节点是表尾,需同步将self.tail更新为current
  4. 同步更新链表长度。

此外,若链表中存在多个相同数据的节点(如两个节点数据都是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
链表为空,无节点可遍历

=== 头部添加010===
010None
链表长度: 2

=== 尾部添加2030===
0102030None
链表长度: 4

=== 在pos=2添加15===
010152030None
链表长度: 5

=== 查找节点 ===
是否存在15True
是否存在100False

=== 删除15===
0102030None
链表长度: 4

=== 删除表头0===
102030None
链表长度: 3

=== 删除表尾30===
1020None
链表长度: 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指向表头,形成循环结构,适合需要“循环访问”的场景(如约瑟夫环问题);
  • 双向循环链表:结合双向链表和循环链表的特点,支持双向循环遍历,功能更强大但实现更复杂。

您可能感兴趣的与本文相关的镜像

Python3.10

Python3.10

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mrliu__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值