☰
Python数据结构与算法源码精读:从抽象基类到排序实现
2026/10/14 21:56:16 网站建设 项目流程

简介:面向 Python 学习者的数据结构和算法源码资源,源自同名经典教程,系统覆盖常用数据结构与算法的设计、分析和实现。作者借助 Python 语言的简明与优雅,给出大量可执行源代码,并始终采用面向对象视角,通过继承机制提升代码复用性,也让各种抽象数据类型和算法方法的异同更直观。资源包共 124 个文件,其中 123 个为 Python 脚本,另含 1 个 gitignore 文件,整体大小仅 89KB,非常轻量,适合离线阅读和对比学习。脚本按知识主题组织,涵盖二叉搜索树、链表二叉树、图、树、排序映射表、欧拉遍历、位置列表、表达式树、红黑树等典型实现,基本对应数据结构课程的核心章节。目前已有 734 人浏览学习,读者可直接运行源码观察行为,逐行分析内部指针与递归过程,理解不同数据结构的复杂度权衡和适用场景,也可将其作为算法面试复习或工程开发的速查参考。

1. 为什么值得啃这份 Python 数据结构和算法源码资源

日常写业务代码的人,很少有机会认真审视「数组越界之后程序到底怎么崩的」「递归深度撞上天花板时解释器在做什么」。这份资源把数据结构和算法的核心内容,用可执行的 Python 源码呈现出来,不是停留在伪代码层面的讲解,而是每一章都能直接跑起来观察行为。它从抽象数据类型的设计出发,贯穿继承和面向对象思想,适合正在补算法基础的后端开发者、准备面试的求职者,以及想把 Python 写得更扎实的脚本爱好者。读完你能获得的不只是几个算法的实现,而是一套「设计 → 分析 → 实现 → 验证」的完整思维路径,这对理解 Python 内部机制也有很大帮助。

2. 抽象基类与继承:先搭好数据结构的骨架再谈实现

2.1 为什么用抽象基类来定义数据结构接口

很多初学者写数据结构时,习惯直接开写具体类,比如class Node、class LinkedList。这本资源里呈现的思路不一样,它先定义抽象基类,把每个数据结构对外暴露的操作集合固定下来,再用具体子类去填充实现细节。

这种设计有一个直接收益:调用方只依赖抽象接口,底层从数组切换成链表时,使用方代码可以做到零改动。另一个收益在调试阶段更明显——当你新增一种容器类型时,编译或运行时会强制你实现全部抽象方法,漏掉任何一个都会在实例化时报错,而不是等到调用缺失方法时才暴露问题。

from abc import ABC, abstractmethod class BaseContainer(ABC): """容器抽象基类:约束子类必须实现的核心操作""" @abstractmethod def __len__(self): """返回容器内元素个数""" pass @abstractmethod def __contains__(self, item): """判断元素是否在容器中""" pass @abstractmethod def clear(self): """清空容器内所有元素""" pass

这段代码的逻辑核心在于ABCMeta元类和abstractmethod装饰器。BaseContainer被实例化时会直接抛出TypeError,因为它是抽象类。子类必须逐个实现__len__、__contains__、clear,否则实例化同样失败。参数层面要注意__contains__的item参数代表待查找的元素,Python 会通过in运算符自动转换到该方法。

2.2 继承复用与代码重用的边界

定义好抽象基类之后,具体的动态数组实现就可以通过继承来复用公共逻辑。比如在动态数组中,clear操作和容量缩放的公共逻辑可以抽到父类,子类只维护自己的存储指针和容量字段。

class DynamicArray(BaseContainer): """动态数组:基于列表实现,支持自动扩容""" def __init__(self): self._data = [] self._size = 0 def __len__(self): return self._size def __contains__(self, item): return item in self._data def clear(self): self._data.clear() self._size = 0 def append(self, value): """末尾追加元素,触发扩容时均摊复杂度仍为 O(1)""" self._data.append(value) self._size += 1

_data是真正的存储容器,_size是逻辑长度。这里有个容易被忽略的设计点:_size和len(self._data)在正常情况下应该一致,但保留独立字段的意义在于,后续如果需要支持「预分配容量但暂不填充元素」的场景,_size能准确表达逻辑长度而存储列表可以更大。

2.3 时间复杂度分析在选型中的作用

资源里对每种数据结构都做了复杂度分析,不是简单贴一张大 O 表,而是从操作层面拆解。以动态数组为例:append均摊 O(1),但触发扩容的某一次操作是 O(n),因为需要把旧元素全部拷贝到新列表。pop从尾部删除是 O(1),但从头部删除是 O(n)。

理解了这些复杂度差异,选型时就不会凭感觉。高频读取、按下标访问的场景选数组;高频在头部插入删除的场景选链表;需要键值对且要求 O(1) 查找选哈希表;需要保持有序且频繁查找选二叉搜索树。这个资源的价值就是把每一种选择的复杂度代价摆在明面上。

