☰
Python数据结构与算法实战:从链表到动态规划的代码实现与避坑指南
2026/10/7 11:00:53 网站建设 项目流程

简介:这份文档面向正在学习Python数据结构与算法的初学者与进阶开发者,系统梳理了从基础概念到高级结构的核心知识,帮助读者建立清晰的数据组织与算法设计思路。资源包内含1个docx文件,整体约15KB,以文字讲解为主,便于随时查阅与笔记整理。内容从数据结构与算法的定义及相互关系切入,依次展开数组、链表等基本结构,并深入二叉树、二叉搜索树与图等高级主题,涵盖节点定义、插入删除、遍历方式及邻接矩阵与邻接表等实现细节,同时结合Python语法给出可参考的代码示例。目前已有789人学习下载,适合希望夯实算法基础、准备课程复习或面试梳理的读者,可作为日常查阅与动手实践的知识手册。

1. 从一份 docx 说起:Python 数据结构与算法分析到底能帮你解决什么

很多人第一次接触数据结构与算法,是在考研复习或者面试突击的时候,翻开一本厚书,看到满页的公式和伪代码,然后就没有然后了。这份《Python 数据结构与算法分析.docx》走的是另一条路:它把线性表、树、图、排序、搜索、分治、动态规划、贪心这些核心内容,全部用 Python 代码串了一遍。你不需要先啃 C 指针,也不需要配一堆编译环境,打开 Python 就能把链表、二叉树、邻接矩阵跑起来。

它适合三类人:正在准备数据结构期末或考研 408 的在校生,想用 Python 把抽象概念跑通;转行做后端或数据方向、需要补算法基础的从业者;以及刷 LeetCode 时总觉得“知道思路但写不出来”的开发者。这份文档不是 API 手册,它的价值在于把每个结构的定义、操作和复杂度分析绑在一起讲,让你在写代码的同时理解为什么这样设计。接下来我会按“结构 → 算法 → 避坑 → 进阶”的顺序,把这份资料里最值得动手的部分拆开。

2. 线性结构落地:数组与链表的 Python 实现和边界处理

2.1 为什么 Python 里还要手写链表

Python 的 list 底层是动态数组,随机访问 O(1),尾部追加摊还 O(1),但头部插入是 O(n)。链表正好相反:头部插入 O(1),随机访问 O(n)。这份资料在第二章用 ListNode 类把单向链表从头搭了一遍,目的不是让你在生产环境放弃 list,而是让你理解“指针”在 Python 里就是对象引用,以及为什么面试官总爱考链表反转和环检测。

先看节点定义和基础操作。资料里的原始代码只有节点类,我补上插入、删除和遍历的完整写法,这样你复制到本地就能跑:

class ListNode: def __init__(self, val=0, next=None): self.val = val self.next = next # 构建 1 -> 2 -> 3 head = ListNode(1) head.next = ListNode(2) head.next.next = ListNode(3) # 在头部插入 0 new_head = ListNode(0) new_head.next = head head = new_head # 删除值为 2 的节点 prev, curr = head, head.next while curr: if curr.val == 2: prev.next = curr.next break prev, curr = curr, curr.next # 遍历 curr = head while curr: print(curr.val, end=" -> ") curr = curr.next

这段代码里,prev和curr双指针是链表删除的标准套路。参数上唯一需要注意的是:删除头节点时要单独处理,否则prev没有前驱。资料原文用del head.next来演示删除,那个写法在 Python 里只是解除引用,并不会自动把前驱的 next 指过去,实际链会断掉。这是原文档的一个小坑,我在第 5 章会集中说。

2.2 数组操作的时间复杂度对照

资料 2.1 节列了 append、insert、del 三种操作,但没有给出复杂度对照。我把它补成表格,方便你选型时直接查:

操作写法时间复杂度适用场景
尾部追加arr.append(x)O(1) 摊还日志收集、栈
指定位置插入arr.insert(i, x)O(n)小规模数据、有序插入
按索引删除del arr[i]O(n)需要保持顺序时
按值删除arr.remove(x)O(n)值唯一且靠前
尾部弹出arr.pop()O(1)栈顶出栈
头部弹出arr.pop(0)O(n)尽量避免,改用 deque

如果你需要频繁在两端插入删除,标准库的collections.deque是更合适的选择,它的两端操作都是 O(1)。资料没有提 deque,但这是实际写代码时绕不开的替代方案。

2.3 用链表实现栈和队列

理解了节点操作之后,用链表实现栈和队列就是水到渠成的事。栈是后进先出,只在头部操作;队列是先进先出,需要同时维护头尾指针。下面是一个最小可用的链式队列:

