当前位置: 首页 > news >正文

树与二叉树【数据结构】

在这里插入图片描述

文章目录

    • 一、基本概念
      • 1. 节点(Node)—— 结构基石
        • 定义
        • 工程实现模板(Python)
        • 设计要点:
      • 2. 根节点(Root)—— 唯一起点
        • 数学定义
        • 工程意义
        • 示例:
      • 3. 父节点(Parent)与子节点(Child)—— 关系建模
        • 定义
        • 工程约束
      • 4. 叶节点(Leaf)—— 终止标识
        • 定义
        • 判定函数
        • 应用场景
      • 5. 深度(Depth)—— 自顶向下度量
        • 定义
        • 递归计算函数
      • 6. 高度(Height)—— 自底向上度量
        • 定义
        • 递归计算(推荐写法)
    • 二、二叉树 —— 从理论到工程实践
      • 严格定义
      • 数学性质(重要!面试高频)
      • 工程中常见的二叉树类型
    • 三、二叉树遍历 —— 四种方式深度剖析
      • (一)深度优先遍历(DFS)—— 递归与迭代双实现
        • ▶ 1. 前序遍历(Pre-order: Root → Left → Right)
          • 递归实现(推荐初学)
          • 迭代实现(显式栈)
          • Morris 前序遍历(空间O(1),进阶)
        • ▶ 2. 中序遍历(In-order: Left → Root → Right)
          • 递归实现
          • 迭代实现(经典栈方法)
        • ▶ 3. 后序遍历(Post-order: Left → Right → Root)
          • 递归实现
          • 迭代实现(双栈法)
          • 单栈+状态标记法
      • (二)广度优先遍历 —— 层序遍历(BFS)
        • 标准层序(按层分组)
        • 扁平化层序(一维列表)
        • 逆层序遍历(从最底层向上)
    • 四、复杂度分析与边界处理大全
      • 时间复杂度统一结论
      • 空间复杂度对比
      • 边界条件处理清单
    • 五、工程最佳实践与面试高频考点
      • 工程规范
      • 面试高频变形题
    • 六、总结对比表
    • 七、学习路线建议


如果觉得本文对您有所帮助,请点个赞和关注吧,谢谢!!!你的支持就是我持续更新的最大动力


一、基本概念

树是一种递归定义的分层非线性数据结构,在计算机科学中用于高效组织具有层级或嵌套关系的数据(如文件系统、DOM树、表达式解析、路由表等)。


1. 节点(Node)—— 结构基石

定义

节点是树的基本组成单位,包含数据域指针域(指向子节点)。

工程实现模板(Python)
from typing import Optional, Listclass TreeNode:"""二叉树节点类属性:val: 节点存储的值(可为任意类型,通常为int/str)left: 左子节点引用,默认Noneright: 右子节点引用,默认None"""def __init__(self, val=0, left: Optional['TreeNode'] = None, right: Optional['TreeNode'] = None):self.val = valself.left = leftself.right = rightdef __repr__(self):return f"TreeNode({self.val})"
设计要点:
  • 使用 Optional[TreeNode] 明确子节点可为空。
  • 实现 __repr__ 便于调试打印。
  • 支持泛型扩展(如需存储对象,可改用 TypeVar)。

2. 根节点(Root)—— 唯一起点

数学定义

根节点是树中入度为0的唯一节点(无父节点)。

工程意义
  • 所有遍历、搜索、插入、删除操作均以根为入口。
  • root is None,则代表空树,应作为边界条件优先处理。
示例:
root = TreeNode(1)
root.left = TreeNode(2)
root.right = TreeNode(3)
# 此时 root 是整棵树的访问入口

3. 父节点(Parent)与子节点(Child)—— 关系建模

定义
  • 父节点:直接上层节点,每个节点最多一个父节点(除根)。
  • 子节点:直接下层节点,在二叉树中最多两个(左、右)。