3. 链表、栈与队列的源码实现:指针操作与边界条件

3.1 单向链表节点的定义与插入删除逻辑

链表部分的源码演示了节点类如何与链表类协作。节点类持有next指针,链表类持有_head指针,插入和删除操作的本质就是重新挂接指针,但边界条件极多。

class Node: """链表节点:只负责持有数据和指向下一个节点的引用""" __slots__ = ('data', 'next') def __init__(self, data=None, next_node=None): self.data = data self.next = next_node class SinglyLinkedList: """单向链表:头插法实现,包含遍历和删除逻辑""" def __init__(self): self._head = None self._size = 0 def insert_head(self, value): """头插法:新节点指向原头节点,链表头指针指向新节点""" new_node = Node(value, self._head) self._head = new_node self._size += 1 def remove_value(self, value): """删除指定值的第一个节点,需要记录前驱节点""" prev, current = None, self._head while current is not None: if current.data == value: if prev is None: self._head = current.next else: prev.next = current.next self._size -= 1 return True prev, current = current, current.next return False

insert_head中Node(value, self._head)这行是关键——先让新节点的next指向当前头节点,再更新_head,顺序不能反过来,否则会丢失原链表。remove_value的边界在于头节点就是目标节点时,prev为None需要单独处理。参数value是需要删除的目标值,方法返回True/False表示是否删除成功。

3.2 双向链表的优势与实现注意点

双向链表在单向链表基础上多了一个prev指针,删除操作不再需要遍历找前驱。但代价是每个节点多占用一个引用空间,且插入操作需要维护四个指针关系。

class DoublyLinkedNode: __slots__ = ('data', 'prev', 'next') def __init__(self, data=None): self.data = data self.prev = None self.next = None class DoublyLinkedList: def __init__(self): self._head = None self._tail = None self._size = 0 def append_tail(self, value): """尾插法:需要同时维护尾节点的 prev 和 next""" new_node = DoublyLinkedNode(value) if self._tail is None: self._head = self._tail = new_node else: self._tail.next = new_node new_node.prev = self._tail self._tail = new_node self._size += 1 def remove_node(self, node): """直接给定节点引用删除,这是双向链表的核心优势""" if node.prev is not None: node.prev.next = node.next else: self._head = node.next if node.next is not None: node.next.prev = node.prev else: self._tail = node.prev self._size -= 1

__slots__的用法值得注意,它限制了实例只能有data、prev、next三个属性,能显著降低内存占用,这对链表这种节点数量大的结构很有价值。remove_node接收的是节点对象而非值,这在需要 O(1) 删除的场景下非常实用。

3.3 栈与队列的数组实现和链表实现对比

栈和队列可以用数组实现,也可以用链表实现。数组实现的栈利用 Python 列表的append和pop,天然就是 O(1);数组实现的队列就要么用collections.deque,要么用环形缓冲区。链表实现的栈和队列在扩容上更平滑,但每个节点有额外内存开销。

class ArrayQueue: """基于列表的环形队列:避免 pop(0) 的 O(n) 开销""" def __init__(self, capacity=8): self._data = [None] * capacity self._capacity = capacity self._head = 0 self._size = 0 def enqueue(self, value): """入队:维护 head 和 size,必要时扩容""" if self._size == self._capacity: self._resize(self._capacity * 2) index = (self._head + self._size) % self._capacity self._data[index] = value self._size += 1 def dequeue(self): """出队:返回头部元素并移动 head 指针""" if self._size == 0: raise IndexError("dequeue from empty queue") value = self._data[self._head] self._data[self._head] = None self._head = (self._head + 1) % self._capacity self._size -= 1 return value def _resize(self, new_capacity): """扩容:把元素按逻辑顺序拷贝到新数组""" old_data = self._data self._data = [None] * new_capacity for i in range(self._size): self._data[i] = old_data[(self._head + i) % self._capacity] self._head = 0 self._capacity = new_capacity

这里的核心是(self._head + self._size) % self._capacity这个取模运算,它让数组在逻辑上首尾相连。enqueue和dequeue都是 O(1),_resize只在队列满时触发,均摊后仍然是 O(1)。参数capacity决定初始容量,太小会导致频繁扩容,太大浪费内存,我一般会根据业务峰值设置初始容量。

4. 避坑指南:Python 特性给数据结构实现埋的四个暗坑

4.1 可变对象作为默认参数

现象:定义一个函数def push(node, stack=[]),每次调用没有显式传入stack时,所有调用共享同一个列表。多次调用后,stack里残留上一次调用的数据,数据污染非常隐蔽。

