1. 二叉树基础概念扫盲
第一次接触二叉树时,我完全被那些"结点"、"度"、"层次"这些术语搞晕了。后来才发现,二叉树其实就像公司的组织架构图一样直观。想象一下,CEO是根节点,下面有两个直接下属(左子树和右子树),每个下属又可能有自己的团队,这就是最简单的二叉树模型。
二叉树(Binary Tree)是每个节点最多有两个子节点的树结构。与普通树不同,二叉树严格区分左孩子和右孩子,即使只有一个子节点也要明确是左还是右。这个特性让二叉树在算法中有着特殊的地位。
1.1 二叉树的五大核心属性
根节点(Root):树的顶端节点,就像家族树的始祖。每个二叉树有且只有一个根节点。
父节点与子节点:节点A直接连接的下一层节点B和C,A是父节点,B/C是子节点。注意:子节点有明确的左右之分!
叶子节点(Leaf):没有子节点的节点,就像组织架构图中的基层员工。
度(Degree):一个节点拥有的子节点数。二叉树中节点的度最大为2。
深度(Depth)与高度(Height):
- 深度:从根到该节点的边数(根深度为0)
- 高度:从该节点到最远叶子节点的边数(叶子高度为0)
注意:不同教材对深度/高度的定义可能不同,有的从1开始计数,学习时要注意统一标准。
1.2 二叉树的三种特殊形态
在实际编码中,我们最常遇到这三种特殊二叉树:
满二叉树:每个节点都有0或2个子节点,且所有叶子节点在同一层。就像完美对称的金字塔。
完全二叉树:除最后一层外,其他层节点数都达到最大,且最后一层节点都靠左排列。这种结构在堆排序中非常重要。
二叉搜索树(BST):左子树所有节点值小于根节点,右子树所有节点值大于根节点。这种特性让查找效率可以达到O(log n)。
2. 二叉树的存储之道
2.1 链式存储:最直观的表达方式
链式存储就像给每个员工做一张员工卡:
struct TreeNode { int val; // 员工工号 TreeNode *left; // 左下属名片 TreeNode *right; // 右下属名片 };这种方式的优势是直观,插入删除灵活。但缺点也很明显:
- 每个节点需要额外空间存储指针
- 非连续存储可能导致缓存命中率低
2.2 顺序存储:数组的妙用
对于完全二叉树,我们可以用数组紧凑存储:
- 根节点放在index=1的位置(index=0空置)
- 对于节点i,其左孩子是2i,右孩子是2i+1
- 父节点是i/2(整数除法)
这种存储方式节省指针空间,且缓存友好。堆(Heap)就是基于这种存储实现的。
实测数据:在100万个节点的完全二叉树遍历中,顺序存储比链式存储快3-5倍。
3. 二叉树遍历的四种姿势
遍历是二叉树算法的基础,就像参观博物馆有不同的参观路线。
3.1 深度优先遍历(DFS)
前序遍历:根→左→右
def preorder(root): if not root: return print(root.val) # 先处理当前节点 preorder(root.left) # 再左子树 preorder(root.right) # 最后右子树应用场景:复制二叉树结构
中序遍历:左→根→右
def inorder(root): if not root: return inorder(root.left) # 先左子树 print(root.val) # 处理当前节点 inorder(root.right) # 最后右子树应用场景:BST中得到有序序列
后序遍历:左→右→根
def postorder(root): if not root: return postorder(root.left) # 先左子树 postorder(root.right) # 再右子树 print(root.val) # 最后处理当前节点应用场景:计算子树大小或释放二叉树内存
3.2 广度优先遍历(BFS)
层次遍历使用队列实现:
from collections import deque def levelOrder(root): if not root: return [] queue = deque([root]) while queue: node = queue.popleft() print(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right)应用场景:按层处理节点,如打印树结构
避坑指南:递归实现DFS虽然简洁,但树很深时可能导致栈溢出。实际工程中,对于深度不确定的树,建议使用显式栈的迭代写法。
4. 二叉树常见算法套路
4.1 递归三要素
解决二叉树问题,90%可以用递归。写好递归需要把握三个要点:
- 终止条件:通常是遇到空节点或叶子节点
- 本级任务:当前节点要处理的事情
- 向下递归:调用函数处理子问题
以计算二叉树深度为例:
def maxDepth(root): if not root: return 0 # 终止条件 left_depth = maxDepth(root.left) # 左子树深度 right_depth = maxDepth(root.right) # 右子树深度 return max(left_depth, right_depth) + 1 # 本级处理4.2 经典问题实战
问题1:判断对称二叉树
技巧:转化为判断两棵树是否镜像
def isSymmetric(root): def compare(left, right): if not left and not right: return True if not left or not right: return False return (left.val == right.val and compare(left.left, right.right) and compare(left.right, right.left)) return compare(root.left, root.right) if root else True问题2:最近公共祖先(LCA)
后序遍历的经典应用:
def lowestCommonAncestor(root, p, q): if not root or root == p or root == q: return root left = lowestCommonAncestor(root.left, p, q) right = lowestCommonAncestor(root.right, p, q) if left and right: return root return left if left else right5. 二叉树算法优化技巧
5.1 记忆化搜索
对于需要重复计算的子树问题,可以用哈希表缓存结果。比如计算从根到叶子节点的所有路径:
def binaryTreePaths(root): memo = {} def helper(node): if node in memo: return memo[node] if not node: return [] if not node.left and not node.right: memo[node] = [str(node.val)] return memo[node] paths = [] for path in helper(node.left): paths.append(f"{node.val}->{path}") for path in helper(node.right): paths.append(f"{node.val}->{path}") memo[node] = paths return paths return helper(root)5.2 Morris遍历:O(1)空间复杂度的中序遍历
利用叶子节点的空指针实现无栈遍历:
def morrisInorder(root): curr = root while curr: if not curr.left: print(curr.val) curr = curr.right else: # 找前驱节点 pre = curr.left while pre.right and pre.right != curr: pre = pre.right if not pre.right: pre.right = curr # 建立线索 curr = curr.left else: pre.right = None # 拆除线索 print(curr.val) curr = curr.right6. 二叉树在实际工程中的应用
6.1 数据库索引:B/B+树的基石
MySQL的InnoDB引擎使用B+树作为索引结构,其本质是多路平衡搜索树,可以看作二叉搜索树的扩展。理解二叉树是学习这些高级数据结构的基础。
6.2 游戏开发:场景树管理
在游戏引擎中,场景中的对象通常组织成树结构(如Unity的GameObject层级),碰撞检测、渲染排序等操作都需要遍历这些树结构。
6.3 机器学习:决策树算法
决策树是二叉树的重要应用,每个内部节点表示一个特征判断,叶子节点表示分类结果。理解二叉树遍历有助于理解决策树的生成过程。
我在实际项目中遇到的一个典型场景是处理组织架构图。需要计算某个部门的所有下级部门,这本质上就是二叉树的遍历问题。通过采用后序遍历结合记忆化技术,我们将原本O(n^2)的复杂度优化到了O(n)。