工程约束
  • 二叉树中左右子节点不可互换,顺序具有语义(如BST中小<大)。
  • 指针方向:单向向下(从父指向子),无反向指针(除非特殊设计如线索二叉树)。

4. 叶节点(Leaf)—— 终止标识

定义

没有子节点的节点称为叶节点(终端节点)。

判定函数
def is_leaf(node: Optional[TreeNode]) -> bool:"""判断是否为叶节点"""return node is not None and node.left is None and node.right is None
应用场景
  • 计算路径和(如LeetCode 112)
  • 统计叶子数
  • 树高计算终止条件

5. 深度(Depth)—— 自顶向下度量

定义

节点深度 = 从根节点到该节点所经过的边数(根深度 = 0)。

递归计算函数
def get_depth(root: Optional[TreeNode], target_val: int) -> int:"""查找特定值节点的深度(若不存在返回-1)时间复杂度: O(n)空间复杂度: O(h)"""def dfs(node, current_depth):if not node:return -1if node.val == target_val:return current_depthleft_depth = dfs(node.left, current_depth + 1)if left_depth != -1:return left_depthreturn dfs(node.right, current_depth + 1)return dfs(root, 0)

6. 高度(Height)—— 自底向上度量

定义

节点高度 = 从该节点到最远叶节点的最长路径边数(叶节点高度 = 0)。

递归计算(推荐写法)
def get_height(node: Optional[TreeNode]) -> int:"""计算节点高度公式: height(node) = max(height(left), height(right)) + 1边界: height(None) = -1 (或 height(Leaf) = 0)返回值: 整数高度"""if not node:return -1  # 空节点高度为-1,使叶节点高度=0left_height = get_height(node.left)right_height = get_height(node.right)return max(left_height, right_height) + 1

标准约定:空树高度 = -1,单节点树高度 = 0。某些教材定义不同,需注意上下文。


二、二叉树 —— 从理论到工程实践

严格定义

二叉树(Binary Tree)是满足以下条件的树结构:

  1. 每个节点最多有两个子节点;
  2. 子节点区分左(left)和右(right),顺序不可交换;
  3. 左右子树本身也是二叉树(递归定义)。

数学性质(重要!面试高频)

性质编号描述公式/说明
P1第 i 层最多节点数2^(i-1) (i ≥ 1)
P2深度为 k 的二叉树最多节点总数2^k - 1
P3叶节点数 = 度为2的节点数 + 1n0 = n2 + 1
P4节点总数 n = n0 + n1 + n2n0:叶, n1:度1, n2:度2
P5完全二叉树中,节点 i 的左孩子 = 2i,右孩子 = 2i+1(数组索引从1起)用于堆实现
P6n 个节点的完全二叉树高度 = ⌊log₂n⌋

推导示例(P3):
设总边数 E = n - 1(树性质)
又 E = 0·n0 + 1·n1 + 2·n2
⇒ n - 1 = n1 + 2n2
代入 n = n0 + n1 + n2
⇒ n0 + n1 + n2 - 1 = n1 + 2n2
⇒ n0 = n2 + 1


工程中常见的二叉树类型

类型定义应用场景
满二叉树除叶节点外,所有节点都有两个子节点理论模型、性能分析基准
完全二叉树除最后一层外全满,最后一层节点靠左排列堆(Heap)、数组存储优化
平衡二叉树任意节点左右子树高度差 ≤ 1AVL树、保证O(log n)操作
二叉搜索树(BST)左子树 < 根 < 右子树(中序遍历有序)快速查找、范围查询
退化二叉树每层仅一个节点(形如链表)最坏情况分析、需避免

三、二叉树遍历 —— 四种方式深度剖析

遍历的本质:按特定顺序访问每个节点一次且仅一次


(一)深度优先遍历(DFS)—— 递归与迭代双实现

