简介:《Python 中的数据结构和算法》配套的可执行源码合集,是一份面向数据结构与算法学习者的系统开源代码资料。书中采用一致的面向对象视角组织全部示例,注重抽象数据类型的设计、分析与实现;通过继承机制最大化代码复用,也能让读者更清楚地看到不同数据结构与算法之间的共性和差异。压缩包共 124 个文件,体积仅 89KB,主体为 123 个 Python 源文件,覆盖二叉搜索树、红黑树、表达式树、图、欧拉遍历、位置列表、有序表映射等经典主题,适合边读教材边运行源码,也可以作为课程设计、算法复习或面试准备时的代码参照。包内文件按类与算法职责拆分,模块边界清晰,Python 开发者可以直接提取其中实现用于自己的工程实验。目前已有 734 人学习下载,对于想要获得一套简洁、规范、可执行的 Python 算法源码范例的中高级学习者,是很实用的参考资料。
1. 从一道面试题说起:Python 数据结构和算法到底在学什么
如果你刷过算法题或准备过技术面试,大概率经历过这种场景:用 Python 写二叉树层序遍历,代码十几行就过了,但面试官接着问“为什么用collections.deque而不是list当队列”,当场卡住。另一个常见场景是:用dict做词频统计,数据量到几十万条时程序开始明显变慢,却说不清瓶颈是哈希碰撞还是扩容。这些问题指向同一个缺口——会写 Python 语法,但不懂 Python 世界里数据结构和算法的底层逻辑。《data-structures-and-algorithms-in-python》这个项目的价值,就是用“Python 风格”重新讲一遍数据结构和算法:不依赖伪代码,而是给出可直接运行的真实实现,同时从面向对象的角度展示抽象数据类型怎么设计、继承怎么让代码复用最大化。适合两类人:一是面试前需要系统补数据结构短板的开发者,二是写了几年 Python 但一直停留在“调包”阶段、想深入容器实现原理的工程师。这本书的独特之处在于,它的代码不是算法教材的 Python 翻译版,而是从 Python 运行时特性出发——比如引用语义、切片机制、迭代器协议——来设计每个数据结构,很多实现方式你能明显感觉到“只有 Python 才会这样写”。
2. 理论基座不牢,代码只是堆砌:复杂度分析方法和 Python 的运行机制
2.1 大 O 复杂度在 Python 里为什么容易被低估
数据结构选型的核心依据永远是复杂度分析,但 Python 里有个特殊问题:内置操作的实际开销和教科书上的“理想复杂度”经常对不上。原因是 Python 的对象模型让很多操作背后藏着额外的行为。比如list的insert(0, x),教科书说数组插入是 O(n),但 Python 里它不仅是 O(n),还会触发所有元素的引用指针移动,涉及对象引用计数的调整。如果列表里存的是大对象,这个迁移成本更高。另一个典型是dict的查找,教科书说哈希表查找 O(1),Python 里确实平均 O(1),但最坏情况(哈希碰撞严重时)会退化成 O(n),而且 Python 的字符串哈希有随机化种子,同样的代码在不同进程里碰撞情况完全不同。
import timeit # 对比 list 头部插入和尾部插入的实际开销 setup = "data = list(range(100000))" head_insert = "data.insert(0, 999)" tail_insert = "data.append(999)" head_time = timeit.timeit(head_insert, setup=setup, number=1000) tail_time = timeit.timeit(tail_insert, setup=setup, number=1000) print(f"头部插入 1000 次耗时: {head_time:.4f}s") print(f"尾部插入 1000 次耗时: {tail_time:.4f}s")这段代码用timeit量化实测,而不是靠感觉判断。setup里的列表有十万个元素,head_insert每次插入都会让后面所有元素的内存地址整体向后挪,tail_insert则只要在末尾追加。运行结果里头部插入通常比尾部慢两到三个数量级。这个实验的价值在于:当你在复杂算法里看到有人用list.insert(0)而不是append之后反转,就知道代码为什么慢。实际写算法时,遇到频繁头部操作应直接换collections.deque,它内部是双向链表结构,头尾操作都是 O(1)。
2.2 Python 引用语义:理解数据结构的钥匙
C 语言里数组存的是值本身,Java 里数组存的是引用,但 Python 里一切皆对象,变量名只是指向对象的“标签”。这个差异直接影响数据结构实现方式。比如 Java 里创建链表节点,Node next存的是下一个节点的引用;Python 里没有指针语法,却天然具备同样的能力——一个类属性可以指向任何对象。
class ListNode: """单向链表节点,用类属性实现指针效果""" def __init__(self, value: int, next_node=None): self.value = value self.next_node = next_node # 创建三个节点并链接 node1 = ListNode(1) node2 = ListNode(2) node3 = ListNode(3) node1.next_node = node2 node2.next_node = node3 # 遍历链表 current = node1 while current is not None: print(current.value, end=" -> ") current = current.next_node print("None")这里的核心逻辑在__init__里:value存数据,next_node默认None,创建节点后通过修改属性来建立链接关系。current = node1不是复制节点,而是让current指向同一个对象,随后current = current.next_node就是沿着链表往后移动。这个设计模式和该书坚持的面向对象路线一脉相承——数据和行为封装在类里,链接关系通过对象引用天然达成,不需要像 C 语言那样处理指针和内存释放。理解了这个,后面看二叉树、图的实现就不会迷惑。
3. 手写核心数据结构:从链表到二叉堆的 Python 实现
3.1 链表:为什么 Python 开发者还是需要手写一遍
有人说 Python 里很少直接写链表,因为list已经够用,这个说法对业务开发成立,但对理解算法不成立。手写链表的价值在于理解“节点 + 引用”这个最基本的离散结构组织方式。更重要的是,Python 里用list模拟链表性能是灾难性的——每个节点存一个独立列表,内存开销巨大。书里给出的做法是定义节点类和链表类,链表类维护头部和长度。
class LinkedList: """带头结点的单向链表,支持插入、删除、查找""" def __init__(self): self.head = None self.size = 0 def insert_at_beginning(self, value): """头部插入 O(1)""" new_node = ListNode(value) new_node.next_node = self.head self.head = new_node self.size += 1 def insert_at_end(self, value): """尾部插入 O(n),因为需要遍历到最后一个节点""" new_node = ListNode(value) if self.head is None: self.head = new_node self.size += 1 return current = self.head while current.next_node is not None: current = current.next_node current.next_node = new_node self.size += 1 def find(self, value): """查找值,返回第一个匹配节点的索引""" current = self.head index = 0 while current is not None: if current.value == value: return index current = current.next_node index += 1 return -1 def delete(self, value): """删除第一个匹配值的节点""" if self.head is None: return False if self.head.value == value: self.head = self.head.next_node self.size -= 1 return True current = self.head while current.next_node is not None: if current.next_node.value == value: current.next_node = current.next_node.next_node self.size -= 1 return True current = current.next_node return False每个方法的复杂度都写在注释里,这不是装饰——面试官追问“链表头插为什么是 O(1)”时你要能答出“因为只需要改头部指针”,而尾插 O(n) 是因为必须从头遍历找到最后一个节点。delete方法里用了一个小技巧:检查current.next_node.value而不是current.value,这样删除节点时可以直接让前一个节点的next_node跳过目标节点,省去了维护“前驱节点”的额外变量。size属性每次操作都同步维护,保证__len__方法能在 O(1) 时间返回链表长度。
3.2 栈和队列:用 Python 的 list 和 deque 展示抽象层的意义
栈和队列在 Python 里有现成实现,但书里的重点是“抽象数据类型”的概念——栈和队列定义的是一组操作规则(只允许在端点增删),至于底层用数组还是链表实现,是另一层的事。这个认知对后续学习树和图很重要。栈可以用list直接实现,入栈是append,出栈是pop(),天然就是 O(1)。队列则要用deque,因为列表头部插入是 O(n)。
from collections import deque class Queue: """基于 deque 的队列实现""" def __init__(self): self._data = deque() def enqueue(self, item): """入队:从右端添加""" self._data.append(item) def dequeue(self): """出队:从左端移除""" if self.is_empty(): raise IndexError("队列为空,无法出队") return self._data.popleft() def is_empty(self): return len(self._data) == 0 def __len__(self): return len(self._data) # 括号匹配检查示例 def is_balanced_parentheses(s: str) -> bool: """用栈检查括号是否匹配""" stack = [] mapping = {')': '(', ']': '[', '}': '{'} for char in s: if char in '([{': stack.append(char) elif char in ')]}': if not stack or stack[-1] != mapping[char]: return False stack.pop() return len(stack) == 0 print(is_balanced_parentheses("({[]})")) # True print(is_balanced_parentheses("({[}])")) # FalseQueue类的设计体现了封装思想:外部调用者只知道enqueue和dequeue,内部用deque还是list是可替换的实现细节。括号匹配这段代码用stack列表和映射表完成判断:遇到左括号压栈,遇到右括号检查栈顶是否匹配,最后看栈是否为空。mapping字典把问题转换成了“查找配对”,避免了写多个if分支。
3.3 二叉堆:用列表实现优先队列的 Python 式思路
二叉堆是树结构里最适合用 Pythonlist实现的一种——因为完全二叉树的节点位置可以用数组下标精确对应:下标i的节点的左子节点在2*i+1,右子节点在2*i+2,父节点在(i-1)//2。这个数学性质让“树”可以用线性结构存储,是最能体现“数据结构是逻辑结构,存储方式可以多样”的例子。
class MinHeap: """最小二叉堆,用列表存储,支持 insert 和 extract_min""" def __init__(self): self._data = [] def _sift_up(self, index): """新元素尾部插入后,向上浮动到合适位置""" parent = (index - 1) // 2 while index > 0 and self._data[index] < self._data[parent]: self._data[index], self._data[parent] = self._data[parent], self._data[index] index = parent parent = (index - 1) // 2 def _sift_down(self, index): """根节点被替换后,向下沉到合适位置""" n = len(self._data) while True: smallest = index left = 2 * index + 1 right = 2 * index + 2 if left < n and self._data[left] < self._data[smallest]: smallest = left if right < n and self._data[right] < self._data[smallest]: smallest = right if smallest == index: break self._data[index], self._data[smallest] = self._data[smallest], self._data[index] index = smallest def insert(self, value): """插入: 先放尾部,再上浮""" self._data.append(value) self._sift_up(len(self._data) - 1) def extract_min(self): """删除最小值: 用最后一个元素替换根,再下沉""" if not self._data: raise IndexError("堆为空") min_value = self._data[0] last = self._data.pop() if self._data: self._data[0] = last self._sift_down(0) return min_value def peek(self): """查看最小值但不删除""" if not self._data: raise IndexError("堆为空") return self._data[0]_sift_up和_sift_down这两个私有方法是堆的核心逻辑。插入时把新元素放到数组末尾,然后不断和父节点比较、交换,直到满足堆性质。取出最小值时,把堆顶替换成末尾元素,然后下沉。insert和extract_min的时间复杂度都是 O(log n),因为树的高度是 log n。这个类可以直接用于 Top-K 问题——重复插入元素,堆大小超过 K 时弹出最小值,最后堆里就是最大的 K 个元素。
4. 排序算法的 Python 源码级视角:不只是写出来,还要懂为什么快
4.1 从 Python 内置sort看 TimSort 的设计哲学
Python 内置的list.sort()使用的 TimSort 算法,是世界上唯一一个把“利用数据中已有序片段”作为核心策略的排序算法。普通快速排序在近乎有序的数据上反而表现差,TimSort 则专门处理这种情况——它扫描数据,把天然有序的子序列识别为 “run”,然后按规则合并这些 run。这个算法是这本书源码分析价值的最好例证:光看伪代码学排序,永远理解不了为什么 Python 的排序在真实数据上总是表现稳定。
# 理解 TimSort 的切入点:查看排序中间状态的稳定性 import random data = [("A", 3), ("B", 1), ("C", 3), ("D", 2)] # 按第二个元素排序,比较相同值之间的相对顺序 data.sort(key=lambda x: x[1]) print(data) # [('B', 1), ('D', 2), ('A', 3), ('C', 3)]如果用lambda x: x[1]作为key,A和C的 key 都是 3,排序后A仍在C前面——这就是排序算法的“稳定性”。TimSort 是稳定排序,归并排序的逻辑天然保证稳定性。实际写代码时,使用key而不是cmp是 Python 社区的共识——key函数只被调用一次,而cmp在比较时反复调用,性能差距巨大。sort还有一个重要的参数reverse,它不是简单地把结果倒过来,而是从底层就按逆序比较,效率更高。
4.2 快速排序的 Python 实现:分区逻辑和递归深度陷阱
快速排序在 Python 里实现起来代码量不大,但有一个 Java 或 C++ 里不太容易踩的坑——递归深度。Python 的默认递归限制是 1000 层,快速排序最坏情况需要 n 次递归,这意味着对 2000 个以上元素的有序数组排序时,快速排序会直接抛出RecursionError。书里给出的解法不是调大限制,而是引入随机化选择基准值,让最坏情况变得几乎不可能发生。
import random def quick_sort(arr): """随机化快速排序,避免有序数据导致最坏情况""" if len(arr) <= 1: return arr pivot = random.choice(arr) less = [x for x in arr if x < pivot] equal = [x for x in arr if x == pivot] greater = [x for x in arr if x > pivot] return quick_sort(less) + equal + quick_sort(greater) # 用近乎有序的数据测试 data = list(range(1, 101)) random.shuffle(data) sorted_data = quick_sort(data) print(len(sorted_data) == 100) # True这个实现用三个列表推导式完成分区,代码可读性极高,虽然空间复杂度比原地快排高(需要额外列表存分区结果),但胜在不会写错。关键优化是random.choice(arr)随机选基准,目的不是“碰运气”,而是让任何特定输入(比如已经排好序的数组)都无法触发最坏情况。另一个细节是equal列表专门收集和基准相等的元素,这样有大量重复元素的数据也能在线性时间内完成分区,避免出现一边为空的情况——这是很多教科书快排实现没处理的细节。实际工程中数据量超过百万时,我不太建议用纯 Python 快排,list.sort()的 C 语言实现比它快一个数量级以上。
4.3 二分查找边界:bisect模块的正确打开方式
二分查找写起来只有十几行,但边界条件错一个字符就全盘皆输——左闭右开还是左闭右闭、相等时往左还是往右。Python 标准库的bisect模块把这个问题彻底解决了,它是 C 语言实现,性能远高于手写的纯 Python 二分法。书里对bisect的源码分析能帮你理解这几点:bisect_left返回第一个不小于目标值的位置,bisect_right返回第一个大于目标值的位置,两者的差异在有重复元素时至关重要。
import bisect def find_insert_position(sorted_list, target): """返回 target 应该插入的位置,保持有序性""" left = bisect.bisect_left(sorted_list, target) right = bisect.bisect_right(sorted_list, target) return left, right nums = [1, 3, 5, 7, 9] pos_left = bisect.bisect_left(nums, 6) print(f"6 应插入位置: {pos_left}") # 3, 因为 nums[3]=7 > 6 dup_list = [1, 3, 3, 3, 5] l = bisect.bisect_left(dup_list, 3) r = bisect.bisect_right(dup_list, 3) print(f"3 的区间: [{l}, {r})") # [1, 4), 表示 3 出现 3 次bisect_left在目标值已存在时返回最左侧的相同值下标,bisect_right返回最右侧下标加一。这个区间语义配合 Python 的切片操作非常自然:dup_list[l:r]就能拿到所有等于目标值的元素。知道这个区别能省掉大量手写二分查找的调试时间——统计一个有序数组里某个值的出现次数,标准做法就是bisect_right - bisect_left。
5. Python 数据结构和算法的避坑指南:五个真实踩过的坑
5.1list当队列用,数据一多直接卡死
现象:用list.insert(0, item)实现队列功能,几千条数据时很流畅,到几万条时程序明显变慢,十万条时几乎卡住。 原因:list是连续内存数组,头部插入要让所有已有元素向后移动一个位置,时间复杂度 O(n),数据量线性增长,耗时是平方级增长。 解决:改用collections.deque,头尾操作都是 O(1)。deque底层是双向链表块,插入和删除只在端点发生时性能稳定。这是这本书最基础的告诫之一——了解容器的复杂度特征,比记住 API 更重要。
5.2 递归深度和内存泄漏:算法的隐形杀手
现象:用递归实现深度优先搜索,处理深度 5000 以上的树结构时抛出RecursionError,或者程序内存占用持续增长。 原因:Python 函数调用栈默认深度只有 1000,超出即报错;递归里如果持有对容器对象的引用,函数返回后也因为闭包等原因不释放。 解决:对于明确知道深度的数据,先用sys.setrecursionlimit()评估安全边界,但更好的做法是把递归改成显式栈迭代。这本书反复强调“递归是思想,不是实现手段”——树的遍历完全可以用while循环加栈结构模拟,避免栈溢出。
5.3 默认参数是可变对象:函数变成黑匣子
现象:定义一个函数def add_item(item, cache=[]),多次调用后发现cache里残留了上一次调用的数据,结果不可预测。 原因:Python 在函数定义时创建默认参数对象,调用时不重新创建,可变对象会在多次调用间共享状态。 解决:默认参数改成None,函数体里再初始化。这个坑在算法题里尤其隐蔽——如果一个递归函数用默认列表存中间结果,每次调用都会积累脏数据,最终返回值错得离谱。
5.4==和is混用:整数缓存带来的幻觉
现象:判断两个位置是否相同时,有时用==返回 True,有时用is返回 True,但换了数字结果又不同,让人摸不着头脑。 原因:Python 对 -5 到 256 的整数有缓存机制,小整数是单例对象,is比较的是对象身份(内存地址),特别大的整数每次创建新对象,is就返回 False。 解决:判断值相等一律用==,判断“是不是同一个对象”才用is。在链表或树结构里,如果两个节点变量的value相同就用is判断,会导致本应相等的节点被认为不同,整个算法逻辑崩溃。这本书里所有节点比较都用==,原因就在这里。
5.5 复杂度分析只看最坏情况,忽略 Python 具体实现
现象:评估一个算法时说“这是 O(n log n) 所以应该够快”,结果实际跑起来比 O(n²) 的暴力解法还慢。 原因:大 O 复杂度忽略了常数因子和 Python 解释器的执行开销。比如归并排序的纯 Python 实现比快速排序慢,因为切片和数组合并产生大量对象分配;而 Python 内置sorted()因为底层是 C 实现,哪怕复杂度相同也快得多。 解决:先靠复杂度分析选算法类别,再用timeit对关键路径做基准测试。书里的做法是每个数据结构都附性能测试代码,把理论分析和实际测量绑定在一起,这个习惯值得照着学。
6. 进阶技巧:用抽象基类统一接口,把数据结构变成可替换组件
到这一步,你已经实现了链表、队列、二叉堆,够用了但总感觉差点意思。差在哪里?接口不统一——链表用insert_at_beginning,队列用enqueue,堆用insert,三个名字三种写法,换一种数据结构实现相同逻辑时调用方代码就得改。这本书反复提到的面向对象观点,在这里落地的关键就是抽象基类(ABC 模块)。
from abc import ABC, abstractmethod class Collection(ABC): """统一所有数据结构的抽象基类""" @abstractmethod def add(self, item): """添加元素""" pass @abstractmethod def remove(self): """移除并返回一个元素""" pass @abstractmethod def __len__(self): """返回元素个数""" pass class Stack(Collection): """栈:后进先出""" def __init__(self): self._items = [] def add(self, item): self._items.append(item) def remove(self): if not self._items: raise IndexError("栈为空") return self._items.pop() def __len__(self): return len(self._items) class Queue(Collection): """队列:先进先出,复用之前实现的 Queue 类接口""" def __init__(self): from collections import deque self._items = deque() def add(self, item): self._items.append(item) def remove(self): if not self._items: raise IndexError("队列为空") return self._items.popleft() def __len__(self): return len(self._items) def process_all(collection: Collection): """不管传进来的是栈还是队列,统一按 add/remove 调用""" for item in ["task1", "task2", "task3"]: collection.add(item) while len(collection) > 0: print(collection.remove()) stack = Stack() queue = Queue() process_all(stack) # 输出顺序: task3, task2, task1 process_all(queue) # 输出顺序: task1, task2, task3Collection抽象基类定义了三个核心操作:add、remove、__len__。任何继承它的类必须实现这些方法,否则实例化时会报TypeError。最关键的优势在process_all函数——它的参数类型标注是Collection,意味着传入栈和队列的行为完全由子类决定。同样的数据、同样的写入顺序,栈返回逆序,队列返回正序。这个模式在真实项目里的价值是:数据结构的选择可以延迟到调用方决定,核心算法代码不感知底层变化。
这套抽象设计也暴露了一个值得思考的点:继承不是越多越好。抽象基类适合在“操作语义一致”的场景使用——栈和队列都支持“加入一个元素、取出一个元素”,但二叉树和图的遍历方式差异太大,强行抽象成同一个接口反而让代码难以理解。这本书的取舍标准是:能抽象就抽象,但每个抽象都基于真实的操作共性,不为了炫技而造继承链。
我对这本书的最终评价是:它能帮你把“会用 Python 容器”升级成“理解容器背后是什么”,并直接作用于面试和工程两个场景。我自己的习惯是每学完一个数据结构,就用它重写一遍最近写的某个业务逻辑——比如把订单列表改成用堆实现优先处理高金额订单,把消息队列换成自定义链表版观察内存变化。这种“学完立刻迁移到真实代码”的做法让我对每个结构都印象深刻。这本书的实战价值不取决于你读了多少遍,而取决于你照着代码敲了几遍、改了几遍、又用在了哪里。希望帮到你。
本文还有配套的精品资源,点击获取