class LinkedQueue: def __init__(self): self.head = None self.tail = None def enqueue(self, val): node = ListNode(val) if self.tail: self.tail.next = node else: self.head = node self.tail = node def dequeue(self): if not self.head: return None val = self.head.val self.head = self.head.next if not self.head: self.tail = None return val

enqueue里判断self.tail是否为空,是为了处理第一个节点入队时 head 和 tail 同时指向它的情况。dequeue里出队后如果 head 变成 None,必须把 tail 也置空,否则 tail 会指向一个已经不在队列里的节点,后续 enqueue 会接错。这个细节在资料原文里没有展开,但它是链式队列最常见的翻车点。

3. 树与图的 Python 建模:从二叉树遍历到邻接矩阵

3.1 二叉树三种遍历的递归与迭代写法

资料第三章给出了前序、中序、后序的递归实现,代码能跑,但参数里多了一个没用的data,而且没有讲迭代写法。递归写法的核心是调用栈,Python 默认递归深度 1000,树稍微深一点就会RecursionError。我一般会同时准备迭代版本,面试和实际项目都用得上。

先看修正后的递归写法:

class Node: def __init__(self, data): self.data = data self.left = None self.right = None def preorder(root): if root is None: return print(root.data) preorder(root.left) preorder(root.right) def inorder(root): if root is None: return inorder(root.left) print(root.data) inorder(root.right) def postorder(root): if root is None: return postorder(root.left) postorder(root.right) print(root.data)

迭代版前序用一个栈就能搞定,中序需要一路压左子节点,后序可以按“根右左”压栈再反转:

def preorder_iter(root): stack = [root] while stack: node = stack.pop() if node: print(node.data) stack.append(node.right) stack.append(node.left) def inorder_iter(root): stack, curr = [], root while stack or curr: while curr: stack.append(curr) curr = curr.left curr = stack.pop() print(curr.data) curr = curr.right

参数上唯一要改的是:迭代版不需要额外传data,遍历逻辑只依赖节点本身。资料原文的preorder(tree, data)里data完全没被使用,属于冗余参数,直接删掉即可。

3.2 二叉搜索树的插入与查找

二叉搜索树(BST)的性质是左子树所有值小于根,右子树所有值大于根。资料 3.1.3 只说了定义,没给插入和查找代码。补上:

def bst_insert(root, val): if root is None: return Node(val) if val < root.data: root.left = bst_insert(root.left, val) elif val > root.data: root.right = bst_insert(root.right, val) return root def bst_search(root, val): if root is None or root.data == val: return root if val < root.data: return bst_search(root.left, val) return bst_search(root.right, val)

插入时如果值相等,这里选择不插入,避免重复节点。查找的平均复杂度是 O(log n),但树退化成链表时会变成 O(n)。所以实际项目里更常用bisect模块维护有序数组,或者用平衡树结构。资料没有展开平衡树,但你需要知道 BST 的 O(log n) 是有前提的。

3.3 图的邻接矩阵与邻接表怎么选

资料 3.2 节提到图可以用邻接矩阵或邻接表表示,但节点类和边类的代码是残缺的。我用 Python 的 list 和 dict 重新实现两种表示,并给出选型依据:

# 邻接矩阵:适合稠密图,判断两点是否相邻 O(1) n = 5 matrix = [[0] * n for _ in range(n)] matrix[0][1] = 1 matrix[1][0] = 1 # 邻接表:适合稀疏图,遍历邻居 O(degree) adj = {i: [] for i in range(n)} adj[0].append(1) adj[1].append(0)

选型标准很简单:边数接近 n² 用矩阵,边数远小于 n² 用邻接表。社交网络、路由表这类稀疏场景,邻接表省内存且遍历快;而 Floyd 算法这种需要频繁查询任意两点距离的场景,矩阵更直接。资料原文的Node类里self.adjacent =后面是空的,Edge类也没有weight的赋值,直接跑会报语法错误,建议按上面的写法替换。

4. 排序与搜索:四种排序的 Python 实现和二分边界

4.1 冒泡、选择、插入、快排的代码与复杂度

资料第四章列了四种排序,但只有文字描述,没有完整代码。我把它们补全,并标注每种的适用场景:

def bubble_sort(arr): n = len(arr) for i in range(n): swapped = False for j in range(0, n - i - 1): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] swapped = True if not swapped: break return arr 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] mid = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return quick_sort(left) + mid + quick_sort(right)

冒泡里的swapped标记是优化点:如果某一轮没有发生交换,说明已经有序,直接退出。插入排序在数据基本有序时接近 O(n),这是它比冒泡和选择更实用的原因。快排这里用了列表推导式,代码短但额外空间 O(n),生产环境更推荐原地分区版本。资料原文说快排“实现较为复杂”,其实 Python 里用三路分区写出来并不长。

