7. 非线性结构--树
目录
树在计算机科学的各个领域中被广泛应用:操作系统、图形学、数据库系统、计算机网络。
跟自然界的树一样,数据结构树也分为:根、枝和叶等三个部分:一般数据结构的图示把根放在上方,也放在下方。
树是一种分层结构,越接近顶部的层越普遍,越接近底部的层越独特。
一个节点的子节点与另一个节点的子节点相互之间是隔离、独立的。
每一个叶节点都具有唯一性。
树的例子1:文件系统:
树的例子2:HTML文档(嵌套标记)
树的例子3:域名体系
1.树结构相关术语
1.节点Node:组成树的基本部分
每个节点具有名称,或“键值”,节点还可以保存额外数据项,数据项根据不同的应用而变。
2.边Edge:边是组成树的另一个基本部分
每条边恰好链接两个节点,表示节点之间具有关联,边具有出入方向;
每个节点(除根节点)恰有一条来自另一节点的入边;
每个节点可以有多条连到其它节点的出边。
3.根Root:树中唯一一个没有入边的节点
4.路径Path:由边依次连接在一起的节点的有序列表
如:HTML->BODY->UL->LI,是一条路径
5.子节点Children:入边均来自于同一个节点的若干节点,称为这个节点的子节点
6.父节点Parent:一个节点是其所有出边所连接节点的父节点。
7.兄弟节点Sibling:具有同一个父节点的节点之间称为兄弟节点
8.子树Subtree:一个节点和其所有子孙节点,以及相关边的集合
9.叶节点Leaf:没有子节点的节点称为叶节点
10.层级Level:从根节点开始到达一个节点的路径,所包含的边的数量,称为这个节点的层级。
如D的层级为2,根节点的层级0
11.高度:树中所有节点的最大层级称为树的高度
如右图的高度为2
# 树的定义1
树由若干节点,以及两两连接节点的边组成,并有如下性质:
其中一个节点被称为根;
每个节点n(除根节点),都恰连接一条来自节点p的边,p是n的父节点;
每个节点从根开始的路径是唯一的,如果每个节点最多有两个子节点,这样的树称为“二叉树”
树的定义2(递归定义)
树是:
空集;
或者由根节点及0或多个字树构成(其中字数也是树),每个字树的根到根节点具有边相连。
2.树的嵌套列表实现
首先我们尝试用python list来实现二叉树数据结构;
递归的嵌套列表实现二叉树,由具有3个元素的列表实现:
第1个元素为根节点的值;
第2个元素是左子树(所以也是一个列表);
第3个元素是右子树(所以也是一个列表)。
[root, left, right]
以下图的示例,一个6节点的二叉树:
根是myTree[0],左子树myTree[1],右子树myTree[2]
# 嵌套列表法的优点
子树的结构与树相同,是一种递归数据结构。很容易扩展到多叉树,仅需要增加列表元素即可。
myTree = ['a',# 树根
['b', # 左子树
['d', [], [] ],
['e', [], [] ],
],
['c', # 右子树
['f', [], [] ],
[]
]
]
我们通过定义一系列函数来辅助操作嵌套列表:
BinaryTree创建仅有根节点的二叉树
insertLeft/insertRight将新节点插入树中作为其直接的左/右子节点
get/setRootVal则取得或返回根节点
getLeft/RightChild返回左/右子树
def binary_tree(r):
return [r, [], []]
def insert_left(root,new_branch):
t = root.pop(1)
if len(t) > 1:
root.insert(1, [new_branch, t, []])
else:
root.insert(1, [new_branch, [], []])
return root
def insert_right(root,new_branch):
t = root.pop(2)
if len(t) > 1:
root.insert(2, [new_branch, [], t])
else:
root.insert(2, [new_branch, [], []])
return root
def get_root_val(root):
return root[0]
def set_root_val(root, new_val):
root[0] = new_val
def get_left_child(root):
return root[1]
def get_right_child(root):
return root[2]
r = binary_tree(3)
insert_left(r,4)
insert_left(r,5)
insert_right(r,6)
insert_right(r,7)
l = get_left_child(r)
print(l)
set_root_val(l,9)
print(r)
insert_left(l,11)
print(r)
print(get_right_child(get_right_child(r)))
'''
[5, [4, [], []], []]
[3, [9, [4, [], []], []], [7, [], [6, [], []]]]
[3, [9, [11, [4, [], []], []], []], [7, [], [6, [], []]]]
[6, [], []]
'''
3.节点链接法实现树
同样可以用节点链接法来实现树。每个节点保存根节点的数据项,以及指向左右子树的链接。
# 定义一个BinaryTree类
成员key保存根节点数据项
成员left/rightChild则保存指向左/右子树的
引用(同样是BinaryTree对象)
class BinaryTree:
def __init__(self, root_obj):
self.key = root_obj
self.left_child = None
self.right_child = None
def insert_left(self, new_node):
if self.left_child == None:
self.left_child = BinaryTree(new_node)
else:
t = BinaryTree(new_node)
t.left_child = self.left_child
self.left_child = t
def insert_right(self, new_node):
if self.right_child == None:
self.right_child = BinaryTree(new_node)
else:
t = BinaryTree(new_node)
t.right_child = self.right_child
self.right_child = t
def get_right_child(self):
return self.right_child
def get_left_child(self):
return self.left_child
def set_root_val(self, obj):
self.key = obj
def get_root_val(self):
return self.key
r = BinaryTree('a')
r.insert_left('b')
r.insert_right('c')
r.get_right_child().set_root_val('hello')
r.get_left_child().insert_right('d')
print(r.get_left_child().__dict__) # {'key': 'b', 'left_child': None, 'right_child': <__main__.BinaryTree object at 0x00000154DA196B48>}
print(r.get_left_child().__dict__['right_child'].__dict__) # {'key': 'd', 'left_child': None, 'right_child': None}
4.树的应用:解析树(语法树)
将树用于表示语言中句子,可以分析句子的各种语法成分,对句子的各种成分进行处理。
语法分析树:主谓宾,定状补。
# 程序设计语言的编译
词法、语法检查
从语法树生成目标代码
# 自然语言处理
机器翻译、语义理解
我们还可以将表达式表示为树结构,叶节点保存操作数,内部节点保存操作符;
全括号表达式((7 + 3)*(5-2)),由于括号的存在,需要计算*的话,就必须先计算7+3和5-2,表达式层次决定计算的优先级,越底层的表达式,优先级越高。
# 树中每个子树都表示一个子表达式
将子树替换为子表达式值的节点,即可实现求值。
下面,我们用树结构来做如下尝试:
从全括号表达式构建表达式解析树,利用表达式解析树对表达式求值。从表达式解析树恢复表达式的字符串形式。
首先,全括号表达式要分解为单词Token列表。
其单词分为括号“()”、操作符“+-/”和操作数“0~9”这几类,左括号就是表达式的开始,而右括号是表达式的结束。
# 建立表达式解析树:实例
全括号表达式:(3+(4*5)),分解为单词表
['(', '3', '+', '(', '4', '*', '5', ')', ')']
# 创建表达式解析树过程
创建空树,当前节点为根节点
读入'(',创建了左子节点,当前节点下降;
读入'3',当前节点设置为3,上升到父节点;
读入'+',当前节点设置为+,创建右子节点,当前节点下降。
读入'(',创建左子节点,当前节点下降;
读入'4',当前节点设置为4,上升到父节点;
读入'*',当前节点设置为*,创建右子节点,当前节点下降;
读入'5',当前节点设置为5,上升到父节点;
读入')',上升到父节点;
读入')',再上升到父节点;
# 建立表达式解析树:规则
从左到右扫描全括号表达式的每个单词,依据规则建立解析树。
如果当前单词是'(':为当前节点添加一个新节点作为其左子节点,当前节点下降为这个新节点;
如果当前单词是操作符'+, -, /, *':将当前节点的值设为此符号,为当前节点添加一个新节点作为其右子节点,当前节点下降为这个新节点;
如果当前单词是操作数:将当前节点的值设为此数,当前节点上升到父节点;
如果当前单词是')':则当前节点上升到父节点。
# 建立表达式解析树:思路
从图示过程中我们看到,创建树过程中关键的是对当前节点的跟踪。
创建左右子树可调用insertLeft/Right
当前节点设置值,可以调用setRootVal
下降到左右子树可调用getLeft/RightChild
但是,上升到父节点,这个没有方法支持!
我们可以用一个栈来记录跟踪父节点
当前节点下降时,将下降前的节点push入栈
当前节点需要上升到父节点时,上升到pop出栈的节点即可!
# 建立表达式解析树:代码
from pythonds import Stack
class BinaryTree:
def __init__(self, root_obj):
self.key = root_obj
self.left_child = None
self.right_child = None
def insert_left(self, new_node):
if self.left_child == None:
self.left_child = BinaryTree(new_node)
else:
t = BinaryTree(new_node)
t.left_child = self.left_child
self.left_child = t
def insert_right(self, new_node):
if self.right_child == None:
self.right_child = BinaryTree(new_node)
else:
t = BinaryTree(new_node)
t.right_child = self.right_child
self.right_child = t
def get_right_child(self):
return self.right_child
def get_left_child(self):
return self.left_child
def set_root_val(self, obj):
self.key = obj
def get_root_val(self):
return self.key
def buildParseTree(fpexp):
fplist = [char for char in fpexp]
p_stack = Stack()
e_tree = BinaryTree('')
p_stack.push(e_tree) # 入栈下降
current_tree = e_tree
for i in fplist:
if i == '(': # 表达式开始
current_tree.insert_left('')
p_stack.push(current_tree) # 入栈下降
current_tree = current_tree.get_left_child()
elif i not in ['+', '-', '*', '/',')']: # 操作数
current_tree.set_root_val((int(i)))
parent = p_stack.pop()
current_tree = parent # 出栈上升
elif i in ['+', '-', '*', '/']: # 操作符
current_tree.set_root_val(i)
current_tree.insert_right('')
p_stack.push(current_tree)
current_tree = current_tree.get_right_child()
elif i == ')': # 表达式结束
current_tree = p_stack.pop() # 出栈上升
else:
raise ValueError
return e_tree
# 利用表达式解析树求值:思路
创建表达式解析树,可用来进行求值
由于二叉树BinaryTree是一个递归数据结构,自然可以用递归算法来处理
求值递归函数evaluate:由前述对子表达式的描述,可从树的底层子树开始,逐步向上层求值,最终得到整个表达式的值
# 求值函数evaluate的递归三要素:
基本结束条件:叶节点时是最简单的子树,没有左右子节点,其根节点的数据项即为子表达式树的值
规模缩小:将表达式树分为左子树、右子树,即为缩小规模
调用自身:分别调用evaluate计算左子树和右子树的值,然后将左右子树的值依根节点的操作符进行计算,从而得到表达式的值;
一个增加程序可读性的技巧:函数引用
import operator
op = operator.add
>>> import operator
>>> operator.add
<built-in function add>
>>> operator.add(1,2)
3
>>> op = operator.add
>>> n = op(1,2)
3
import operator
def evaluate(parse_tree):
opers = {'+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv()}
left_c = parse_tree.get_left_child() # 缩小规模
right_c = parse_tree.get_right_child()
if left_c and right_c:
fn = opers[parse_tree.get_root_val()]
return fn(evaluate(left_c), evaluate(right_c)) # 递归调用
else:
return parse_tree.get_root_val() # 基本结束条件
5.树的遍历Tree Traversals
对一个数据集中的所有数据项进行访问的操作称为“遍历Traversal”
线性数据结构中,对其所有数据项的访问比较简单直接,按照顺序依次进行即可
树的非线性特点,使得遍历操作较为复杂
我们按照对节点访问次序的不同来区分3中遍历:
前序遍历(preorder):先访问根节点,再递归地前序访问左子树、最后前序访问右子树;
中序遍历(inorder):先递归地中序访问左子树,再访问根节点,最后中序访问右子树;
后续遍历(postorder):先递归地后序访问左子树,再后序访问右子树,最后访问根节点。
前序遍历的例子:一本书的章节阅读
Book -> Ch1 -> S1.1 -> S1.2 -> S1.2.1 -> S1.2.2 -> Ch2 -> S2.1 -> S2.2 -> S2.2.1 ->S2.2.2
树的遍历:递归算法代码
树遍历的代码非常简洁
def preorder(tree):
if tree:
print(tree.get_root_val())
preorder(tree.get_left_child())
preorder(tree.get_right_child())
后序和中序遍历的代码仅需要调整顺序
def postorder(tree):
if tree != None:
postorder(tree.get_left_child())
postorder(tree.get_right_child())
print(tree.get_root_val())
def inorder(tree):
if tree != None:
inorder(tree.get_left_child())
print(tree.get_root_val())
inorder(tree.get_right_child())
也可以再BinaryTree类中实现前序遍历的方法:
需要加入子树是否为空的判断
def preorder(self):
print(self.key)
if self.left_child:
self.left_child.preorder()
if self.right_child:
self.right_child.preorder()
回顾前述的表达式解析树求值,实际上也是一个后序遍历的过程
采用后序遍历法重写表达式求值代码:
def post_order_eval(tree):
opers = {'+':operator.add, '-':operator.sub, '*':operator.mul, '/':operator.truediv}
res1 = None
res2 = None
if tree:
res1 = post_order_eval(tree.get_left_child()) # 左子树
res2 = post_order_eval(tree.get_right_child()) # 右子树
if res1 and res2:
return opers[tree.get_root_val()](res1, res2) # 根节点
else:
return tree.get_root_val()
# 中序遍历:生成全括号中缀表达式
采用中序遍历递归算法来生成全括号中缀表达式
下列代码中队每个数字也加了括号,请自行修改代码去除(练习)
def print_exp(tree):
s_val = ""
if tree:
s_val = '(' + print_exp(tree.get_left_child())
s_val = s_val + str(tree.get_root_val())
s_val = s_val + print_exp(tree.get_right_child()) + ')'
return s_val
6.优先队列和二叉堆
# 优先队列Priority Queue
前面我们学习了一种FIFO数据结构队列,队列有一种变体称为“优先队列”。
银行窗口区号排队,VIP客户可以插到队首;
操作系统中执行关键任务的进程或用户特别指定进程在调度队列中靠前。
优先队列的出队跟队列一样从队首出队;
但在优先队列内部,数据项的次序却是由“优先级”来确定:
高优先级的数据项排在队首,而低优先级的数据项则排在后面。这样,优先队列的入队操作就比较复杂,需要将数据项根据其优先级尽量挤到队列前方。
思考:有什么方案可以用来实现优先队列?出队和入队的复杂度大概是多少?
# 二叉堆Binary Heap 实现优先队列
实现优先队列的经典方案是采用二叉堆数据结构,二叉堆能够将优先队列的入队和出队复杂度都保持在O(log n)
二叉堆的有趣之处在于,其逻辑结构上像二叉树,却是用非嵌套的列表来实现的!
最小key排在队首的称为“最小堆min heap”,反之,最大key排在队首的是“最大堆max heap”
ADT BinaryHeap的操作定义如下:
BinaryHeap():创建一个空二叉堆对象;
insert(k):将新key加入到堆中;
findMin():返回堆中的最小项,最小项仍保留在堆中;
delMin():返回堆中的最小项,同时从堆中删除;
isEmpty():返回堆是否为空;
size():返回堆中key的个数;
buildHeap(list):从一个key列表创建新堆;
from pythonds.trees.binheap import BinHeap
bh = BinHeap()
bh.insert(5)
bh.insert(7)
bh.insert(3)
bh.insert(11)
print(bh.delMin()) # 3
print(bh.delMin()) # 5
print(bh.delMin()) # 7
print(bh.delMin()) # 11
'''
3
5
7
11
'''
用非嵌套列表实现二叉堆
# 用非嵌套列表实现二叉堆
为了使堆操作能保持在对数水平上,就必须采用二叉树结构;
同样,如果要使操作始终保持在对数数量级上,就必须始终保持二叉树的“平衡”(树根左右子树拥有相同数量的节点)
我们采用完全二叉树的结构来近似实现“平衡”
完全二叉树,叶节点最多只出现在最底层和次底层,而且最底层的叶节点都连续集中在最左边,每个内部节点都有两个子节点,最多可有1个节点例外
# 完全二叉树的列表实现及性质
完全二叉树由于其特殊性,可以用非嵌套列表,以简单的方式实现,具有很好性质。
如果节点的下标为p,那么其左子节点下标为2p,右子节点为2p+1,其父节点下标为p//2
# 堆次序Heap Order
任何一个节点x,其父节点p中的key均小于x中的key。这样,符合“堆”性质的二叉树,其中任何一条路径,均是一个已排序数列,根节点的key最小。
# 二叉堆操作的实现
二叉堆初始化,采用一个列表来保存堆数据,其中表首下标为0的项无用,但为了后面代码可以用到简单的优化,仍保留它。
class BinHeap:
def __init__(self):
self.heap_list = [0]
self.current_size = 0
insert(key)方法
首先,我为了保持“完全二叉树”的性质,新key应该添加到列表末尾。会有问题吗?
# insert(key)方法
新key加在列表末尾,显然无法保持“堆”次序,虽然对其它路径的次序没有影响,但对于其到根的路径可能破坏次序。
# insert(key)方法
需要将新key沿着路径来“上浮”到其正确位置
注意:新key的“上浮”不会影响到其它路径节点的“堆”次序
class BinHeap:
def __init__(self):
self.heap_list = [0]
self.current_size = 0
def perc_up(self, i):
while i // 2 > 0:
if self.heap_list[i] < self.heap_list[i // 2]:
tmp = self.heap_list[i // 2] # 与父节点交换
self.heap_list[i // 2] = self.heap_list[i]
self.heap_list[i] = tmp
i //= 2 # 沿路径向上
def insert(self, k):
'''
insert:
'''
self.heap_list.append(k) # 添加 到末尾
self.current_size += 1
self.perc_up(self.current_size) # 新key上浮
del_min()方法
移走整个堆中最小的key:根节点heapList[1],为了保持“完全二叉树”的性质,只用最后一个节点来代替根节点。
同样,这么简单的替换,还是破坏了“堆”次序。
解决方法:将新的根节点沿着一条路径“下沉”,直到比两个子节点都小。
“下沉”路径的选择:如果比子节点大,那么选择较小的子节点进行交换下沉。
def perc_down(self, i):
while (i * 2) <= self.current_size:
mc = self.min_child(i)
if self.heap_list[i] > self.heap_list[mc]:
tmp = self.heap_list[i]
self.heap_list[i] = self.heap_list[mc] # 交换下沉
self.heap_list[mc] = tmp
i = mc # 沿路径向下
def min_child(self, i):
if i * 2 + 1 > self.current_size:
return i * 2 # 唯一子节点
else:
if self.heap_list[i * 2] < self.heap_list[i * 2 + 1]:
return i * 2
else: # 返回较小的
return i * 2 + 1
def del_min(self):
retval = self.heap_list[1] # 移走堆顶
self.heap_list[1] = self.heap_list[self.current_size]
self.current_size = self.current_size - 1
self.heap_list.pop()
self.perc_down(1) # 新顶下沉
return retval
buildHeap(lst)方法:从无序表生成“堆”。
我们最自然的想法是:用insert(key)方法,将无序表中数据项逐个insert到堆中,但这么做的总代价是O(nlog n)
其实,用“下沉”法,能够将总代价控制在O(n)
buildHeap(lst)方法:从无序表生成“堆”
其实,用“下沉”法,能够将总代价控制在O(n)
def build_heap(self, alist):
i = len(alist) // 2 # 从最后节点的父节点开始,因叶节点无需下沉
self.current_size = len(alist)
self.heap_list = [0] + alist[:]
print(len(self.heap_list), i)
while i > 0:
print(self.heap_list, i)
self.perc_down(i)
i -= 1
print(self.heap_list, i)
思考:利用二叉堆来进行排序?
“堆排序”算法:O(nlog n)
一、写一个buildTree函数(返回一个BinaryTree对象),函数通过调用BinaryTree类方法,返回如图所示的二叉树:
二、请为链接实现的BinaryTree类写一个__str__方法,把二叉树的内容用嵌套列表的方式打印输出。
三、请为链接实现的BinaryTree类写一个height方法,返回树的高度。
7.二叉查找树Binary Search Tree
在ADT Map的实现方案中,可以采用不同的数据结构和搜索算法来保存和查找key,前面已经实现了两个方案。
有序表数据结构 + 二分搜索算法
散列表数据结构 + 散列及冲突解决算法
接下来试试用二叉查找树保存key,实现key的快速搜索。
1.二叉查找树:ADT Map
复习下ADT Map的操作:
Map():创建一个空映射
put(key,val):将key-val关联对加入映射中,如果key已经存在,则将val替换旧关联值;
get(key): 给定key,返回关联的数据值,如不存在,则返回None;
del: 通过del map[key]的语句形式删除key-va1关联;
len(): 返回映射中key-val关联的数目;
in: 通过key in map的语句形式,返回key是否存在于关联中,布尔值
2.二叉查找树BST的性质
比父节点小的key都出现在左子树,比父节点大的key都出现在右子树。
按照70、31、93、94、14、23、73的顺序插入
首先插入的70成为树根
31比70小,放到左子节点
93比70大,放到右子节点
94比93大,放到右子节点
14比31小,放到左子节点
23比14大,放到其右
73比93小,放到其左
注意: 插入顺序不同,生成的BST也不同
3.二叉搜索树的实现
# 二叉搜索树的实现:节点和链接结构
需要用到BST和TreeNode两个类,BST的root成员引用根节点TreeNode
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__()
# TreeNode类
class TreeNode:
def __init__(self,key, val, left=None, right=None, parent=None):
# 键值、数据项、左右子节点、父节点
self.key = key
self.val = val
self.left_child = left
self.right_child = right
self.parent = parent
def has_left_child(self):
return self.left_child
def has_right_child(self):
return self.right_child
def is_left_child(self):
return self.parent and self.parent.left_child == self
def is_right_child(self):
return self.parent and self.parent.right_child == self
def is_root(self):
return not self.parent
def is_leaf(self):
return not (self.right_child or self.left_child)
def has_any_children(self):
return self.right_child or self.left_child
def has_both_children(self):
return self.right_child and self.left_child
def replace_node_data(self,key, value, lc, rc):
self.key = key
self.val = value
self.left_child = lc
self.right_child = rc
if self.has_left_child():
self.left_child.parent = self
if self.has_right_child():
self.right_child.parent = self
# BST.put方法
put(key, val)方法:插入key构造BST,首先BST是否为空,如果一个节点都没有,那么key称为根节点root。
否则,就调用一个递归函数_put(key, val, root)来放置key
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
# _put(key, val, currentNode)的流程:如果key比currentNode小,
# 那么_put到左子树,但如果没有左子树,那么key就称为左子节点;
# 如果key比currentNode大,那么_put到右子树,但如果没有右子树,那么key就成为右子节点。
def _put(self, key, val, currentNode):
if key < currentNode.key:
if currentNode.has_left_child():
self._put(key, val, currentNode.left_child) # 递归左子树
else:
currentNode.left_child = TreeNode(key, val, parent=currentNode)
else:
if currentNode.has_right_child():
self._put(key, val, currentNode.right_child) # 递归右子树
else:
currentNode.right_child = TreeNode(key, val,parent=currentNode)
# 二叉搜索树的实现:索引赋值
随手把__setitem__做了,特殊方法(前后双下划线)
可以myZipTree['PKU'] = 100871
def __setitem__(self, k, v):
self.put(k, v)
mytree = BinarySearchTree()
mytree[3] = 'red'
mytree[4] = 'blue'
mytree[6] = 'yellow'
mytree[2] = 'at'
# 二叉搜索树的实现:BST.put图示
插入key=19,currentNode的变化过程
# 二叉树的实现:BST.get方法
在树中找到key所在的节点取到payload
def get(self, key):
if self.root:
res = self._get(key, self.root) # 递归函数
if res:
return res.val # 找到节点
else:
return None
else:
return None
def _get(self, key, currentNode):
if not currentNode:
return None
elif currentNode.key == key:
return currentNode
elif key < currentNode.key:
return self._get(key, currentNode.left_child)
else:
return self._get(key, currentNode.right_child)
# 二叉树的实现:索引和归属判断
__getitem__特殊方法
实现val=myZipTree['PKU']
__contains__特殊方法
实现'PKU' in myZipTree 的归属判断运算符in
def __getitem__(self, key):
return self.get(key)
def __contains__(self, key):
if self._get(key, self.root):
return True
else:
return False
mytree[3] = 'red'
mytree[4] = 'blue'
mytree[6] = 'yellow'
mytree[2] = 'at'
print(3 in mytree)
print(mytree[6])
# 二叉搜索树的实现:迭代器
我们可以用for循环枚举字典中的所有key,ADT Map也应该实现这样的迭代器功能。
特殊方法__iter__可以用来实现for迭代,BST类中的__iter__方法直接调用了TreeNode中的同名方法
mytree = BinarySearchTree()
mytree[3] = 'red'
mytree[4] = 'blue'
mytree[6] = 'yellow'
mytree[2] = 'at'
print(3 in mytree)
print(mytree[6])
del mytree[3]
print(mytree[2])
for key in mytree:
print(key, mytree[key])
TreeNode 类中的__iter__迭代器
迭代器函数中用了for迭代,实际上是递归函数yeild是对每次迭代的返回值中序遍历的迭代
def __iter__(self):
if self:
if self.has_left_child(): # 取左子树
for elem in self.left_child:
yield elem
yield self.key # 取根
if self.has_right_child():
for elem in self.right_child: # 取右子树
yield elem
# 二叉查找树的实现:BST.delete方法
# 有增有减,最复杂的delete方法:
# 用_get找到要删除的节点,然后调用remove来删除,找不到则提示错误
def delete(self,key):
if self.size > 1:
nodeToRemove = self._get(key, self.root)
if nodeToRemove:
self.remove(nodeToRemove)
self.size = self.size - 1
else:
raise KeyError('Error, key not in tree')
elif self.size == 1 and self.root.key == key:
self.root = None
self.size = self.size - 1
__delitem__特殊方法
实现del myZipTree['PKU']这样的语句操作
def __delitem__(self, key):
self.delete(key)
在delete中,最复杂的是找到key对应的节点之后的remove节点方法!
# 二叉查找树的实现:BST.remove方法
从BST中remove一个节点,还要求仍然保持BST的性质,分以下3中情形:
这个节点没有子节点
这个节点有1个子节点
这个节点有2个子节点
1.没有子节点的情况好办,直接删除
if currentNode.is_leaf(): # leaf
if currentNode == currentNode.parent.left_child:
currentNode.parent.left_child = None
else:
currentNode.parent.right_child = None
# 二叉查找树的实现:BST.remove方法
2.这个节点有1个子节点
解决:将这个唯一的子节点上移,替换掉被删节点的位置
但替换操作需要区分集中情况:
被删节点的子节点是左?还是右子节点?
被删节点本身是其父节点的左?还是右子节点?
被删节点本身就是根节点?
else: # this is node has one child
if currentNode.has_left_child():
if currentNode.is_left_child(): # 左子节点删除
currentNode.left_child.parent = currentNode.parent
currentNode.parent.left_child = currentNode.left_child
elif currentNode.is_right_child(): # 右子节点删除
currentNode.left_child.parent = currentNode.parent.left_child
currentNode.parent.right_child = currentNode
else: # 根节点删除
currentNode.replace_node_data(currentNode.left_child.key,
currentNode.left_child.val,
currentNode.left_child.left_child,
currentNode.left_child.right_child)
else:
if currentNode.is_left_child(): # 左子节点删除
currentNode.right_child.parent =
currentNode.parent.left_child =
elif currentNode.is_right_child(): # 右子节点删除
currentNode.right_child.parent =
currentNode.parent.right_child =
else: # 根节点删除
currentNode.replace_node_data(currentNode)
# 二叉查找树的实现:BST.remove方法
3.第三种情形最复杂:被删节点有2个子节点
这时无法简单地将某个子节点上移替换被删节点,但可以找到另一个合适的节点来替换被删节点,这个合适节点就是被删节点的下一个key值节点,即被删节点右子树中最小的那个,称为“后继”
# BinarySearchTree类:remove方法(情形3)
elif currentNode.has_both_children(): # interior
succ = currentNode.find_successor()
succ.splice_out()
currentNode.key = succ.key
currentNode.val = succ.val
# TreeNode类:寻找后继节点
def find_successor(self):
succ = None
if self.has_right_child():
succ = self.right_child.find_min()
else:
if self.parent:
if self.is_left_child():
succ = self.parent
else: # 目前不会遇到
self.parent.right_child = None
succ = self.parent.find_successor()
self.parent.right_child = self
return succ
def find_min(self):
current = self
while current.has_left_child():
current = current.left_child # 到左下角
return current
def splice_out(self):
if self.is_leaf(): # 摘出叶节点
if self.is_left_child():
self.parent.left_child = None
else:
self.parent.right_child = None
elif self.has_any_children():
if self.has_left_child():
if self.is_left_child(): # 目前不会遇到
self.parent.left_child = self.left_child
else:
self.parent.right_child = self.left_child
self.left_child.parent = self.parent
else:
if self.is_left_child(): # 摘出带右子节点的节点
self.parent.left_child = self.right_child
else:
self.parent.right_child = self.right_child
self.right_child.parent = self.parent
4.二叉查找树:算法分析(以put为例)
其性能决定因素在于二叉搜索树的高度(最大层次),而其高度又受数据项key插入顺序的影响。
如果key的列表是随机分布的话,那么大于和小于根节点key的键值大致相等。
BST的高度就是log2 n (n是节点的个数),而且,这样的树就是平衡树。
put方法最差性能位O(log2 n)。
但key列表分布极端情况就完全不同。按照从小到大顺序插入的话,如下图:这时候put方法的性能为O(n)其它方法也是类似情况。
如何改进BST?不受key插入顺序影响?
8.平衡二叉查找树
1.AVL树的定义
我们来看看能够在key插入时一直保持平衡的二叉树:AVL树
AVL是发明者的名字缩写:G.M. Adelson-Velskii and E.M. Landis
利用AVL树实现(AbstractDataType)ADT Map,基本上与BST的实现相同
不同之处仅在于二叉树的生成与维护过程
AVL树实现中,需要对每个节点跟踪“平衡因子balance factor”参数
平衡因子是根据节点的左右子树的高度来定义的,确切地说,是左右子树高度差:
balanceFactor = height(leftSubTree) - height(rightSubTree)
如果平衡因子大于0,称为“左重left-heavy”,小于零称为“右重right-heavy”,;平衡因子等于0,则称作平衡。
# 平衡二叉查找树:平衡因子
如果一个二叉查找树中每个节点的平衡因子都在-1,0,1之间,则把这个二叉搜索树称为平衡树。
# 平衡二叉查找树:AVL树的定义
在平衡树操作过程中,右节点的平衡因子超出此范围,则需要一个重新平衡的过程
要保持BST的性质!
思考:如果重新平衡,应该变成什么样?
2.AVL树的性能
我们来分析AVL树最差情形下的性能:即平衡因子为1或者-1
下图列出平衡因子为1的“左重”AVL树,树的高度从1开始,来看看问题规模(总节点数N)和比对次数(树的高度h)之间的关系如何?
# AVL树性能分析
观察上图h=1~4时,总节点数N的变化
h = 1, N= 1
h = 2, N= 2= 1+ 1
h = 3, N= 4= 1+ 1+ 2
h = 4, N= 7= 1+ 2+ 4
Nh= 1 + N(h-1) + N(h-2)
观察这个通式,很接近斐波那契数列!
图1、图2
图1:
图2:
3.AVL树的python实现
既然AVL平衡树确实能够改进BST树的性能,避免退化情形。
我们来看看向AVL树插入一个新key,如何才能保持AVL树的平衡性质。
首先,作为BST,新key必定以叶节点形式插入到AVL树中
叶节点的平衡因子是0,其本身无需重新平衡。
但会影响其父节点的平衡因子:
作为左子节点插入,则父节点平衡因子会增加1;
作为右子节点插入,则父节点平衡因子会减少1。
这种影响可能随着其父节点到根节点的路径一直传递上去,直到:
传递到根节点为止;
或者某个父节点平衡因子被调整到0,不再影响上层节点的平衡因子为止。
(无论从-1或者1调整到0,都不会改变子树高度)
def _put(self, key, val, currentNode):
if key < currentNode.key:
if currentNode.has_left_child():
self._put(key, val, currentNode.left_child)
else:
currentNode.left_child = TreeNode(key, val, parent=currentNode)
self.update_balance(currentNode.left_child) # 调整因子
else:
if currentNode.has_right_child():
self._put(key, val, currentNode.right_child)
else:
currentNode.right_child = TreeNode(key, val, parent=currentNode)
self.update_balance(currentNode.right_child) # 调整因子
# UpdateBalance方法
def update_balance(self, node):
if node.balance_factor > 1 or node.balance_factor < -1:
self.rebalance(node) # 重新平衡
return
if node.parent != None:
if node.is_left_child():
node.parent.balance_factor += 1
elif node.is_right_child():
node.parent.balance_factor -= 1
if node.parent.balance_factor != 0:
self.update_balance(node.parent) # 调整父节点因子
# AVL 树的实现:rebalance重新平衡
主要手段:将不平衡的子树进行旋转(rotation)
视“左重”或者“右重”进行不同方向的旋转,同时更新相关父节点引用,更新旋转后被影响节点的平衡因子。
# AVL 树的实现:rebalance重新平衡
如图,是一个“右重”子树A的左旋转(并保持BST性质)
将右子节点B提升为子树的根,将旧根节点A作为新根节点B的左子节点。
如果新根节点B原来有左子节点,则将此节点设置为A的右子节点(A的右子节点一定有空)
# AVL 树的实现:rebalance重新平衡
更复杂一些的情况:如图的“左重”子树右旋转。旋转后,新根节点将旧根节点作为右子节点,但是新根节点原来已有右子节点,需要将原有的右子节点重新定位!原有的右子节点D改到旧根节点E的左子节点。同样,E的左子节点在旋转后一定有空。
# AVL树的实现:rotateLeft代码 参照下图
def rotate_left(self, rotRoot):
# 指出新的根节点是旧根节点A的右子节点
newRoot = rotRoot.right_child
# 旧的根节点的右子节点指向新的根节点的左子节点。当然我们的这个图中B是没有左子节点的。
# 如果要有,也需要把B的左子节点挂到A的右边去(A的右子节点一定为空,因为原来的右子节点B为新的根,A的右子节点空出来了),
rotRoot.right_child = newRoot.left_child
if newRoot.right_child != None:
newRoot.left_child.parent = rotRoot
if rotRoot.isRoot():
self.root = newRoot
else:
if rotRoot.is_left_child():
rotRoot.parent.left_child = newRoot
else:
rotRoot.parent.right_child = newRoot
newRoot.left_child = rotRoot
rotRoot.parent = newRoot
# 仅有两个节点需要调整因子:旧的根A和新的根B 平衡因子的调节看下面的分析
rotRoot.balance_factor = rotRoot.balance_factor + 1 - min(newRoot.balance_factor, 0)
rotRoot.balance_factor = rotRoot.balance_factor + 1 + max(rotRoot.balance_factor, 0)
# AVL树的实现:如何调整平衡因子
看看左旋转对平衡因子的影响:
保持了次序ABCDE
ACE的平衡因子不变
hA/hB/hE不变
主要看BD新旧关系
我们来看看B的变化:
新B= hA- hC
旧B= hA- 旧hD
而:
旧hD= 1+ max(hC, hE),所以旧B= hA- (1+ max(hC, hE))
新B- 旧B= 1+ max(hc, hE)- hC
新B= 旧B+ 1+ max(hC, hE)- hC;把hC移进max函数里就有
新B= 旧B+ 1+ max(0,-旧D) <==> 新B= 旧B+ 1- min(0,旧D)
rotRoot.balanceFactor = rotRoot.balanceFactor + 1 - min(newRoot.balanceFactor, )
# AVL树的实现:更复杂的情形
下图的“右重”子树,单纯的左旋转无法实现平衡。
左旋转后变成“左重”了,“左重”再右旋转,还回到“右重”
# AVL树的实现:更复杂的情形
所以,再左旋转之前检查右子节点的因子。如果右子节点“左重”的话,先对它进行右旋转,再实施原来的左旋转。# 根据右子节点因子为依据,如果左重(右子节点平衡因子大于零),则右子节点右边(右子节点的左子节点)的旋转;如果右重(右子节点平衡因子小于零),则右子节点左边(右子节点的左子节点)的旋转
同样,在右旋转之前检查左子节点的因子。如果左子节点“右重”的话,先对它进行左旋转,再实施原来的右旋转。
# AVL树的实现:rebalance代码
def rebalance(self, node):
if node.balance_factor < 0: # 右重需要左旋
if node.right_child.balance_factor > 0:
# Do an LR Rotation
self.rotate_right(node.right_child) # 右子节点左重先右旋
self.rotate_left(node)
else:
# single left
self.rotate_left(node)
elif node.balance_factor > 0: # 左重需要右旋
if node.left_child.balance.factor < 0:
# Do an RL Rotation
self.rotate_left(node.left_child) # 左子节点右重先左旋
self.rotate_right(node)
else:
# single right
self.rotate_right(node)
# AVL树的实现:结语
经过复杂的put方法,AVL树始终维持平衡,get方法也始终保持O(log n)高性能。
不过,put方法的代价有多大?
将AVL树的put方法分为两个部分:
需要插入的新节点是叶节点,更新其所有父节点和祖先节点的代价最多为0(log n)
如果插入的新节点引发了不平衡,重新平衡最多需要'2次'旋转,但旋转的代价与问题规模无关,是常数0(1)。所以,整个put方法的时间复杂度还是O(log n)
# ADT Map的实现方法小结
我们采用了多种数据结构和算法来实现ADT Map,其时间复杂度数量级如下表所示:
如果对内存和计算时间要求不是特别高的话,使用散列表是比较合适的,接下来是AVL树的二叉查找的方式来进行。在python的内置数据类型当中,字典就是用散列表来实现的。
9.用AVL树来实现字典类型
# ---- 用AVL树实现字典类型 ----
# 用AVL树来实现字典类型,使得其put/get/in/del操作均达到对数性能
# 采用如下的类定义,至少实现下列的方法
# key至少支持整数、浮点数、字符串
# 请调用hash(key)来作为AVL树的节点key
# 【注意】涉及到输出的__str__, keys, values这些方法的输出次序是AVL树中序遍历次序
# 也就是按照hash(key)来排序的,这个跟Python 3.7中的dict输出次序不一样。
# 请在此编写你的代码
class TreeNode:
"""
二叉树节点
请自行完成节点内部的实现,并实现给出的接口
"""
def __init__(self, key, val=None): # 初始化方法
pass
def getLeft(self): # 获取左子树 (不存在时返回None)
pass
def getRight(self): # 获取右子树 (不存在时返回None)
pass
class mydict:
"""
以AVL树作为内部实现的字典
"""
def getRoot(self): # 返回内部的AVL树根
pass
def __init__(self): # 创建一个空字典
pass
def __setitem__(self, key, value): # 将key:value保存到字典
# md[key]=value
pass
def __getitem__(self, key): # 从字典中根据key获取value
# v = md[key]
# key在字典中不存在的话,请raise KeyError
pass
def __delitem__(self, key): # 删除字典中的key
# del md[key]
# key在字典中不存在的话,请raise KeyError
pass
def __len__(self): # 获取字典的长度
# l = len(md)
pass
def __contains__(self, key): # 判断字典中是否存在key
# k in md
pass
def clear(self): # 清除字典
pass
def __str__(self): # 输出字符串形式,参照内置dict类型,输出按照AVL树中序遍历次序
# 格式类似:{'name': 'sessdsa', 'hello': 'world'}
pass
__repr__ = __str__
def keys(self): # 返回所有的key,类型是列表,按照AVL树中序遍历次序
pass
def values(self): # 返回所有的value,类型是列表,按照AVL树中序遍历次序
pass
# 代码结束
#mydict=dict
# 检验
print("========= AVL树实现字典 =========")
md = mydict()
md['hello'] = 'world'
md['name'] = 'sessdsa'
print(md) # {'name': 'sessdsa', 'hello': 'world'}
for f in range(1000):
md[f**0.5] = f
for i in range(1000, 2000):
md[i] = i**2
print(len(md)) # 2002
print(md[2.0]) # 4
print(md[1000]) # 1000000
print(md['hello']) # world
print(20.0 in md) # True
print(99 in md) # False
del md['hello']
print('hello' in md) # False
for i in range(1000, 2000):
del md[i]
print(len(md)) # 1001
for f in range(1000):
del md[f**0.5]
print(len(md)) # 1
print(md.keys()) # ['name']
print(md.values()) # ['sessdsa']
for a in md.keys():
print(md[a]) # sessdsa
md.clear()
print(md) # {}
欧几里得算
# 使用欧几里得算法实现一个分数化简
class Fraction:
def __init__(self, a, b):
self.a = a
self.b = b
# self.x = self.gcd(a,b)
self.x = self.gcd(a, b)
self.a /= self.x
self.b /= self.x
# 求分子分母的最大公约数
def gcd(self, a, b):
while b > 0:
r = a % b
a = b
b = r
return a
求两分数的分母的最小公倍数
def zxgbs(self, a, b):
x = gcd(a, b)
return x * (a / x) * (b / x) # 最小公倍数
# 实现一个分数加法
def __add__(self, other):
a = self.a
b = self.b
c = other.a
d = other.b
fenmu = self.zxgbs(b, d)
fenzi = a * (fenmu / b) + c * (fenmu / d)
return Fraction(fenzi, fenmu)
def __str__(self):
return '%s/%s' % (int(self.a), int(self.b))
# 分数 18/12 化简
# f = Fraction(18,12)
# print(f)
# 分数3/4 + 5/6
a = Fraction(3, 4)
b = Fraction(5, 6)
print(a + b) # 19/12