原因:Python 函数的默认参数在定义时只求值一次,之后每次调用如果没传参数,用的都是同一个对象。这违背了「每次调用应该创建全新容器」的直觉。

解决:默认参数一律写成None,在函数体内自行判空并创建新列表或新字典。

def push(value, stack=None): if stack is None: stack = [] stack.append(value) return stack

4.2 循环中修改正在遍历的列表

现象:在for item in my_list循环体内执行my_list.remove(item),结果发现一部分元素没有被遍历到,程序行为完全不符合预期。

原因:列表迭代器依赖下标推进,删除元素后,后续元素会整体前移,迭代器下标仍然递增,于是跳过了一个元素。

解决:遍历副本或反向遍历。遍历副本的写法是for item in my_list[:]:,反向遍历是for i in range(len(my_list) - 1, -1, -1):,删除下标i不影响前面未遍历的元素。

# 正确做法:遍历副本 for item in my_list[:]: if item < 0: my_list.remove(item)

4.3 递归深度超限导致程序崩溃

现象:实现一个递归的二分查找或树的深度优先遍历,数据量稍大就抛出RecursionError: maximum recursion depth exceeded。

原因:Python 默认递归深度限制是 1000 层,递归深度超过这个阈值直接抛异常,而不是像其他语言那样栈溢出后崩溃。树的深度等于节点数时,递归遍历必然超限。

解决:先评估递归深度,如果数据规模确定会超过 500 层,优先改成显式栈的迭代实现。如果只是偶尔需要更大的递归深度,可以临时调高限制,但这是「后悔药」不是常规方案。

import sys # 临时调高递归上限,谨慎使用 sys.setrecursionlimit(10000)

4.4 在迭代过程中改变字典大小

现象:遍历字典并尝试删除某些键值对,运行时直接抛出RuntimeError: dictionary changed size during iteration。

原因:字典迭代器在迭代期间检测到字典大小变化,立即拒绝继续迭代,这是 Python 的一种保护机制,防止迭代状态失效。

解决:先收集所有要删除的键,迭代结束后统一删除;或者用字典推导式构建新字典。

# 先收集再删除 keys_to_delete = [k for k, v in my_dict.items() if v is None] for k in keys_to_delete: del my_dict[k]

5. 非线性结构:二叉搜索树与图的源码级剖析

5.1 二叉搜索树的插入与查找实现

二叉搜索树的核心不变量是左子树所有节点值小于根节点,右子树所有节点值大于根节点。插入和查找都沿着这个规则递归下降。

class TreeNode: __slots__ = ('value', 'left', 'right') def __init__(self, value): self.value = value self.left = None self.right = None class BinarySearchTree: def __init__(self): self._root = None self._size = 0 def insert(self, value): """插入节点:小于走左子树,大于走右子树,等于时覆盖或忽略""" if self._root is None: self._root = TreeNode(value) self._size += 1 return current = self._root while current is not None: if value < current.value: if current.left is None: current.left = TreeNode(value) self._size += 1 return current = current.left elif value > current.value: if current.right is None: current.right = TreeNode(value) self._size += 1 return current = current.right else: # 值已存在,可以选择返回 False 或覆盖 return def search(self, value): """查找节点:不涉及修改,递归或迭代均可""" current = self._root while current is not None: if value == current.value: return True elif value < current.value: current = current.left else: current = current.right return False

这里体现的是一种自查逻辑:每次比较都让搜索空间减半,树平衡时查找复杂度 O(log n)。参数value是目标值。要注意这棵树目前没有处理删除操作,删除要维护 BST 不变量,通常需要找后继节点或前驱节点来替补,逻辑比插入复杂一个量级。

5.2 树的三种遍历顺序与递归转迭代

前序遍历(根左右)、中序遍历(左根右)、后序遍历(左右根)是树的三个经典遍历方案。其中中序遍历在 BST 上的输出结果是有序序列。递归实现非常直观,但理解其调用栈变化是必要的。

def inorder_traversal_recursive(root): """中序遍历递归实现:输出结果在 BST 上是有序的""" if root is None: return [] result = [] result.extend(inorder_traversal_recursive(root.left)) result.append(root.value) result.extend(inorder_traversal_recursive(root.right)) return result def inorder_traversal_iterative(root): """中序遍历迭代实现:用显式栈模拟系统调用栈""" result = [] stack = [] current = root while current is not None or stack: while current is not None: stack.append(current) current = current.left current = stack.pop() result.append(current.value) current = current.right return result

递归转迭代的关键是找到「系统调用栈保存了什么状态」。中序遍历里,每次递归调用保存的是当前节点和「左子树还没处理完」这个状态,所以显式栈压入节点时,要一路向左走到尽头再开始弹出。stack.append(current)压入的是待访问的节点,弹出时先访问节点值再转向右子树,循环条件current is not None or stack精确表达了这两个终止条件的或关系。

