
简介这份文档面向正在学习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, val0, nextNone): 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 valenqueue里判断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 resultmerge里用而不是是为了保持稳定性相等时先取左边的元素这样相同值的相对顺序不变。归并排序的时间复杂度稳定在 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(maxsizeNone) def fib(n): if n 2: return n return fib(n - 1) fib(n - 2) print(fib(100))不加装饰器时fib(40)就要跑几十秒加上之后fib(100)瞬间返回。maxsizeNone表示缓存不设上限适合状态空间有限的场景。如果是背包问题这种二维状态可以把参数设计成可哈希的元组或者手动维护 dp 数组。验证方法也很直接在函数里加一个计数器对比加与不加装饰器时的调用次数。我一般会跑三组数据——n10、n20、n30看调用次数是否从指数增长变成线性增长。如果没变化说明缓存没命中检查参数是否可变类型。还有一个技巧是用sys.setrecursionlimit提高递归深度但这只是治标。树深度超过几千时迭代写法才是正解。从那以后我每次写递归算法都会先问自己三个问题状态能不能缓存、深度会不会超、有没有迭代替代。希望帮到你。本文还有配套的精品资源点击获取