▶ 1. 前序遍历(Pre-order: Root → Left → Right)
递归实现(推荐初学)
def preorder_recursive(root: Optional[TreeNode]) -> List[int]:"""递归前序遍历执行顺序:1. 访问当前节点(追加值)2. 递归遍历左子树3. 递归遍历右子树时间复杂度: O(n) — 每个节点访问一次空间复杂度: O(h) — 递归栈深度(h=树高)"""if not root:return []result = [root.val]result += preorder_recursive(root.left)result += preorder_recursive(root.right)return result
迭代实现(显式栈)
def preorder_iterative(root: Optional[TreeNode]) -> List[int]:"""迭代前序遍历(使用栈模拟递归)关键技巧:- 先压右子节点,再压左子节点(栈LIFO,左先出)- 根节点最先访问,符合“根左右”顺序步骤:1. 初始化栈,压入根节点2. 当栈非空:a. 弹出栈顶,记录值b. 若有右子,压入右子c. 若有左子,压入左子"""if not root:return []stack, result = [root], []while stack:node = stack.pop()result.append(node.val)if node.right:  # 先压右stack.append(node.right)if node.left:   # 后压左 → 左先弹出stack.append(node.left)return result
Morris 前序遍历(空间O(1),进阶)
def preorder_morris(root: Optional[TreeNode]) -> List[int]:"""Morris遍历:利用叶节点右指针建立临时链接,遍历后恢复核心思想:对每个节点:- 若无左子,访问并移至右子- 若有左子:a. 找左子树最右节点(前驱)b. 若前驱右指针为空,建立到当前节点的链接,访问当前,移至左子c. 若已建立链接,断开链接,移至右子空间复杂度: O(1)时间复杂度: O(n) — 每条边最多遍历两次"""result = []current = rootwhile current:if not current.left:result.append(current.val)current = current.rightelse:# 找前驱节点(左子树最右)predecessor = current.leftwhile predecessor.right and predecessor.right != current:predecessor = predecessor.rightif not predecessor.right:# 建立临时链接predecessor.right = currentresult.append(current.val)  # 访问根(前序)current = current.leftelse:# 恢复树结构predecessor.right = Nonecurrent = current.rightreturn result

▶ 2. 中序遍历(In-order: Left → Root → Right)
递归实现
def inorder_recursive(root: Optional[TreeNode]) -> List[int]:"""递归中序遍历执行顺序:1. 递归遍历左子树2. 访问当前节点3. 递归遍历右子树BST应用: 输出升序序列"""if not root:return []result = []result.extend(inorder_recursive(root.left))result.append(root.val)result.extend(inorder_recursive(root.right))return result
迭代实现(经典栈方法)
def inorder_iterative(root: Optional[TreeNode]) -> List[int]:"""迭代中序遍历关键技巧:- 一路向左到底,沿途节点入栈- 弹出栈顶访问,转向其右子树- 重复直到栈空且无当前节点为什么有效?左子树全部入栈后,栈顶即为“最左节点”,访问后其右子树成为新子问题"""stack, result = [], []current = rootwhile stack or current:# 1. 一路向左,全部入栈while current:stack.append(current)current = current.left# 2. 弹出栈顶(此时无左子),访问current = stack.pop()result.append(current.val)# 3. 转向右子树current = current.rightreturn result