4.2 二分搜索的三种边界写法

资料 4.2.2 讲了二分搜索的原理,但没有给代码。二分最容易被边界条件搞晕,我给出最不容易出错的左闭右开写法:

def binary_search(arr, target): left, right = 0, len(arr) while left < right: mid = (left + right) // 2 if arr[mid] == target: return mid elif arr[mid] < target: left = mid + 1 else: right = mid return -1

关键参数是right = len(arr)而不是len(arr) - 1,循环条件是left < right,这样right始终是开区间。如果你写成right = len(arr) - 1和left <= right,就要在更新时写right = mid - 1,两种写法不能混。混用的后果是死循环或者漏掉最后一个元素,这是二分搜索最经典的血泪经验。

4.3 分治与归并排序的关系

资料 4.3 节讲了分治策略,并提到归并排序是典型应用,但没有给归并代码。归并排序的合并步骤是理解分治的关键:

def merge_sort(arr): if len(arr) <= 1: return arr mid = len(arr) // 2 left = merge_sort(arr[:mid]) right = merge_sort(arr[mid:]) return merge(left, right) def merge(left, right): result = [] i = j = 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result

merge里用<=而不是<,是为了保持稳定性:相等时先取左边的元素,这样相同值的相对顺序不变。归并排序的时间复杂度稳定在 O(n log n),但额外空间 O(n),这是它和快排最大的取舍点。

5. 避坑与排查:这份文档里那些跑不通的代码

5.1 数组索引越界:print(arr)的写法问题

现象:资料 2.1 节写print(arr)注释说输出 1、2、3,但实际arr是列表,直接 print 会输出整个列表[1, 2, 3],不是单个元素。原因:原文在复制时丢了索引下标。解决:改成print(arr[0])、print(arr[1])、print(arr[2]),或者用循环遍历。

5.2 链表删除用del head.next会断链

现象:资料 2.2 节用del head.next删除第二个节点,运行后链表从 1 直接断掉,后面的节点全丢了。原因:del只解除当前对象对属性的引用,不会把前驱节点的 next 指向后继。解决:用双指针找到待删节点的前驱,执行prev.next = curr.next,也就是第 2 章里我给的标准写法。

5.3 二叉树遍历的多余参数导致调用困惑

现象:preorder(tree, data)里data参数在函数体内从未使用,调用时必须多传一个无意义的值。原因:原文从其他语言示例移植时残留的参数。解决:删掉data,签名改为preorder(root),递归调用同步修改。

5.4 图节点类语法不完整直接报 SyntaxError

现象:资料 3.2.1 的Node类里self.adjacent =后面没有值,Edge类里self.后面直接换行,复制到编辑器会报语法错误。原因:文档在排版时截断了赋值语句。解决:self.adjacent = [],Edge补上self.weight = weight,或者直接用第 3 章的 dict 邻接表方案。

5.5 动态规划背包的初始化写法有误

现象:资料 5.2.2 的dp = [ * (capacity + 1) for _ in range(n + 1)]在 Python 里会报错,因为[ * (capacity + 1)]不是合法表达式。原因:原文想写的是二维数组初始化,但星号位置错了。解决:改成dp = [[0] * (capacity + 1) for _ in range(n + 1)],这样每个子列表独立,不会被共享引用坑到。

6. 进阶技巧:用functools.lru_cache给递归算法加后悔药

资料第五章的动态规划部分讲了状态转移方程,但没提 Python 标准库里的记忆化工具。实际写递归解法时,functools.lru_cache能直接把指数级递归压成多项式时间,相当于给递归加了一层后悔药。以斐波那契为例:

from functools import lru_cache @lru_cache(maxsize=None) def fib(n): if n < 2: return n return fib(n - 1) + fib(n - 2) print(fib(100))

不加装饰器时,fib(40)就要跑几十秒;加上之后,fib(100)瞬间返回。maxsize=None表示缓存不设上限,适合状态空间有限的场景。如果是背包问题这种二维状态,可以把参数设计成可哈希的元组,或者手动维护 dp 数组。

验证方法也很直接:在函数里加一个计数器,对比加与不加装饰器时的调用次数。我一般会跑三组数据——n=10、n=20、n=30,看调用次数是否从指数增长变成线性增长。如果没变化,说明缓存没命中,检查参数是否可变类型。

还有一个技巧是用sys.setrecursionlimit提高递归深度,但这只是治标。树深度超过几千时,迭代写法才是正解。从那以后我每次写递归算法,都会先问自己三个问题:状态能不能缓存、深度会不会超、有没有迭代替代。希望帮到你。

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

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

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

立即咨询