树
- 视频链接:https://www.bilibili.com/video/BV1VC4y1x7uv?
- 章节:P56-P73
树是一种基本的非线性数据结构
- 示例:分类树:界、门、纲、目、科、属、种
特征:
1、层次化:数是一种分层结构,越接近顶部的层越普遍,越接近底部的层次越独特
2、节点之间相互隔离、独立
3、每一个叶节点都具有唯一性,从根开始到达每个种的完全路径来唯一标识每个物种
- 其他示例:计算机文件系统、HTML文档(嵌套标记)、域名体系
树结构相关术语
- 节点Node
组成树的基本部分,每个节点具有名称,或“键值”,节点还可以保存额外数据项,数据项根据不同的应用而变。
- 边Edge
边是组成树的另一个基本部分,每条边恰好连接两个节点,表示节点之间具有关联,边具有出入方向;
每个节点(除根节点)恰有一条来自另一节点的入边,每个节点可以有多条连接到其他节点的出边。
- 根root
树中唯一没有入边的节点
- 路径path
由边依次连接在一起的节点的有序列表
如:HTML->BODY->UL->LI,是一条路径
- 子节点Children
入边均来自于同一个节点的若干节点,称为这个节点的子节点
- 父节点Parent
一个节点是其所有的出边所连接节点的父节点
- 兄弟节点Sibling
具有同一个父节点的节点之间称为兄弟节点
- 子树Subtree
一个节点和其所有子孙节点,以及相关边的集合
- 叶节点Leaf
没有子节点的节点称为叶节点
- 层级Level
从根节点开始到达一个节点的路径,所包含的边的数量,称为这个节点的层级
如图:D的层级为2,根节点的层级为0
- 高度
树中所有节点的最大层级称为树的高度
如图:右图树的高度为2
另一种表示法就是2+1=3 层
树的定义
- 定义1
树是由若干节点,以及两两连接节点的边组成,并有如下性质:
- 其中一个节点被设定为根;
- 每个节点n(除根节点),都恰连接一条来自节点p的边,p是n的父节点;
- 每个节点从根开始的路径是唯一的
- 如果每个节点最多有两个子节点,这样的数称为“二叉树”
- 定义2(递归定义)
树是空集或者由根节点及0或多个子树构成(其中子树叶也是树),每个子树的根到根节点具有边相连。
python实现树
基本操作:
常用方法:
-
binary_tree
创建仅有根节点的二叉树
-
insert_left/insert_right
将新节点插入树中作为其直接的左右子节点
-
get_root_val/set_root_val
取得或返回根节点
-
get_left_child/get_right_child
返回左右子树
实现方式:
嵌套列表
[root,left,right]
链表形式
def __init__(self, val):
self.key = val
self.left = None
self.right = None
def insert_left(self, val):
node = BinaryTree(val)
if not self.left:
self.left = node
else:
node.left = self.left
self.left = node
树结构应用-表达式解析
树的遍历
- 前序遍历
遍历顺序:根节点、左节点、右节点
例子:一本书的章节阅读
封面->第一章->第一章的第一小节->第一章的第二小节->第一章的第二小节的第一子小节->第一章的第二小节的第二子小节->第二章->第二章的第一小节->第二章的第二小节->第二章的第二小节的第一子小节->第二章的第二小节的第二子小节
def preorder(bt):
"""
前序遍历
:return:
"""
if bt is None:
return
print(bt.get_root_val())
preorder(bt.get_left_child())
preorder(bt.get_right_child())
- 中序遍历
遍历顺序:左节点、根节点、右节点
代码:修改print
的位置,在递归调用的中间
- 后序遍历
遍历顺序:左节点、右节点、根节点
代码:修改print
的位置,在递归调用的最下面
优先队列与二叉堆
优先队列
根据数据的优先级确定出队顺序,优先队列的出队和队列一样从队首出队;但是优先队列内部,数据项的次序却是由“优先级”来确定:
高优先级的数据项排在队首,而低优先级的数据项则排在后面。这样优先队列的入队操作就比较复杂需要将数据项根据其优先级尽量挤到队列前方。
思考:使用有序表实现的话,入队的时间复杂度只能为O(n)
如果使用二叉堆来实现优先队列,能够将优先队列的入队和出队复杂度都保持在O(log n)
二叉堆可以保证队首的一直是最小值,所以优先级最高
二叉堆
- 特点
1、二叉堆的特别之处在于,其逻辑结构上像二叉树,却是用非嵌套的列表来实现的
2、最小key排在队首的称为最小堆min heap
3、最大key排在队首的是最大堆max heap
-
最小堆基本操作:
- BinaryHeap():创建一个空二叉堆对象
- insert(k):将新key加入到堆中
- find_min: 返回堆中的最小项,最小项仍保留在堆中
- del_min: 返回堆中的最小项,同时从堆中删除
- is_empty(): 返回堆是否为空
- size(): 返回堆中key的个数
- build_heap(list): 从一个key列表创建新堆
-
使用非嵌套列表实现二叉堆
为了使堆操作能保持在对数O(log n)水平上,就必须采用二叉树结构,同样如果是操作始终保持在对数数量级上,就必须始终保持二叉树的平衡,即满二叉树(下图,左右节点数必须相同)。
但是满二叉树的数量节点要始终保持在2^k - 1,如果数据项总数不是这样就无法使用。
可以采用“完全二叉树”的结构来近似的实现这种平衡
完全二叉树
完全二叉树,叶节点最多只出现在最底层和次底层,而且最底层的叶节点连续集中在最左边,每个内部节点都有两个子节点,最多可有1个节点例外。
- 特点
完全二叉树,可以用非嵌套列表,以简单的方式实现,具有很好的性质,如果节点的下标为p,那么其左子节点下标为2p,右子节点为2p+1,其父节点下标为p//2
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 5 | 9 | 11 | 14 | 18 | 19 | 21 | 33 | 17 | 27 |
- 堆次序
任何一个节点x,其父节点p中的key均小于x中的key。这样符合“堆”性质的二叉树,其中任何一条路径,均是一个已排序数列,根节点的key最小。
二叉查找树
P68
Binary Search Tree
映射结构,使用二叉查找树保持key,实现key的快速搜索
-
Map的基本操作:
- Map():创建一个空映射
- put(key,val):将
key-val
关联对加入映射中,如果key
已经存在,则将val替换旧关联值; - get(key):给定
key
,返回关联的数据值,如不存在,则返回None
; - del:通过
del map[key]
的语句形式删除key-val
关联: - len():返回映射中
key-val
关联的数目; - in:通过
key in map
的语句形式,返回key
是不存在于关联中,布尔值
-
性质
比父节点小的key都出现在左子树,
比父节点大的key都出现在右子树。
注意:原数据列项为70 31 93 94 14 23 73的顺序插入,这是用70作为树根,如果使用其他值比如31作为树根,那生成的BST就完全不同,所以插入顺序不同生成BST就不同
算法分析【以put为例】
性能决定因素在于二叉搜索树的高度(最大层次),而其高度受数据项key插入顺序的影响
key的列表是随机分布的话,那么大于和小于根节点key的键值分布大致相等,bst的高度就是log 2 n(n是节点的个数),这样的树称为平衡树,put方法最差性能为O(log 2 n)【参考二分法查找】
key列表分布极端情况就完成不同,按照从小到大顺序插入的话,生成一个单列表的形式,put方法的性能为O(n)
python实现
TreeNode
class TreeNode:
def __init__(self, key, val, left=None, right=None, parent=None):
self.key = key
self.payload = val
self.leftChild = left
self.rightChild = right
self.parent = parent
def has_left_child(self):
return self.leftChild
def has_right_child(self):
return self.rightChild
def is_left_child(self):
return self.parent and self.parent.leftChild == self
def is_right_child(self):
return self.parent and self.parent.rightChild == self
def is_root(self):
return not self.parent
def is_leaf(self):
return not (self.rightChild or self.leftChild)
def has_any_children(self):
return self.rightChild or self.leftChild
def has_both_children(self):
return self.rightChild and self.leftChild
def replace_node_data(self, key, value, lc, rc):
self.key = key
self.payload = value
self.leftChild = lc
self.rightChild = rc
if self.has_left_child():
self.leftChild.parent = self
if self.has_right_child():
self.rightChild.parent = self
def __iter__(self):
"""
递归函数
"""
# 判断非None
if self:
if self.has_left_child():
# 遍历左子树,调用__iter__,
# for循环就是递归调用自身
for ele in self.leftChild:
# 返回子树key值
yield ele
# 返回当前节点的key值
yield self.key
# 遍历右子树
if self.has_right_child():
for ele in self.rightChild:
yield ele
BinarySearchTree
class BinarySearchTree:
def __init__(self):
self.root = None
self.size = 0
def length(self):
return self.size
def __len__(self):
return self.size
def __iter__(self):
return self.root.__iter__()
- put
put(key,val) 插入key构造BST
- bst为空,那么key成为根节点root
- bst不为空,调用递归函数_put(key,val,root)来放置key_
- _put(key,val,current_node)
- 如果key比current_node小,那就
_put
到左子树,没有左子树,就成为左子节点 - 如果key大,那就
_put
到右子树,没有右子树就成为右子节点 - 相同时,替换payload值
- 如果key比current_node小,那就
- _put(key,val,current_node)
def put(self, key, val):
if self.root:
self._put(key, val, self.root)
else:
self.root = TreeNode(key, val)
self.size = self.size + 1
def _put(self, key, val, current_node):
if key < current_node:
if current_node.has_left_child():
self._put(key, val, current_node.leftChild)
else:
current_node.leftChild = TreeNode(key, val, parent=current_node)
elif key > current_node:
if current_node.has_right_child():
self._put(key, val, current_node.rightChild)
else:
current_node.rightChild = TreeNode(key, val, parent=current_node)
else:
current_node.payload = val
def __setitem__(self, key, value):
"""
[]方式添加键和值
:param key:
:param value:
:return:
"""
self.put(key, value)
-
get
get(key),找到key对应的val值
- bst为空,返回None
_get(key, self.root)
查找节点,对应key不存在,返回None,找到key,返回对应payload- 根节点则直接返回
- 非根节点,
_get(key,current_node)
递归查找- key比current_node小,从左子树中查找,没有左子节点则返回None
- 相等时直接返回current_node.payload
- key比current_node大,从右子树中查找,没有右子节点则返回None
def get(self, key):
if self.root:
ret = self._get(key, self.root)
if ret:
return ret.payload
return None
def _get(self, key, current_node):
if not current_node:
return None
elif key < current_node.key:
return self._get(key, current_node.leftChild)
elif key == current_node.key:
return current_node
else:
return self._get(key, current_node.rightChild)
def __getitem__(self, item):
"""
[]查询
:param item:
:return:
"""
return self.get(item)
def __contains__(self, item):
"""
判断是否包含'in'
:param item:
:return:
"""
res = self.get(item)
if res:
return True
return False
__iter__
用来实现for迭代,遍历key。for key in my_tree: print(key,my_tree[key])
BST类中的__iter__
方法直接调用TreeNode中的同名方法
TreeNode __iter__
,函数中用了for迭代,实际上是递归函数yield
是对每次迭代的返回值,中序遍历的迭代
yield 生成器中使用,返回对应值并记录上下文
def __iter__(self):
return self.root.__iter__()
- delete
delete(key),删除节点
-
bst.size=0,bst为空,抛异常
-
bst.size=1,则bst只有root
- self.root.key==key,则初始化root=None,size-1=0
- self.root.key !=key,抛异常
def delete(self, key): if self.size == : raise KeyError('error') elif self.size == 1 and self.root.key == key: self.root = None self.size = 0 elif self.size > 1: re_node = self._get(key, self.root) if re_node: self.remove_node(re_node) self.size = self.size - 1 else: raise KeyError('error')
-
bst.size >1:使用_get(key,self.root)递归查找节点
-
key不存在,抛异常
-
key存在,remove方法实现删除节点的细节,删除后,size-1
-
remove节点之后需要保持二叉树的结构不变,需要从3种情况进行分析
-
这个节点是叶节点(没有子节点),直接删除,操作:父节点的左子or右子设为None
def remove_node(self, node): if node.is_leaf: # 判断当前在父节点的位置 if node.parent.leftChild == node: node.parent.leftChild = None else: node.parent.rightChild = None
-
被删除节点有1个节点时,将这个唯一的子节点上移,替换掉被删除的节点,替换操作需要区分几种情况
- 被删除节点的子节点是左,还是右子节点
- 被删除本身是其父节点的左、右
- 被删除本身是根节点
else: # this node has one child if node.has_left_child(): # 当前节点拥有唯一的左子节点 if node.is_left_child(): # 左子节点删除 node.leftChild.parent = node.parent node.parent.leftChild = node.leftChild if node.is_right_child(): # 右子节点删除 node.leftChild.parent = node.parent node.parent.rightChild = node.leftChild else: # 根节点删除 node.replace_node_data(node.leftChild.key, node.leftChild.payload, node.leftChild.leftChild, node.leftChild.rightChild, ) else: # 当前节点拥有唯一的右子节点 if node.is_left_child(): # 左子节点删除 node.rightChild.parent = node.parent node.parent.leftChild = node.rightChild if node.is_right_child(): # 右子节点删除 node.rightChild.parent = node.parent node.parent.rightChild = node.rightChild else: # 根节点删除 node.replace_node_data(node.rightChild.key, node.rightChild.payload, node.rightChild.leftChild, node.rightChild.rightChild, )
-
被删除节点有2个子节点,无法直接将某个子节点上移替换被删除节点,但可以找另一个合适的节点来替换被删除节点,选用右子树中的最小节点,称为后继
1、右子树任意大于左树全部,右子树的最小值,满足大于左树全部小于右树全部的要求
2、这个后继节点最多就一个满足,使用这个后继节点替换被删除节点
3、后继节点可能是叶节点or拥有唯一的右子节点
elif node.has_both_children(): # 节点拥有2个子节点 # 查找后继 succ = node.find_successor() # 后继子节点与后继父节点关联 succ.splice_out() # 后继节点替换被删除节点key,value node.key = succ.key node.payload = succ.payload
# class TreeNode def find_successor(self): succ = None if self.has_right_child(): # 被删除节点拥有2个子节点,所以这里必为true succ = self.rightChild.find_min() # 右子树的最小值 return succ def find_min(self): current = self while current.has_left_child(): # 递归查找直至没有左子(比当前小的节点) current = current.leftChild return current
-
摘出后继节点,将后继节点的子节点与父节点进行关联
- 后继节点为叶节点,将父节点的左or右子设置为None(同remove的第一种情况)
- 后继节点拥有右子节点,父的左or右子与后继的右子关联,右子父关联父节点
# class TreeNode def splice_out(self): # 摘出后继节点 if self.is_leaf(): # 后继节点为叶节点 if self.is_left_child(): self.parent.leftChild = None else: self.parent.rightChild = None elif self.has_right_child(): # 后继节点有一个右子节点 # 极端情况,就是BST树被删除的节点的右子节点没有左子树,正好是最小的key # find_min方法中没有进入while循环 if self.is_right_child(): self.parent.rightChild = self.rightChild else: # 最小值一般为父节点的左子树 self.parent.leftChild = self.rightChild self.rightChild.parent = self.parent
-
-
-
-
额外说明:
AVL树-平衡二叉查找树
视频P71开始
基本上与BST的实现相同,不同之处在于二叉树的生成与维护过程。
实现方式:AVL树需要对每个节点跟踪“平衡因子balance factor”参数。
- 平衡因子
平衡因子是根据节点的左右子树的高度来定义的,确切来说是左右子树的高度差:
balanceFactor = height(leftSubTree) - height(rightSubTree)
如果平衡因子大于0,称为“左重left-heavy”,小于零称为“右重”【下图】,平衡因子等于0,则称为平衡。
- 平衡树
如果一个二叉查找树中每个节点的平衡因子都在-1,0,1之间,则把这个二叉树搜索树称为平衡树。
- 操作:重新平衡
在平衡树操作过程中,有节点的平衡因子超出此范围,则需要一个重新平衡的过程(要保持BST的性质)
见上图,可以将B提上去作根节点,A降下来作左子节,这样就完成平衡了
- AVL树的性能
AVL树最差下的性能,平衡因子为1或者-1。
下图列出平衡因子为1的“左重”AVL树,树的高度从1开始,看问题规模(总节点数N)和对比次数(树的高度h)之间的关系
性能结论:AVL树的搜索时间复杂度为O(log n)
树的小结
映射结构时间复杂度对比
有序表(索引映射) | 散列表 | 二叉查找树 | AVL树 | |
---|---|---|---|---|
put | O(n) | O(1)->O(n) | O(log 2 n)->O(n) | O(log 2 n) |
get | O(log 2 n) | O(1)->O(n) | O(log 2 n)->O(n) | O(log 2 n) |
in | O(log 2 n) | O(1)->O(n) | O(log 2 n)->O(n) | O(log 2 n) |
del | O(n) | O(1)->O(n) | O(log 2 n)->O(n) | O(log 2 n) |
散列表存在散列冲突的存在,使时间复杂度增加,大于O(1),最差为O(n)
二叉查找树在极端插入顺序情况下,生成一个类似线性表的结构,使时间复杂度增加至O(n)
下图列出平衡因子为1的“左重”AVL树,树的高度从1开始,看问题规模(总节点数N)和对比次数(树的高度h)之间的关系
[外链图片转存中…(img-Qp3JwJs9-1715872409511)]
性能结论:AVL树的搜索时间复杂度为O(log n)
树的小结
映射结构时间复杂度对比
有序表(索引映射) | 散列表 | 二叉查找树 | AVL树 | |
---|---|---|---|---|
put | O(n) | O(1)->O(n) | O(log 2 n)->O(n) | O(log 2 n) |
get | O(log 2 n) | O(1)->O(n) | O(log 2 n)->O(n) | O(log 2 n) |
in | O(log 2 n) | O(1)->O(n) | O(log 2 n)->O(n) | O(log 2 n) |
del | O(n) | O(1)->O(n) | O(log 2 n)->O(n) | O(log 2 n) |
散列表存在散列冲突的存在,使时间复杂度增加,大于O(1),最差为O(n)
二叉查找树在极端插入顺序情况下,生成一个类似线性表的结构,使时间复杂度增加至O(n)
推荐:有序使用散列表,更复杂的情况可以使用AVL树