▶ 3. 后序遍历(Post-order: Left → Right → Root)
递归实现
def postorder_recursive(root: Optional[TreeNode]) -> List[int]:"""递归后序遍历执行顺序:1. 递归遍历左子树2. 递归遍历右子树3. 访问当前节点应用场景:- 删除树(先删子节点)- 计算目录大小(先算子目录)- 表达式树求值"""if not root:return []result = []result.extend(postorder_recursive(root.left))result.extend(postorder_recursive(root.right))result.append(root.val)return result
迭代实现(双栈法)
def postorder_iterative_dualstack(root: Optional[TreeNode]) -> List[int]:"""双栈迭代后序遍历思路:栈1:模拟前序遍历但顺序为 根→右→左栈2:接收栈1弹出的节点最终从栈2弹出即得 左→右→根步骤:1. 根入栈12. 当栈1非空:a. 弹出节点,压入栈2b. 压入左子(若有)c. 压入右子(若有)3. 依次弹出栈2得到后序序列"""if not root:return []stack1, stack2, result = [root], [], []while stack1:node = stack1.pop()stack2.append(node)if node.left:stack1.append(node.left)if node.right:stack1.append(node.right)while stack2:result.append(stack2.pop().val)return result
单栈+状态标记法
def postorder_iterative_onestack(root: Optional[TreeNode]) -> List[int]:"""单栈迭代后序遍历(带访问状态标记)状态设计:(node, visited) — visited=False表示首次访问,需继续探左/右;True表示子树已遍历完,可访问输出步骤:1. 压入 (root, False)2. 当栈非空:a. 查看栈顶b. 若未访问:- 标记为已访问(不弹出)- 依次压入右子(False)、左子(False)(注意顺序!)c. 若已访问:- 弹出并记录值"""if not root:return []stack, result = [(root, False)], []while stack:node, visited = stack.pop()if visited:result.append(node.val)else:# 重新压入,标记已访问stack.append((node, True))# 先压左?不!后序是左→右→根,所以要先压右再压左,让左先处理if node.right:stack.append((node.right, False))if node.left:stack.append((node.left, False))return result

(二)广度优先遍历 —— 层序遍历(BFS)

标准层序(按层分组)
from collections import dequedef level_order_grouped(root: Optional[TreeNode]) -> List[List[int]]:"""分层输出层序遍历结果核心技巧:- 使用队列- 每层开始前记录当前层节点数(queue长度)- 一次性处理完该层所有节点,再进入下一层时间复杂度: O(n)空间复杂度: O(w) — w为最大层宽(最宽层节点数)"""if not root:return []queue = deque([root])result = []while queue:level_size = len(queue)  # 当前层节点数current_level = []for _ in range(level_size):node = queue.popleft()current_level.append(node.val)if node.left:queue.append(node.left)if node.right:queue.append(node.right)result.append(current_level)return result
扁平化层序(一维列表)
def level_order_flat(root: Optional[TreeNode]) -> List[int]:"""不按层分组,直接返回一维列表"""if not root:return []queue, result = deque([root]), []while queue:node = queue.popleft()result.append(node.val)if node.left:queue.append(node.left)if node.right:queue.append(node.right)return result
逆层序遍历(从最底层向上)
def level_order_reverse(root: Optional[TreeNode]) -> List[List[int]]:"""自底向上分层输出"""if not root:return []queue = deque([root])result = []while queue:level_size = len(queue)current_level = []for _ in range(level_size):node = queue.popleft()current_level.append(node.val)if node.left:queue.append(node.left)if node.right:queue.append(node.right)result.append(current_level)return result[::-1]  # 反转结果列表

四、复杂度分析与边界处理大全

时间复杂度统一结论

所有遍历算法时间复杂度均为 O(n) —— 每个节点被访问恰好一次。

空间复杂度对比

遍历方式递归空间复杂度迭代空间复杂度备注
前序/中序/后序O(h)O(h) — 栈深度h为树高,最坏O(n)
层序遍历不适用O(w) — 最大层宽完全二叉树时w≈n/2
Morris遍历O(1)O(1)修改树结构,需恢复

最坏情况:退化树(链表状)→ h = n → DFS空间复杂度O(n)


边界条件处理清单

所有函数必须处理:

if root is None:return []   # 或 return 0 / None,根据函数语义

节点访问前检查是否为None(尤其在迭代中)

栈/队列操作前检查是否为空

递归基线条件明确(叶节点或空节点)


五、工程最佳实践与面试高频考点

