(2020.02.07, 2022.05.04补细节)
树结构在数据库设计中扮演重要作用。
基本概念:
树叶和分支节点:没有子节点的节点称为树叶,其余都是分支节点。
(节点)度数degree: 一个节点的子节点个数。
层: 根的层数为0,位于k层的节点,其子节点是k+1层的元素。
高度(or深度): 树中节点的最大层数,只有根节点的树,其高度为0.
高度是从树叶往上数到目标节点。深度是从根节点往下数到目标节点。
满二叉树:二叉树中所有分支节点的度数为2。
满二叉树性质:树叶比分支节点多一个。
证明:从一个非空满二叉树,每次取走一个叶节点和它的母节点,并将剩余的节点相连形成新树,并对新树执行相似的过程。最终只剩下一个叶节点。证明树叶比分支节点多一个。
扩充二叉树:对已有二叉树T加入足够多的新叶节点,使T的原有节点都变为度数为2的节点,形成的新二叉树. 分为内部节点和外部节点。空树的扩充二叉树规定为空树。
扩充二叉树都是满二叉树(?)
完全二叉树:共h层,0- (h-1)层的节点数是.如果最下一层的节点不满,则所有节点在左边连续排列,空位都在右边。此为完全二叉树。(除最下两层外,其余都是度数为2的节点。节点数为n的完全二叉树,其高度是不大于log2(n)的最大整数) 完全二叉树可自然的存入一个连续表。
二叉树的list实现:
atree = [root, lleaf, rleaf] 其中的lleaf和rleaf都可以是一个子树
用二叉树实现表达式求值。
二叉树的简单实现
(2020.12.21 Mon)
# -*- coding: utf-8 -*-
class btree:
def __init__(self, value):
self.left = None
self.right = None
self.data = value
def insert_left(self, value):
self.left = btree(value) //插入的也是一个树
return self.left
def insert_right(self, value):
self.right = btree(value)
return self.right
def show(self):
print(self.data)
if __name__ == '__main__':
root = btree('root')
a = root.insert_left('a')
c = a.insert_left('c')
d = a.insert_right('d')
f = d.insert_left('f')
root.show()
遍历(traverse): 深度优先、宽度优先
深度优先(Depth First Search, DFS):根节点P左子节点L右子节点R,先根遍历顺序PLR,后根遍历顺序LRP,中序遍历LPR。
宽度优先(Breadth First Search, BFS):按路径长度由近及远的访问节点,亦即按层次访问树的各层节点。用队列(Queue)实现宽度遍历。
(2020.12.21 Mon)
树的深度遍历(DFS)方法有三种,先序遍历、中序遍历和后序遍历。
一个记忆技巧:先、中、后序遍历,是针对根节点在遍历中位置来定义的,也就如果顺序是根节点-左-右,则称为先序遍历,以此类推。
- 先序遍历: 如果二叉树不为空,则访问根节点,然后访问左子树,最后访问右子树;否则程序退出
- 中序遍历: 如果二叉树不为空,则访问左子树,然后访问根节点,最后访问右子树;否则程序退出
- 后序遍历: 如果二叉树不为空,则访问左子树,然后访问右子树,最后访问根节点;否则程序退出
递归形式的遍历代码
# node = btree(5)
def preorder(node):
if node.data:
node.show()
if node.left:
preorder(node.left)
if node.right:
preorder(node.right)
def inorder(node):
if node.data:
if node.left:
inorder(node.left)
node.show()
if node.right:
inorder(node.right)
def postorder(node):
if node.left:
postorder(node.left)
if node.right:
postorder(node.right)
if node.data:
node.show()
if __name__ == '__main__':
root = btree('root')
# some operations, insert trees
...
inorder(root)
preorder(root)
postorder(root)
树的BFS遍历
(2020.12.22 Tues)
也就是按层遍历,需要用到queue作为工具。
class queue:
default_capacity = 30
def __init__(self):
self.data = [None] * queue.default_capacity
self.size = 0
self.front = 0 # front index
def enqueue(self, value):
if self.size == len(self.data):
self.resize(2*len(self.data))
tail = (self.front + self.size)% len(self.data)
self.data[tail] = value
self.size += 1
def dequeue(self):
if self.size == 0:
raise Empty('queue empty.')
ans = self.data[self.front]
self.data[self.front] = None
self.front = (self.front+1) % len(self.data)
self.size -= 1
return ans
def resize(self, newlen):
obs = deepcopy(self.data)
obsind = self.front
self.data = [None] * newlen
for k in range(self.size):
self.data[k] = obs[obsind]
obsind = (obsind + 1)%len(obs)
self.front = 0
def is_empty(self):
return self.size == 0
#2020.12.24
if __name__ == '__main__':
root = btree('root')
# some insertion operation
...
q = queue()
q.enqueue(root)
result = []
while q.is_empty():
tmp = q.dequeue()
result.append(tmp.data)
if tmp.left:
q.enqueue(tmp.left)
if tmp.right:
q.enqueue(tmp.right)
# result is what we want
堆(heap)及性质
代码参考《树-堆heap, since 2022-05-03》
堆: 用树形结构实现的优先队列,完全二叉树。
任意节点保存的数据,在优先级上优于或等于其子节点的数据。从根到任意叶的路径,优先级(非严格)递减。堆中最优先数据在根节点,检索复杂度O(1)。
插入元素:向堆(完全二叉树)的结尾加入一个元素,此时二叉树不是堆。新加入元素和父节点做对比,如果新的优先级高就与父节点交换,并重复该过程;否则结束插入元素的过程。复杂度O(log2(n)).该过程称为向上筛选。
弹出元素:弹出优先级最高的元素,即堆顶的元素。弹出后堆顶空缺,从堆的尾部取出最后一个元素放在堆顶,并比较堆顶和两个子节点,将优先级最高的放在顶点,此时原来处在堆尾的点就下移,重复这个过程直到堆形成。称为向下筛选。复杂度O(log2(n)).
堆的应用:数组排序。
*非递归的遍历方法(没看)
*用生成器函数遍历(没看)
(2020.02.09)
Huffman tree
路径长途:根节点到叶节点的路径长度之和,i.e., 沿途节点个数(含根节点,不含终点节点)
用E = {[图片上传失败...(image-8474c1-1608347722558)]
}表示。
W = {[图片上传失败...(image-283282-1608347722558)]
} 是n个叶节点的值(or权重).
带权扩充外部二叉树的外部路径长度WPL = [图片上传失败...(image-730933-1608347722558)]
当扩充二叉树T以W为权,而T的WPL在所有这样的扩充二叉树中达到最小,T被称为最优二叉树/Huffman二叉树。
构造Huffman二叉树:
1 从W中找出最小的两个[图片上传失败...(image-4fb6f7-1608347722558)]
形成一棵二叉树,叶节点是两个[图片上传失败...(image-8d8d87-1608347722558)],根节点是他们的权重之和。
2 用1生成的根节点权重代替生成这个根节点的两个叶节点放回W
3 重复1/2,直到生成一棵树。
实现:优先队列。
算法分析:略。
Huffman编码:略
树(二叉树的扩展)
分为有序树和无序树。两棵树按无序树观点来看相同,但按照有序树的观点看可能不同。
树的结点度数是连接子树的个数。一棵树的度数定义为该树中度数最大的结点的度数。
树的性质:
1 度数为k的树,第i层至多有[图片上传失败...(image-35b6c3-1608347722558)]个结点
2 度数为k,高度为h的树,至多有...个结点(计算方法: 等比数列求和)
3 n个结点的树里n-1条边。(除了根节点,每个点都有一条边连接父节点,并且这些边不重合.)
4 n个结点的k度完全树,高度h是不大于[图片上传失败...(image-e14a9-1608347722558)]的最大整数。
二叉排序树:
一种结点存储数据的二叉树,有如下性质:
1 根节点保存一个数据项和其关键码(value & key)
2 如果有左子树,则左子树保存的所有关键码,都小于(或不大于)根节点的关键码;
3 如果有右子树,则右子树保存的所有关键码,都大于根节点的关键码;
4 非空左子树/右子树也是二叉排序树;
5 中序排列这棵二叉树得到的关键码序列是递增序列。
检索:从上到下依次比较结点的关键码,如果比结点关键码大,则向右,小则向左。
插入:(假设与树中key重合,则新值代替旧值)从根节点开始,与结点的key比较,当应该向左走而左子树为空时,则添加成左子树;当应该向右走而右子树为空时,则添加为右子树;当与结点key重合,则更新结点的key-value。
删除:设删除的点是c,其父节点为p,设c是p的左子树(右子树情况以此类推)
1 如果c没有结点,则直接删除c,p的左子树更新为None;
2 如果c没有左子树,则删除c,c的右子树作为p的左子树;
3 如果c有左子树(有无右子树操作相同),则删除c,找出c的左子树中的最右结点r(从c开始往右下走,第一个没有右子树的结点?)。用c的左子结点代替c称为p的左子结点,c的右子树作为r的右子树.给出我的思路: c的最右结点r取代c,原r的位置空出,对r的左子树(如果有),从步骤1开始迭代执行。
复杂度分析:
2020.02.10
如果树形非畸形,即链条从上到下的没有分支,则检索的复杂度是O(n);而如果树形良好,即高度与结点数目成对数关系,则检索的复杂度是[图片上传失败...(image-9d404b-1608347722558)].
数据更新、插入和删除的操作都是在检索的基础上做常数时间操作,复杂度与检索相同。
最佳二叉排序树:
最佳的标准应该基于检索效率。平均检索长度的概念。
设计最佳二叉排序树依据:检索关键码(key)的频度(次数,也称分布?).
假设二叉树排序树由n个内部节点和n+1个叶节点构成,其中不管内部节点构成的二叉树为何形状,加入外部节点扩展后,形成扩充二叉树。
key的平均检索长度[图片上传失败...(image-38fe41-1608347722558)]
有
E(n) = \frac{1}{w} [\sum_{0}^{n-1} p_{i}(l_{i}+1) +\sum_{0}^{n} q_{i} l_{i}^{'} ]
其中1/w = sigma p_i + sigma q_i
l_i是内部节点i的层数,l_{i}^{'}是外部节点
最优二叉排序树是使平均检索次数E(n)达到最少的二叉排序树。
最简单情况:所有key的检索频度相等。公式略。
结论是最低的树最好。
构造最简单情况的最优二叉排序树:
设a按关键码排序的字典,
1 low = 0, high = len(a) - 1
2 m = (low + high) /2
3 a[m]存入二叉树的根节点;片段a[low, m-1] 作为a[m]的左子树,a[m+1, high]作为a[m]的右子树;片段为空,则None,表示空树。循环此过程得到最优二叉排序树。由排序字典构成排序树的复杂度O(n),考虑到生成排序字典的复杂度最低是O(nlogn),则由任意一组关键码出发,构建最优二叉树的复杂度是O(nlogn).
一般情况的最优二叉树:用动态规划寻找。
平衡二叉(排序)树 a.k.a. AVL树:
最优二叉树的缺点:需要提前知道key的分布并且字典是静态,任何加入或删除的操作都会破坏树的结构。
平衡二叉树亦称AVL树,与其结构类似的有红黑树和B树。
定义:特殊的二叉排序树,或空树,或左右子树都是平衡二叉排序树,其左右子树的高度最大差距是1。(我的理解:平衡二叉排序树中的任何子树,其根节点的左右子树高度差都不超过1.)
BF平衡因子(balance factor): 对结点定义,该结点的左子树高度与右子树高度差,左高-右高。对平衡二叉树来说,BF=-1,0,1.即决定值不超过1. (每个结点如果能同时记录该点的最高高度和BF就非常好了.)
平衡二叉树的树高h与结点数n的关系:推导
2020.02.12
用[图片上传失败...(image-492b77-1608347722558)]代表高度为[图片上传失败...(image-c69d5f-1608347722558)]的'临界'平衡二叉树,也就是相同高度[图片上传失败...(image-7fc40d-1608347722558)]的平衡二叉树中节点最少的。容易得到[图片上传失败...(image-a2982-1608347722558)] = 1, [图片上传失败...(image-fe1076-1608347722558)]
= 2,从高度为2的临界平衡二叉树开始,都可以当做是一个根节点与高度为[图片上传失败...(image-6e32dc-1608347722558)]和[图片上传失败...(image-9c718f-1608347722558)]的两棵临界平衡二叉树的组合。有下面推导式
[图片上传失败...(image-bfbfa6-1608347722558)]
[图片上传失败...(image-7e334c-1608347722558)]
- [图片上传失败...(image-dc0250-1608347722558)]
- 1
其中的1代表一个根节点。
Fibonacci数列 [图片上传失败...(image-ffedb4-1608347722558)]
由数学归纳法可证(?)
[图片上传失败...(image-1a665e-1608347722558)]
且[图片上传失败...(image-d28842-1608347722558)]
树高[图片上传失败...(image-c9bbc6-1608347722558)]
.
结论,与最优二叉树相比,最长路径的长度仅差一个常量,检索复杂度[图片上传失败...(image-6ea7a1-1608347722558)].
对平衡二叉树的设计核心,是控制不同子树的高度差,保证高度和所存项数的对数关系,保证检索效率。
对平衡二叉树进行插入操作的再平衡
最小非平衡子树(minimum unbalanced subtree, MUS):包含新插入结点位置的、根结点的BF非0的最小子树。
如果在MUS中插入新结点后,MUS扔保持平衡,并且高度不变,则整棵树的高度也不变。
插入新结点后,结构调整能够在子树内的一条路径上完成,插入的复杂度O(logn).
插入新结点如果MUS失去平衡的调整和恢复的情况分以下4种:
LL: MUS的左子树较高,新结点插到左子树的左子树
LR: MUS的左子树较高,新结点插到左子树的右子树
RL: MUS的左子树较高,新结点插到右子树的左子树
RR: MUS的左子树较高,新结点插到右子树的右子树
其中LL、RR的处理方式相似,LR、RL的处理方式相似。
LL的处理方式:
图中a是MUS的根结点,b的左右子树等高。
在没有插入新结点之前,各结点和子树的排列顺序是AbBaC。新结点插入子树A,形成A'。此时顺时针调整MUS,b取代a成为根节点,b的右子树成为a的左子树,a成为b的右子树。调整后的子树和结点排序时A'bBaC。与调整前保持一致。(2020.02.18补充我的理解:在树的下面有一个平面,这个树在平面上的project的顺序就是树中节点的排列顺序。调整二叉树失衡之后得到的新树,只要其project顺序与原project顺序相同即可。)
a.left = b.right
b.right = a
a.bf = b.bf = 0
# RR的情况:
a.right = b.left
b.left = a
a.bf = b.bf = 0
LR的调整:
插入新结点前的排序时AbBcCaD。新结点插入到B或C的结点之下,从而导致c为根节点的子树失衡。
失衡之后的状态如图(2)所示。此时调整为(3)所示的关系,插入后的排序时AbB'(B)cC(C')aD,顺序没有变化,高度没有变化,新形成的子树仍然是平衡二叉树。
b = c.left
b.right = c.left
a.left = c.right
c.right = a
c.left = b
之后调整各结点bf(略略略).
AVL平衡二叉树的插入操作复杂度O(logn).
动态多分支排序树:
B/B+树: placeholder....
(2020.02.19)
红黑树
一种平衡二叉树,满足5个性质
- 每个节点是红色或者黑色;
- 根节点是黑色;
- 每个叶子节点(nill)是黑色(注意这里即便叶节点为空,也要标注在图中);
- 每个红色结点的叶子节点一定是黑色;
- 任意一个结点都叶子结点一定包含相同数量的黑色结点。
插入和删除结点后,如果平衡二叉树失衡,则需要通过变色和旋转调整树。
左旋转
以某点P为支点旋转,其右子节点V旋转后称为P的父节点,V的左子节点称为P的右子节点,V的右子节点不变,V的新左子节点是P,如下图[2]
右旋转
以某点P为支点旋转,其左子节点F旋转后称为P的父节点,F的右子节点成为P的左子节点,F的左子节点不变,F的新右子节点是P,如下图[2]
旋转之后根据5个性质对变化后的节点变色。
reference:
1 裘宗燕,数据结构与算法 Python语言描述.
2 https://www.jianshu.com/p/e136ec79235c
3 http://www.360doc.com/content/18/0904/19/25944647_783893127.shtml