5.3 图的邻接表表示与深度优先遍历

图在 Python 里最常见的表示是邻接表:一个字典,键是节点,值是该节点的邻居列表。这种表示在稀疏图场景下内存效率远高于邻接矩阵。

from collections import defaultdict class Graph: """邻接表图:支持有向和无向两种模式""" def __init__(self, directed=False): self._graph = defaultdict(list) self._directed = directed self._nodes = set() def add_edge(self, u, v): """添加一条边:无向图需要添加反向边""" self._graph[u].append(v) self._nodes.add(u) self._nodes.add(v) if not self._directed: self._graph[v].append(u) def dfs(self, start): """深度优先遍历:返回从 start 可达的所有节点""" visited = set() stack = [start] order = [] while stack: node = stack.pop() if node not in visited: visited.add(node) order.append(node) stack.extend(self._graph[node]) return order

邻接表的核心参数是字典defaultdict(list),其中缺失的键会自动获得空列表作为默认值。_directed控制是否添加反向边。dfs用显式栈避免递归深度问题,弹出的节点被访问后,其邻居全部压入栈。注意stack.extend(self._graph[node])这一行,如果节点不存在于_graph中,defaultdict会自动创建空列表,不会抛出KeyError。遍历顺序受到压栈顺序影响,先压入的后被弹出,这点和无向图的邻接表顺序有关。

6. 排序算法与复杂度验证:用数据说话检查自己的实现

6.1 插入排序与快速排序的实现对比

插入排序适合小规模数据或接近有序的数据,它的内层循环在数据有序时几乎不执行。快速排序的平均复杂度 O(n log n),但最坏情况会退化到 O(n²),痛点集中在基准值的选择上。

def insertion_sort(arr): """插入排序:从第二个元素开始,逐个向前插入到合适位置""" for i in range(1, len(arr)): key = arr[i] j = i - 1 while j >= 0 and arr[j] > key: arr[j + 1] = arr[j] j -= 1 arr[j + 1] = key return arr def quick_sort(arr): """快速排序:选基准,分区,递归排序左右子数组""" if len(arr) <= 1: return arr pivot = arr[len(arr) // 2] left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return quick_sort(left) + middle + quick_sort(right)

插入排序的核心变量是key(当前要插入的值)和j(从已排序部分末尾往前扫描的索引)。quick_sort这里用了三路分区,把等于基准值的元素集中在中间,能显著减少重复元素场景下的递归深度。基准值选中间位置而不是第一个元素,最坏情况出现的概率小很多。

6.2 用随机数据验证排序正确性

写完排序算法后,不能只看人眼觉得对就收工,要做一个可重复的验证流程。常见做法是生成不同规模、不同初始形态的测试数据,对比排序结果和标准库排序结果。

import random def verify_sort(sort_func, size=1000, trials=50): """随机生成多组数据,逐一对比排序结果""" for _ in range(trials): arr = [random.randint(-1000, 1000) for _ in range(size)] sorted_arr = sort_func(arr.copy()) if sorted_arr != sorted(arr): print("排序失败,样例数据:", arr[:20]) return False return True # 验证插入排序和快速排序 print("插入排序验证结果:", verify_sort(insertion_sort)) print("快速排序验证结果:", verify_sort(quick_sort))

这里的参数size控制单组数据长度,trials控制测试轮数。测试里有一类隐含的边界情况:全相同元素、已经有序的数组、逆序数组。随机数测试能覆盖多数情况,但极端分布未必能命中,所以还需要手动补几组定向测试。我一般会专门准备[5, 5, 5]、[]、[1]这种样本,检查空数组和单元素数组的边界行为。

6.3 复杂度不再靠猜:逐步数据量对比运行时间

复杂度分析会告诉你理论值,但真实环境中常数因子、Python 解释器开销、缓存友好度都会影响实际速度。验证复杂度的方式是用倍增的数据量测运行时间,看时间如何随数据规模增长。

import time def time_sort_algorithm(sort_func, sizes): """对每个数据规模测量运行时间,观察增长趋势""" for size in sizes: arr = [random.randint(0, 1000000) for _ in range(size)] start = time.perf_counter() sort_func(arr.copy()) elapsed = time.perf_counter() - start print(f"数据规模 {size:8d},耗时 {elapsed * 1000:8.2f} ms")

对比的参照物是把规模从 1000 翻倍到 2000、4000、8000。如果耗时大致翻倍,可以判断近似 O(n);如果耗时变 4 倍左右,可以推测近似 O(n²);如果接近线性但略带斜率变化,可能受常数因子影响。这个验证方法同样适用于哈希表、树的查找操作。从那以后我每次实现一个结构,都会拿不同规模的数据跑一遍,把复杂度曲线打印出来确认是否符合预期,这也是读这份源码给我留下的最好习惯,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询