1 树的特点与性质
树(Tree)是一种特殊的图,满足以下条件:
连通无环:任意两个顶点之间有且只有一条路径相连,且没有闭环。
边数最少:若树有 n个顶点,则恰好有 n−1条边。
删边会不连通,加边会成环:这是树的脆弱性和极简性的体现。
类比:树像一棵倒挂的家族树,从根向下分叉,没有“近亲结婚”(无环)。
树是图的子集:所有树都是图,但并非所有图都是树。
图的生成树:如果一个连通图有环,可以通过删除某些边得到一棵覆盖所有顶点的树(称为生成树)。
示例:假设一个图表示城市间的所有可能道路(含环路),其生成树就是保留部分道路,使所有城市连通且无冗余路径。
叶子节点:度为1的顶点(即只有一条边连接的顶点)。
根树:指定一个顶点为根,可以定义父子关系和层级(如二叉树)。
最小连通性:树是保证连通的前提下,边数最少的图。
2 加权无向图的最小生成树
3 二叉树与k叉树
3.1 定义
二叉树:每个节点最多有2个子节点(左子节点和右子节点)
根节点(Root):树的顶端节点
叶子节点(Leaf):没有子节点的节点
深度(Depth):从根节点到该节点的最长路径长度
高度(Height):从该节点到最远叶子节点的最长路径长度
k叉树:每个节点最多有 k个子节点
节点数计算:若树高为 h,则最多有 k^h−1/k−1 个节点(等比数列求和)
3.2 性质
第 i 层最多有 2^i−1个节点。
深度为 h 的二叉树最多有 2^h−1个节点(满二叉树)。
对于任何非空二叉树,叶子节点数 = 度为2的节点数 + 1
3.3 二叉树的分类
3.3.1 满二叉树(Full Binary Tree)
每个节点都有0或2个子节点
A
/ \
B C
/ \
D E
3.3.2 完全二叉树(Complete Binary Tree)
除最后一层外,其他层节点都填满,且最后一层节点靠左排列
A
/ \
B C
/ \
D E
3.3.3 二叉搜索树(Binary Search Tree, BST)
对于每个节点:
- 左子树所有节点的值 < 当前节点的值
- 右子树所有节点的值 > 当前节点的值
4
/ \
2 6
/ \ / \
1 3 5 7
3.3.4 平衡二叉树(AVL Tree)与二分法查询
节点的左右子树高度差不超过1
3
/ \
2 4
/ \
1 5
二分法查询核心特点
1.时间复杂度:始终为O(log n),得益于树的平衡性
2.比较规则:
-
目标值 == 当前节点:返回节点
-
目标值 < 当前节点:搜索左子树
-
目标值 > 当前节点:搜索右子树
3.平衡性保障:为了保持BST的高效性,需要平衡树——通过旋转操作让树的高度尽可能低,确保查找、插入、删除的时间复杂度稳定在O(log n)
-
AVL树:通过旋转保持左右子树高度差≤1,通过旋转(左旋、右旋、双旋)调整树结构
-
红黑树:通过颜色规则保持最长路径≤2*最短路径
a.每个节点是红色或黑色
b.根节点是黑色
c.叶子节点(NIL空节点)是黑色
d.红色节点的子节点必须是黑色(即不能有连续红节点)
e.从任意节点到其叶子节点的路径上,黑色节点数相同(称为“黑高”)
特性 |
AVL树 |
红黑树 |
---|---|---|
平衡标准 |
绝对平衡(高度差≤1) |
近似平衡(黑高相同) |
插入/删除 |
频繁旋转,效率较低 |
旋转少,效率较高 |
查询效率 |
更高(严格平衡) |
稍低 |
适用场景 |
静态数据(如数据库索引) |
动态数据(如Map、STL) |
def search(root, target):
"""二分法查询核心逻辑"""
if root is None or root.key == target:
return root
# 目标值小于当前节点,搜索左子树
if target < root.key:
return search(root.left, target)
# 目标值大于当前节点,搜索右子树
else:
return search(root.right, target)
3.4 二叉树的遍历
A
/ \
B C
/ \ \
D E F
3.4.1 深度优先遍历(DFS)
前序遍历(根-左-右)
def preorder(root):
if root:
print(root.val)
preorder(root.left)
preorder(root.right)
//输出:A B D E C F
中序遍历(左-根-右)
def postorder(root):
if root:
postorder(root.left)
postorder(root.right)
print(root.val)
//输出:D B E A F C
后序遍历(左-右-根)
def postorder(root):
if root:
postorder(root.left)
postorder(root.right)
print(root.val)
//输出:D E B F C A
3.4.2 广度优先遍历(BFS)
from collections import deque
def bfs(root):
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)
//输出:A B C D E F
3.5 应用举例
3.5.1 表达式求值(语法树)
*
/ \
+ /
/ \ / \
3 4 5 2
表示:(3+4)*(5/2)
3.5.2 文件系统目录结构
/
/ \
bin home
/ \
user doc
3.5.3 简单决策系统(决策树 / 森林、机器学习分类问题、集成学习)
天气?
/ \
晴朗 阴天
/ \ / \
游泳 爬山 看电影
分类/拟合,
多棵树,每棵树给系数->森林,投票法、系数法,
根据先验知识,依赖专家系统,
4 哈夫曼编码
哈夫曼编码(Huffman Coding)是一种基于字符出现频率的变长编码方法,核心思想是用更短的二进制码表示高频出现的字符,从而减少总存储空间。由David A. Huffman于1952年提出。它是数据压缩领域最重要的算法之一,广泛应用于ZIP、JPEG、MP3等文件格式中。
4.1 核心思想
-
变长编码:高频字符用短编码,低频字符用长编码
-
前缀码:任何字符的编码都不是其他字符编码的前缀(避免解码歧义)
-
最优编码:对于给定的字符频率分布,哈夫曼编码能产生最短的平均编码长度
4.2 算法特性
-
贪心算法:每次合并频率最小的两个节点
-
时间复杂度:O(n log n)(使用优先队列实现)
-
空间复杂度:O(n)
4.3 构建步骤
-
统计字符频率,创建叶子节点
-
将节点按频率放入优先队列(最小堆)
-
循环取出两个最小频率的节点,合并为新节点(频率=子节点频率和)
-
将新节点放回队列
-
重复直到只剩一个节点,形成哈夫曼树
4.4 示例
import heapq
from collections import defaultdict
def build_huffman_tree(freq):
heap = [[weight, [char, ""]] for char, weight in freq.items()]
heapq.heapify(heap)
while len(heap) > 1:
lo = heapq.heappop(heap)
hi = heapq.heappop(heap)
for pair in lo[1:]:
pair[1] = '0' + pair[1]
for pair in hi[1:]:
pair[1] = '1' + pair[1]
heapq.heappush(heap, [lo[0] + hi[0]] + lo[1:] + hi[1:])
return sorted(heap[0][1:], key=lambda p: (len(p[1]), p))
# 示例1:简单字母频率
print("示例1:字母频率 {'A': 50, 'B': 30, 'C': 20}")
freq1 = {'A': 50, 'B': 30, 'C': 20}
huffman1 = build_huffman_tree(freq1)
for char, code in huffman1:
print(f"'{char}': {code}")
# 示例2:英文句子
print("\n示例2:句子 'this is an example of huffman encoding'")
text = "this is an example of huffman encoding"
freq2 = defaultdict(int)
for char in text:
freq2[char] += 1
huffman2 = build_huffman_tree(freq2)
for char, code in huffman2:
print(f"'{char}': {code}")
示例1:字母频率 {'A': 50, 'B': 30, 'C': 20}
'A': 0
'B': 11
'C': 10
示例2:句子 'this is an example of huffman encoding'
' ': 101
'n': 010
'a': 1100
'e': 1101
'f': 1110
'h': 0010
'i': 1111
'm': 0011
'o': 0110
's': 1000
'c': 00000
'd': 00001
'g': 00010
'l': 00011
'p': 01110
't': 01111
'u': 10010
'x': 10011
5 哈希算法
哈希(Hash)是一种将任意长度的输入数据转换为固定长度输出的算法,这种输出称为哈希值或散列值。
5.1 哈希的核心特性
-
确定性:相同输入永远产生相同输出(例:
"hello"
→ 总是生成2cf24dba5...
(SHA-256) -
快速计算:O(1)时间复杂度(理想情况下)
-
抗碰撞性:难以找到两个不同输入产生相同输出
-
不可逆性:无法从哈希值还原原始数据
5.2 常见哈希算法对比
算法 | 输出长度 | 特点 | 典型应用 |
---|---|---|---|
MD5 | 128位 | 已不安全,易碰撞 | 文件校验 |
SHA-1 | 160位 | 逐渐被淘汰 | Git版本控制 |
SHA-256 | 256位 | 当前推荐的安全哈希 | 区块链、密码存储 |
CRC32 | 32位 | 快速但仅用于错误检测 | 网络数据包校验 |
5.3 哈希冲突解决方案
1、链地址法(Separate Chaining):
冲突元素存储在链表中
实现简单但需要额外内存
2、开放寻址法(Open Addressing):
线性探测:h(k,i) = (h'(k) + i) mod m
平方探测:h(k,i) = (h'(k) + i²) mod m
双重哈希:h(k,i) = (h₁(k) + i*h₂(k)) mod m
3、布谷鸟哈希(Cuckoo Hashing):
使用两个哈希函数
冲突时踢出原有元素并重新插入
5.4 应用
密码保护
网站存储的是哈希值而非明文密码
加盐(随机数据)防止彩虹表攻击
文件校验
下载文件后对比哈希值验证是否被篡改
比如Linux系统ISO文件的SHA256校验
区块链基础
每个区块包含前一个区块的哈希值
形成不可篡改的链条
数据去重
云存储用哈希识别重复文件
比如Dropbox节省存储空间的原理
快速查找
哈希表能在O(1)时间复杂度找到数据
比遍历查找快数百倍
6 剪枝
剪枝(Pruning)是优化树形结构的关键技术,主要用于消除冗余分支。 通过提前终止对“无效分支”的搜索或计算,减少不必要的资源消耗。
6.1 剪枝的本质
就像修剪果树多余的枝条,剪枝通过移除树结构中不必要的部分来:
-
降低复杂度(减少计算量)
-
防止过拟合(提升泛化能力)
-
加速查询(缩短搜索路径)
6.2 剪枝的两种类型
6.2.1 决策树剪枝
场景:机器学习中防止模型过拟合(记住训练数据但泛化能力差,模型在训练集上表现极好,但在新数据上表现很差。)
方法:
-
预剪枝:生长时提前停止分裂
(如:达到最大深度/节点样本数少于阈值) -
后剪枝:完全生长后删除冗余分支
(如:计算剪枝前后验证集准确率变化)
6.2.2 搜索剪枝
场景:算法优化(如Alpha-Beta剪枝)
原理:
当发现某分支不可能优于已知解时,立即停止搜索该分支
6.3 剪枝核心
-
评估当前路径:计算已探索部分的评估值
-
对比已知最优:与当前全局最优解比较
-
决策剪枝:
- 最大化问题:当前路径≤已知最大值 → 剪枝
- 最小化问题:当前路径≥已知最小值 → 剪枝