工程规范

  1. 类型标注必写

    from typing import Optional, List
    def func(root: Optional[TreeNode]) -> List[int]: ...
    
  2. 异常安全

    • 不假设输入有效性,主动检查 None
    • 不修改原树结构(除非明确要求,如Morris遍历需恢复)
  3. 命名清晰

    • preorder_recursive, inorder_iterative 等明确标识实现方式
    • 避免 traversal1, func2 等无意义命名
  4. 性能注释

    # 时间: O(n), 空间: O(h) — h为树高
    

面试高频变形题

  1. Zigzag层序遍历(LeetCode 103)

    • 奇数层从左到右,偶数层从右到左
    • 解法:层序遍历 + 按层号反转列表
  2. 右视图/左视图(LeetCode 199)

    • 每层最后一个/第一个节点
    • 解法:层序遍历取每层末元素
  3. 验证是否为BST(LeetCode 98)

    • 中序遍历应为严格升序
  4. 最近公共祖先LCA(LeetCode 236)

    • 后序遍历思想:自底向上找共同祖先
  5. 路径总和系列(LeetCode 112, 113, 437)

    • 前序遍历 + 回溯

六、总结对比表

特性前序遍历中序遍历后序遍历层序遍历
访问顺序根→左→右左→根→右左→右→根层级从左到右
递归实现难度★☆☆☆☆★★☆☆☆★★★☆☆不适用
迭代实现难度★★☆☆☆★★★☆☆★★★★☆★★☆☆☆
BST应用复制树输出排序序列删除节点层级分析
表达式树前缀表达式中缀表达式后缀表达式-
空间复杂度O(h)O(h)O(h)O(w)
是否可Morris✅(较复杂)
面试出现频率★★★★☆★★★★★★★★★☆★★★★☆

七、学习路线建议

掌握节点定义与树术语
理解二叉树性质与分类
手写四种遍历递归版本
实现迭代版本
掌握Morris遍历思想
刷LeetCode遍历相关题目
学习BST/平衡树/堆等衍生结构

如果觉得本文对您有所帮助,请点个赞和关注吧,谢谢!!!你的支持就是我持续更新的最大动力

http://www.dtcms.com/a/389633.html

相关文章:

  • RPM包版本号系统解析:设计哲学、比较规则与实践指南
  • IDEA启动异常
  • vite使用vue2项目
  • 前端性能优化实用方案(一):减少50%首屏资源体积的Webpack配置
  • SQL 条件函数 IF、CASE WHEN 用法速查
  • 【深度学习新浪潮】如何估算大模型的训练和推理内存需求?
  • PyTorch查看模块/类的所有方法/属性
  • 8大Android开发框架效率翻倍
  • docker基础知识与具体实践
  • 【多模态】Simple o3 提高多模态模型准确率
  • hybrid的配置
  • 理解虚拟细胞:初学者指南
  • 哪种体量的公司或者哪些行业哪些项目需要上云服务器?
  • Linux安装问题:404 Not Found?配置源列表sources.list,修改为可用镜像源就可以了!
  • Vue3 中 props 与 $emit 的使用及 defineProps 与 defineEmits 的区别详解
  • vue的跨域配置
  • 计算机网络实验03:交换机VLAN配置
  • Vue中v-if与v-show的区别及应用场景解析
  • C++造轮子:手搓 List 容器
  • redis-list的基本介绍
  • ​​[硬件电路-247]:开关电源的工作原理、优缺点及应用场合
  • 【面试】Java中的垃圾回收算法详解
  • AI使用心得-完善中
  • rust编写web服务01-项目起步与环境准备
  • ORM框架及SQLAlchemy
  • 驱动开发---双机调试搭建支持win11(2025)
  • 驱动开发1:内核程序框架
  • 生产制造如何应对客户的订单变更
  • 深入浅出SpringMVC:从入门到实战指南
  • 深度学习入门:从感知机到多层感知机,用逻辑电路讲透神经网络的进化