数据结构 (树) 学习 2025年6月12日12:59:39
数据结构 (树)
二叉树
树形数据结构 每个节点 最多 有两个子节点
//基本二叉树 图示A/ \B C/ \ \D E F//A:根节点 树的顶层节点 子节点为 B , C //B 子节点 为 D E //C 子节点 只有F //D E F 叶子节点(没有子节点)//相关概念 //深度:根节点到当前节点的路径长度 //高度:从当前节点到最深 叶子节点(D,E,F) 的路径长度//拓展 完全二叉树 图示1/ \2 3/ \ / \4 5 6 7 //所有层级完全填充 每个节点都有2个 子节点
- 完全二叉树:除最后一层外,其他层节点全部填满,且最后一层节点靠左对齐。
- 满二叉树:每个节点都有0或2个子节点。
二叉树的遍历方式:深度优先遍历(DFS) 和 广度优先遍历(BFS)
DFS:
前序遍历 根节点>左子树>右子树 复制树、序列化、前缀表达式
中序遍历 左子树>根节点>右子树 BST排序、中缀表达式、验证BST
后序遍历 左子树>右子树>根节点 计算高度、删除树、后缀表达式
关键思想(deepseek):
-
前序:先处理当前节点,再递归处理子节点(适合“自上而下”操作)。
-
中序:先处理左子树,再处理当前节点(BST 相关操作)。
-
后序:先处理子节点,再处理当前节点(适合“自下而上”计算)。
class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = right# 前序遍历
def preorder_traversal(root):if not root:return []return [root.val] + preorder_traversal(root.left) + preorder_traversal(root.right)# 中序遍历
def inorder_traversal(root):if not root:return []return inorder_traversal(root.left) + [root.val] + inorder_traversal(root.right)# 后序遍历
def postorder_traversal(root):if not root:return []return postorder_traversal(root.left) + postorder_traversal(root.right) + [root.val]
BFS:
按层级从上倒下,从左到右遍历节点 (通常用队列实现)
# 广度优先遍历
from collections import deque//是 Python 标准库中的一个双端队列
def bfs_traversal(root):if not root:return []queue = deque([root])result = []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
倾斜二叉树(左倾斜)
//倾斜二叉树 图示A/B/
C
//整个树 只有 左边节点 形成链状结构//拓展 带空节点 的二叉树 明确树 不是 完全的A/ \B null/ \null C//用null 标记缺失的 子节点class TreeNode:def __init__(self, val=0, left=None, right=None):self.val = valself.left = leftself.right = right# 构造一个右倾斜二叉树: 1 → 2 → 3
root = TreeNode(1)
root.right = TreeNode(2)
root.right.right = TreeNode(3)
二叉搜索树(BST)
//二叉搜索树 图示8/ \3 10/ \ \1 6 14//由图可以 看出 左边子节点 都小于 根节点 8
//由图可以 看出 右边子节点 都大于 根节点 8class TreeNode:def __init__(self, val):self.val = val # 节点值self.left = None # 左子节点self.right = None # 右子节点
class BST:def __init__(self):self.root = None # 根节点初始化# ---------- 插入操作 ----------def insert(self, val):self.root = self._insert(self.root, val)def _insert(self, root, val):if not root:return TreeNode(val) # 创建新节点if val < root.val:root.left = self._insert(root.left, val) # 递归插入左子树elif val > root.val:root.right = self._insert(root.right, val) # 递归插入右子树return root # 返回当前节点(避免重复插入相同值)# ---------- 查找操作 ----------def search(self, val):return self._search(self.root, val)def _search(self, root, val):if not root:return Falseif val == root.val:return Trueelif val < root.val:return self._search(root.left, val)else:return self._search(root.right, val)# ---------- 删除操作(最复杂) ----------def delete(self, val):self.root = self._delete(self.root, val)def _delete(self, root, val):if not root:return Noneif val < root.val:root.left = self._delete(root.left, val) # 递归左子树删除elif val > root.val:root.right = self._delete(root.right, val) # 递归右子树删除else:# 找到目标节点,分三种情况处理if not root.left: # 情况1:无左子节点return root.rightelif not root.right: # 情况2:无右子节点return root.leftelse: # 情况3:有左右子节点# 找到右子树的最小节点(或左子树的最大节点)min_node = self._find_min(root.right)root.val = min_node.val # 用最小值覆盖当前节点root.right = self._delete(root.right, min_node.val) # 删除右子树的最小节点return rootdef _find_min(self, root):while root.left:root = root.leftreturn root# ---------- 遍历操作 ----------def inorder(self):"""中序遍历(升序输出)"""self._inorder(self.root)print()def _inorder(self, root):if root:self._inorder(root.left)print(root.val, end=' ')self._inorder(root.right)
插入:
根据BST性质(左小右大)递归找到合适位置插入。
时间复杂度:平均 O(logn)O(logn),最坏 O(n)O(n)(树退化为链表时)。
删除:
无子节点:直接删除。
有一个子节点:用子节点替换自身。
有两个子节点:用右子树的最小值(或左子树的最大值)替换自身,再递归删除那个最小值节点。
查找:
类似二分查找,递归比较节点值。
遍历:
中序遍历(左-根-右)会输出升序序列,这是BST的重要特性。
平衡二叉树
- AVL树:左右子树高度差不超过1 严格平衡,查询效率高
//AVL树通过四种旋转操作维持平衡:
class AVLNode:def __init__(self, val):self.val = valself.left = Noneself.right = Noneself.height = 1 # 节点高度(新增属性)
//平衡因子计算
def _get_height(self, node):if not node:return 0return node.heightdef _get_balance(self, node):"""计算平衡因子:左子树高度 - 右子树高度"""if not node:return 0return self._get_height(node.left) - self._get_height(node.right)//四种旋转操作
def _left_rotate(self, z):"""左旋(处理右右不平衡)"""y = z.rightT2 = y.left# 旋转y.left = zz.right = T2# 更新高度z.height = 1 + max(self._get_height(z.left), self._get_height(z.right))y.height = 1 + max(self._get_height(y.left), self._get_height(y.right))return y # 返回新的根节点def _right_rotate(self, z):"""右旋(处理左左不平衡)"""y = z.leftT3 = y.right# 旋转y.right = zz.left = T3# 更新高度z.height = 1 + max(self._get_height(z.left), self._get_height(z.right))y.height = 1 + max(self._get_height(y.left), self._get_height(y.right))return y//插入操作(含平衡调整)def insert(self, val):self.root = self._insert(self.root, val)def _insert(self, root, val):# 1. 标准BST插入if not root:return AVLNode(val)if val < root.val:root.left = self._insert(root.left, val)elif val > root.val:root.right = self._insert(root.right, val)else:return root # 不允许重复值# 2. 更新高度root.height = 1 + max(self._get_height(root.left), self._get_height(root.right))# 3. 计算平衡因子balance = self._get_balance(root)# 4. 根据不平衡类型旋转# 左左情况if balance > 1 and val < root.left.val:return self._right_rotate(root)# 右右情况if balance < -1 and val > root.right.val:return self._left_rotate(root)# 左右情况if balance > 1 and val > root.left.val:root.left = self._left_rotate(root.left)return self._right_rotate(root)# 右左情况if balance < -1 and val < root.right.val:root.right = self._right_rotate(root.right)return self._left_rotate(root)return root//删除操作(类似插入,需平衡调整)
def delete(self, val):self.root = self._delete(self.root, val)def _delete(self, root, val):# 1. 标准BST删除if not root:return rootif val < root.val:root.left = self._delete(root.left, val)elif val > root.val:root.right = self._delete(root.right, val)else:if not root.left:return root.rightelif not root.right:return root.leftelse:min_node = self._find_min(root.right)root.val = min_node.valroot.right = self._delete(root.right, min_node.val)# 2. 更新高度和平衡(与插入相同)if not root:return rootroot.height = 1 + max(self._get_height(root.left),self._get_height(root.right))balance = self._get_balance(root)# 左左if balance > 1 and self._get_balance(root.left) >= 0:return self._right_rotate(root)# 左右if balance > 1 and self._get_balance(root.left) < 0:root.left = self._left_rotate(root.left)return self._right_rotate(root)# 右右if balance < -1 and self._get_balance(root.right) <= 0:return self._left_rotate(root)# 右左if balance < -1 and self._get_balance(root.right) > 0:root.right = self._right_rotate(root.right)return self._left_rotate(root)return root
关键点总结
旋转操作:左旋和右旋是平衡的核心,需熟练掌握四种不平衡情况的处理。
高度更新:每次插入/删除后需递归更新节点高度。
平衡因子:通过
左子树高度 - 右子树高度
判断是否失衡。红黑树简化:工业级库更多使用红黑树(如Java的
TreeMap
),因其在修改频繁时性能更优。
- 线段树:处理区间查询 区间更新 解决动态区间问题(区间求和 ,最大值,最小值)
核心概念
用途:在 O(logn)O(logn) 时间内完成以下操作:
区间查询(Query):查询数组任意区间
[L, R]
的聚合值(如和、最值)。区间更新(Update):更新数组某个元素或区间。
结构:
每个节点代表数组的一个区间。
根节点代表整个数组
[0, n-1]
。叶子节点代表单个元素。
非叶子节点存储其子节点值的聚合结果(如区间和)。
- 红黑树:通过颜色和规则保持近似平衡 插入/删除效率更高,适合频繁修改
红黑树是一种自平衡的二叉搜索树,它通过对节点着色和旋转操作来保持树的平衡,确保最坏情况下的基本动态集合操作时间为O(log n)。
红黑树必须满足以下5个性质:
-
节点颜色性质:每个节点是红色或黑色
-
根节点性质:根节点是黑色
-
叶子节点性质:所有叶子节点(NIL节点)都是黑色
-
红色节点性质:红色节点的两个子节点都是黑色(即不能有连续的红色节点)
-
黑色高度性质:从任一节点到其每个叶子节点的所有路径都包含相同数目的黑色节点
插入操作
1. 按照普通二叉搜索树的方式插入新节点
2. 将新节点着色为红色
3. 通过旋转和重新着色来修复可能违反的红黑树性质
插入后可能需要进行以下调整:
- 情况1:新节点是根节点 → 变为黑色
- 情况2:父节点是黑色 → 无需调整
- 情况3:父节点和叔节点都是红色 → 重新着色父、叔和祖父节点
- 情况4:父节点红而叔节点黑,且新节点与父节点不在同侧 → 旋转父节点
- 情况5:父节点红而叔节点黑,且新节点与父节点在同侧 → 旋转祖父节点并重新着色
删除操作
1. 按照普通二叉搜索树的方式删除节点
2. 通过旋转和重新着色来修复可能违反的红黑树性质
删除操作比插入更复杂,需要考虑多种情况。
红黑树的优点
1. 保证最坏情况下O(log n)的时间复杂度
2. 相对于AVL树,插入和删除操作需要更少的旋转
3. 适用于频繁插入删除的场景
红黑树的应用
- C++ STL中的map和set
- Java中的TreeMap和TreeSet
- Linux内核中的进程调度
- 文件系统等需要高效查找和插入的场景
红黑树通过相对宽松的平衡条件(不像AVL树那样严格要求左右子树高度差不超过1),在插入和删除时减少了旋转操作,从而提高了性能。
- 伸展树(Splay Tree):通过伸展操作将访问节点移到根 局部性强 局部性原理(最近访问的节点可能再次被访问)下提高查询效率,适合缓存应用
伸展树的基本性质
不需要存储额外的平衡信息(如红黑树的颜色或AVL树的平衡因子)。
摊还时间复杂度(Amortized Time Complexity)为 O(log n),最坏情况下单次操作可能达到 O(n),但多次操作的平均性能良好。
自适应性强:频繁访问的节点会被移动到靠近根的位置,提高后续访问速度。
伸展操作(Splaying)
伸展操作是伸展树的核心,它通过旋转(Zig、Zig-Zag、Zig-Zig)将目标节点移动到根节点。
Zig(单旋转)当 x 是根节点的子节点时使用。类似AVL树的单旋转(左旋或右旋)p x/ \ / \x C → A p/ \ / \ A B B CZig-Zag(双旋转)当 x 是 "左-右" 或 "右-左" 结构时使用(类似AVL树的双旋转)。g x/ \ / \p D / \/ \ → p g A x / \ / \/ \ A B C DB C Zig-Zig(连续同方向旋转) 当 x 和 p 都是左孩子或都是右孩子时使用。g x/ \ / \p D A p/ \ → / \ x C B g
- 可持久化线段树(Persistent Segment Tree) 主席树(Treap)
能够保存所有历史版本,并在这些版本上进行查询操作。它的核心思想是部分复用历史数据,避免每次修改都完全重建整棵树.
可持久化线段树的特点
-
支持版本回溯:可以访问任意修改前的历史版本。
-
空间优化:每次修改只新增部分节点,而不是复制整棵树,空间复杂度为 O(n log n)。
-
查询效率:单次查询时间复杂度仍为 O(log n)。
可持久化线段树的实现原理
(1) 普通线段树的局限性
普通线段树在修改时直接覆盖原数据,无法保留历史版本。如果每次修改都复制整棵树,空间复杂度会达到 O(n²),无法接受。
(2) 可持久化的关键思想
-
路径复制:每次修改时,只复制受影响的节点路径,其余节点复用旧版本。
-
动态开点:不预先分配所有节点,而是在修改时动态创建新节点。
示例:单点更新
假设我们要修改 A[3] 的值:从根节点出发,沿着查询路径向下遍历。复制路径上的所有节点,并更新相关指针。非路径上的节点直接复用旧版本的节点。初始版本 v0:[1,4]/ \[1,2] [3,4]/ \ / \[1,1][2,2][3,3][4,4]修改 A[3] 后生成版本 v1:[1,4] (new)/ \[1,2] (reuse) [3,4] (new)/ \[3,3] (new) [4,4] (reuse)//单点更新
def update(node, l, r, idx, value):if l == r:return new_node(value) # 创建新叶子节点mid = (l + r) // 2if idx <= mid:# 左子树受影响,复制左孩子,右孩子复用new_left = update(node.left, l, mid, idx, value)return new_node(left=new_left, right=node.right)else:# 右子树受影响,复制右孩子,左孩子复用new_right = update(node.right, mid+1, r, idx, value)return new_node(left=node.left, right=new_right)//区间查询
def query(node, l, r, ql, qr):if qr < l or ql > r:return 0 # 无交集if ql <= l and r <= qr:return node.value # 完全包含mid = (l + r) // 2return query(node.left, l, mid, ql, qr) + query(node.right, mid+1, r, ql, qr)
适用场景:离线查询、历史版本分析、数据流统计等。
霍夫曼树:霍夫曼树让数据压缩更高效!
又称最优二叉树,是一种带权路径长度最短的二叉树,广泛应用于数据压缩(如ZIP、JPEG、MP3等编码)。
1. 基本概念
(1) 带权路径长度(WPL)
-
路径长度:从根节点到某个节点的边数。
-
节点的带权路径长度:
节点权值 × 路径长度
。 -
树的带权路径长度(WPL):所有叶子节点的带权路径长度之和。
霍夫曼树的目标:构造一棵WPL最小的二叉树。
示例
假设有字符集 {A, B, C, D},出现频率(权值)为 {5, 2, 4, 3},则两种可能的二叉树如下:普通二叉树(WPL = 5×1 + 2×2 + 4×3 + 3×3 = 28)*/ \A */ \* D/ \B C
霍夫曼树(WPL = 5×1 + 4×2 + 2×3 + 3×3 = 22)*/ \* A/ \* C/ \
B D
霍夫曼树示例2
对 {A:5, B:2, C:4, D:3} 构建霍夫曼树:初始森林:[B:2, D:3, C:4, A:5]合并 B:2 和 D:3 → [ (B+D):5, C:4, A:5 ]合并 C:4 和 (B+D):5 → [ (C+B+D):9, A:5 ]合并 A:5 和 (C+B+D):9 → [ (A+C+B+D):14 ](14)/ \A:5 (9)/ \C:4 (5)/ \B:2 D:3编码规则
左分支 = 0,右分支 = 1从根到叶子的路径即为字符的编码霍夫曼树的应用
(1) 数据压缩
ZIP、GZIP:使用霍夫曼编码减少文件大小。JPEG、MP3:结合DCT/FFT变换后,用霍夫曼编码进一步压缩。(2) 网络传输
HTTP/2 HPACK:压缩HTTP头部字段。(3) 加密与编码
哈夫曼加密:动态调整编码表增强安全性。
堆 (Heap):基与完全二叉树的特殊数据结构 堆是高效管理最值问题的利器!
堆通常用于实现优先队列(Priority Queue),支持高效地获取或删除最大值/最小值。
-
堆序性:每个节点的值必须满足某种特定顺序(如最大堆或最小堆)。
-
完全二叉树结构:除了最后一层,其他层必须填满,且最后一层节点靠左排列。
最大堆(Max Heap): 父节点的值 ≥ 子节点的值 根节点为最大值
100/ \70 80
/ \ / \
50 40 20 30
最小堆(Min Heap)父节点的值 ≤ 子节点的值 根节点为最小值
10/ \20 30
/ \ / \
50 40 80 100
堆的存储方式
由于堆是完全二叉树,通常用数组存储(节省指针空间):
-
对于索引
i
的节点:-
父节点:
(i - 1) // 2
-
左孩子:
2i + 1
-
右孩子:
2i + 2
-
示例最大堆数组:[100, 70, 80, 50, 40, 20, 30]
对应树结构:100 (0)/ \70 (1) 80 (2)/ \ / \
50(3) 40(4) 20(5) 30(6)
堆的核心操作
插入元素(Heapify Up)
将新元素添加到数组末尾。向上调整(与父节点比较,交换直到满足堆性质)。
时间复杂度:O(log n)//向最大堆插入 90
插入前:100/ \70 80/ \ / \50 40 20 30插入 90 后:100/ \90 80/ \ / \70 40 20 30/
50删除堆顶元素(Heapify Down)
交换堆顶与最后一个元素,并删除最后一个元素。向下调整(与较大的子节点交换,直到满足堆性质)。
时间复杂度:O(log n)//删除最大堆的堆顶 100
删除前:100/ \90 80/ \ / \70 40 20 30删除后:90/ \70 80/ \ / 50 40 20 构建堆(Heapify)
从最后一个非叶子节点开始,向前做 Heapify Down时间复杂度:O(n)(看似 O(n log n),但实际更低)//将 [10, 20, 30, 50, 40, 80, 100] 构建为最小堆)
初始:10/ \20 30/ \ / \50 40 80 100调整后:10/ \20 30/ \ / \50 40 80 100
(已经是合法最小堆)堆的应用
(1) 优先队列(Priority Queue)
获取最值:O(1)(直接取堆顶)插入/删除:O(log n)(2) 堆排序(Heap Sort)
构建最大堆。反复取出堆顶(最大值),并调整堆。
时间复杂度:O(n log n)(3) Top K 问题
求最大 K 个元素:维护一个最小堆(堆顶是最小的候选者)。求最小 K 个元素:维护一个最大堆(堆顶是最大的候选者)。(4) Dijkstra 最短路径算法
用最小堆优化查找当前最短路径节点。
非二叉树:每个节点可以有任意数量的子节点
2. B树家族
(1) B树(B-Tree)
(2) B+树
(3) B*树
3. Trie树(前缀树)
二、空间划分树结构
1. 四叉树(Quadtree)
2. 八叉树(Octree)
3. k-d树(k-dimensional Tree)
三、高级应用结构
1. 后缀树(Suffix Tree)
2. 决策树(Decision Tree)
3. 语法分析树(Parse Tree)
-
1. 一般树(General Tree)
-
特点:任意节点可以有0-n个子节点
-
应用:文件系统目录结构、组织结构图
-
存储方式:
-
左孩子右兄弟表示法(转换为二叉树)
-
动态指针数组存储子节点
-
-
特点:
-
每个节点包含多个键和多个子节点
-
保持半满状态,保证平衡
-
所有叶子节点在同一层
-
-
典型应用:数据库索引、文件系统
-
改进点:
-
非叶子节点只存键不存数据
-
叶子节点用指针连接形成链表
-
-
优势:更适合磁盘存储,范围查询效率高
-
进一步优化空间利用率
-
要求节点至少2/3满
-
特点:
-
每个节点代表一个字符
-
从根到叶子的路径构成完整字符串
-
-
变种:
-
压缩Trie(合并单分支路径)
-
三向Trie(Ternary Search Trie)
-
-
应用:自动补全、拼写检查、IP路由
-
维度:2D空间
-
分割方式:每个节点分成4个象限
-
应用:图像处理、地理信息系统、碰撞检测
-
维度:3D空间
-
分割方式:每个节点分成8个立方体
-
应用:3D图形渲染、医学成像、点云处理
-
特点:
-
交替按不同维度划分
-
二叉树结构但处理多维数据
-
-
应用:最近邻搜索、范围查询
-
构建:一个字符串的所有后缀构成的压缩Trie
-
特点:线性空间构造
-
应用:DNA序列匹配、文本搜索
-
特点:
-
非终端节点表示判断条件
-
叶子节点表示决策结果
-
-
变种:ID3、C4.5、CART算法
-
应用:编译器语法分析
-
类型:
-
具体语法树(CST)
-
抽象语法树(AST)
-