Python数据结构与算法分析
第六章 树
6.1 何谓树
- 树: 一种“非线性”数据结构
- 数据结构树分为:根、枝和叶
6.1.1特点
-
树的分类体系是层次化的
- 越接近顶部的层越普遍
- 越解决底部的层越独特
-
一个节点的子节点与另一个节点的子节点相互之间是隔离、独立的
-
每一个叶节点都具有唯一性
6.1.2树结构相关术语
-
节点 Node :组成树的基本部分
- 节点是树的基础部分。它可以有自己的名字,我们称作“键”。节点也可以带有附加信息,我们称作“有效载荷”
-
边 Edge: 边是树的另一个基础部分
- 。两个节点通过一条边相连,表示它们之间存在关系。除了根节点以外,其他每个节点都仅有一条入边,出边则可能有多条。
-
根节点 Root :根节点是树中唯一没有入边的节点。
-
路径 Path :由边连接的有序节点列表。
- 哺乳纲→食肉目→猫科→猫属→家猫就是一条路径。
-
子节点 Children :入边均来自于同一个节点的若干节点 (图中的 B C是根节点的子节点)
-
父节点 Parent :一个节点是其所有出边所连接节点的父节点 (图中的 B是DE的父节点 C是F的父节点)
-
兄弟点 Sibling :具有同一个父节点的节点之间称为兄弟节点 (图中B C是兄弟节点)
-
子树 Subtree :一个节点和其所有子孙节点,以及相关边的集合
-
叶子节点 Leaf :没有子节点的节点成为叶子节点 (图中D E F是叶子节点)
-
层级 Leval :从根节点开始到达一个节点的路径,所包含的边的数量称为这个节点的层级 (图中D层级为2 根节点层级为0)
-
高度 :树中所有节点的最大层级称为树的高度 (图中树的高度为2)
6.1.3 树的定义
-
树是由若干节点,以及两两连接节点的边组成,并有如下性质:
- 有一个根节点;
- 除根节点外,其他每个节点都与其唯一的父节点相连;
- 从根节点到其他每个节点都有且仅有一条路径;
- 如果每个节点最多有两个子节点,我们就称这样的树为二叉树。
-
一棵树要么为空,要么由一个根节点和零棵或多棵子树构成,子树本身也是一棵树
- 空集是树
- 每个子树的根到根节点具有边相连
6.2 实现树:嵌套列法
-
用Python List来实现二叉树数据结构;
-
递归的嵌套列表实现二叉树,由三个元素的列表来实现
- 第一个元素为根节点的值
- 第二个元素是左子树(列表)
- 第三个元素是右子树(列表)
-
辅助函数
- BinaryTree() 创建一个只有根节点二叉树
- insertLeft(val)/insertRight(val) 新建一棵二叉树,并将其作为当前节点的左/右子节点。
- setRootVal(val)getRootVal() 返回/获取当前节点存储的对象。
- getLeftChild()/getRightChild() 返回当前节点的左/右子树。
#树
## 嵌套列表实现树
"""0:根
1:左
2:右
"""
def BinaryTree(r):
return [r,[],[]] #只有根节点,左右子树为空
def insertLeft(root,newBranch): #插入左子树
t = root.pop(1) #把左子树弹出 放入变量t
if len(t) > 1: #有左子树
root.insert(1,[newBranch,t,[]]) #插入这个节点newbranch 作为左子树的根节点,原来节点t作为newBranch的左节点
else:#没有节点
root.insert(1,[newBranch,[],[]]) #做为左子树的根节点,无子节点
return root
def insertRight(root,newBranch):#插入右子树
t = root.pop(2)
if len(t) > 1: #若右子树上有节点
root.insert(2,[newBranch,[],t]) #插入这个节点newbranch 作为右子树的根节点,原来节点t作为newBranch的右节点
else:#没有节点
root.insert(2,[newBranch,[],[]]) #做为左右子树的根节点,无子节点
return root
def getRootVal(root): #获取根节点
return root[0]
def setRootVal(root,newVal): #设置根节点
root[0] = newVal
def getLeftChild(root):#获取左子树
return root[1]
def getRightChild(root):#获取右子树
return root[2]
r = BinaryTree(3) #创建一个根节点为3的二叉树
insertLeft(r,4) #3的左子树插入4
insertLeft(r,5) #左子树插入5 现在3的左子树的根节点为5 左:3~5~4
insertRight(r,6) #3的右子树插入6
insertRight(r,7)#右子树插入7 现在3的右子树的根节点为7 右:3~7~6
l = getLeftChild(r) #获取左子树
print(l)
setRootVal(l,9) #把l的左子树的根节点替换为9 左:3~9~4
print(r)
insertLeft(l,11) #把l的左子树插入11
print(r)
print(getRightChild(getRightChild(r))) #先获取r的右子树 7~6 再获取以7为根节点的右子树6
[5, [4, [], []], []]
[3, [9, [4, [], []], []], [7, [], [6, [], []]]]
[3, [9, [11, [4, [], []], []], []], [7, [], [6, [], []]]]
[6, [], []]
6.3 树的链表实现
-
用节点链接法实现树
- 每个节点保存根节点的数据项,以及指向左右子树的链接
-
定义一个BinaryTree 类
- 成员key保存根节点数据项
- 成员leftchild/rightchild 保存指向左/右子树的引用
## 链表实现树
class BinaryTree:
def __init__(self,rootObj): #初始化只有根节点的二叉树
self.key = rootObj #根节点
self.leftchild = None
self.rightchild = None
def insertLeft(self,newNode):
if self.leftchild == None:
self.leftchild = BinaryTree(newNode) #左子树为空,根的左节点指向以binarytree生成的以newNode为根的二叉树
else:
t = BinaryTree(newNode) #用binarytree生成一颗以newnode为根节点的二叉树t
t.leftchild = self.leftchild #新t左子树指向给原self左子树
self.leftchild = t #把self根的左子树指向t
def insertRight(self,newNode):
if self.rightchild == None:
self.rightchild = BinaryTree(newNode) #根节点右子树为空,根的右节点指向以binarytree生成的以newNode为根的二叉树
else:
t = BinaryTree(newNode)
t.rightchild = self.rightchild
self.rightchild = t
def getRightChild(self):
return self.rightchild
def getLeftChild(self):
return self.leftchild
def setRootVal(self,obj):
self.key = obj
def getRootVal(self):
return self.key
r = BinaryTree("a")
r.insertLeft("b")
r.insertRight("c")
r.getRightChild().setRootVal("hello") #把右子树的根改为hello
r.getLeftChild().insertRight("d") #在左子树b后插入一个右子树d
6.4 二叉树的应用
6.4.1 表达式解析
语法分析树:主谓宾,定状补
表达式树:((7+5)*(5-2))
树中每个子树都表示一个子表达式
本节研究问题
-
① 如何根据完全括号表达式构建解析树
-
全括号表达式要分解成单词列表
- 左括号( 表达式的开始,右括号) 表达式的结束
-
规则:
- 如果当前标记是左括号(,就为当前节点添加一个左子节点,并下沉至该左子节点;
- 如果当前标记在列表['+', '-', '/', '*']中,就将当前节点的值设为当前标记对应的运算符;为当前节点添加一个右子节点,并下沉至右子节点;
- 如果当前标记是数字,就将当前节点的值设为这个数并返回至父节点;
- 如果当前标记是右括号),就跳到当前节点的父节点。
过程:
(a) 创建一棵空树(灰色)。
(b) 读入 ( 。子树的开始,为根节点添加一个左子节点,当前节点(灰色)下降左子节点。
(c) 读入 3 。3为操作数,将当前节点的值设为 3,并回到父节点。
(d) 读入 + 。将当前节点的值设为+(根节点),添加一个右子节点,当前节点下降到右子节点。
(e) 读入(。为当前节点添加一个左子节点,当前节点下降左子节点。
(f) 读入 4。将当前节点的值设为 4,当前节点回到父节点。
(g) 读入*。将当前节点的值设为*,并添加右子节点。新节点成为当前节点。
(h) 读入 5。将当前节点的值设为 5,并回到父节点。
(i) 读入)。将*的父节点作为当前节点。
(j) 读入。根据规则 4,将+的父节点作为当前节点。因为+没有父节点,所以工作完成。
解析完全表达式解析树
-
创建树过程中关键在于对当前节点的跟踪
- 创建左右子树,调用 insertRight/insertLeft
- 当前节点设置值,调用serRootVal
- 下降到左右子树,调用getLeft/getRight
-
用栈Stack来跟踪父节点
- 节点下降,下降前节点用push入栈
- 节点上升到父节点,上升用pop出栈
from part_3 import Stack
def bulidParseTree(fpexp):
fplist = fpexp.split() #划分表达式
pStack = Stack() #建立空栈
eTree = BinaryTree('') #创建空树
pStack.push(eTree) #空树入栈
currentTree = eTree #当前节点
for i in fplist :#遍历表达式
if i == "(": #当前标记是左括号(,当前节点添加一个左子节点,当前节点并下沉至该左子节点
currentTree.insertLeft("") #添加左子节点
pStack.push(currentTree) #当前节点入栈
currentTree = currentTree.getLeftChild() #下沉至左子节点
elif i not in ['+','-','*','/',')']: #操作数 将当前节点的值设为这个数i,并返回至父节点 上升 出栈
currentTree.setRootVal(int(i)) #将当前节点的值设为这个数i
pStack.pop(currentTree) #弹出当前节点
currentTree = parent #当前节点为父节点
elif i in ['+','-','*','/',')']: #将当前节点的值设为当前标记对应的运算符;为当前节点添加一个右子节,并下沉至右子节点;
currentTree.setRootVal(i)
currentTree.insertRight("") #添加右子节点
pStack.push(currentTree) #节点下降
currentTree = currentTree.getRightChild() #当前节点下降到右子节点
elif i == ")": #右括号 就跳到当前节点的父节点 上升。
currentTree = pStack.pop() #上升到父节点
else:
raise ValueError
return eTree
-
** ② 如何计算解析树中的表达式**
-
求值递归函数evaluate
- 从树的底层子树开始逐步向上层求值,最终得到整个表达式的值
- 基本结束条件:叶节点没有左右子树,则根节点的数据项为子表达树的值
- 减小规模:将表达式划分为左右子树
- 调用自身:分别调用evaluate计算左右子树的值,再将左右子树的值依据根节点的操作符进行计算,得表达式的解
-
from part_3 import Stack
import operator
def bulidParseTree(fpexp):
fplist = fpexp.split() #划分表达式
pStack = Stack() #建立空栈
eTree = BinaryTree('') #创建空树
pStack.push(eTree) #空树入栈
currentTree = eTree #当前节点
for i in fplist :#遍历表达式
if i == "(": #当前标记是左括号(,当前节点添加一个左子节点,当前节点并下沉至该左子节点
currentTree.insertLeft("") #添加左子节点
pStack.push(currentTree) #当前节点入栈
currentTree = currentTree.getLeftChild() #下沉至左子节点
elif i not in ['+','-','*','/',')']: #操作数 将当前节点的值设为这个数i,并返回至父节点 上升 出栈
currentTree.setRootVal(int(i)) #将当前节点的值设为这个数i
pStack.pop(currentTree) #弹出当前节点
currentTree = parent #当前节点为父节点
elif i in ['+','-','*','/',')']: #将当前节点的值设为当前标记对应的运算符;为当前节点添加一个右子节,并下沉至右子节点;
currentTree.setRootVal(i)
currentTree.insertRight("") #添加右子节点
pStack.push(currentTree) #节点下降
currentTree = currentTree.getRightChild() #当前节点下降到右子节点
elif i == ")": #右括号 就跳到当前节点的父节点 上升。
currentTree = pStack.pop() #上升到父节点
else:
raise ValueError
return eTree
def evaluate(parseTree):
opers = {'+':operator.add,
'-':operator.sub,
'*':operator.mul,
'/':operator.truediv} #操作符字典 操作树到实际操作函数的映射
leftC = parseTree.getLeftChild() #缩小规模:划分左右子树
rightC = parseTree.getRightChild()
if leftC in rightC : #存在左右子树
fn = opers[parseTree.getRootVal()] #获取操作符
return fn(evaluate(leftC),evaluate(rightC)) #运算左右子树
else:#没有左右子树 基本情况 退出
return parseTree.getRootVal()
6.5 树的遍历
- 前序遍历:根左右
- 中序遍历:左根右
- 后序遍历:左右根
前序:书->第一章->1.1节->1.2节->1.2.1节->1.2.2节->第二章->2.1节->2.2节->2.2.1节->2.2.2节
#树的遍历
def preorder(tree): #前序
if tree:
print(tree.getRootVal()) #根
preorder(tree.getLeftChild())#左
preorder(tree.getRightChild())#右
def inorder(tree) :#中序
if tree != None:
inorder(tree.getLeftChild()) #左
print(inorder(tree.getRootVal()))#根
inorder(tree.getRightChild())#右
def postorder(tree): #后序
if tree != None:
postorder(tree.getLeftChild())#左
postorder(tree.getRightChild())#右
print(tree.getRootVal())#根
-
如何将解析树还原成最初的数学表达式
- 后序遍历
def postordereval(tree):
opers = {'+':operator.add,
'-':operator.sub,
'*':operator.mul,
'/':operator.truediv} #操作符字典 操作树到实际操作函数的映射
res1 = None
res2 = None
if tree:
res1 = postorder(tree.getLeftChild()) #获取左子树操作树
res2 = postorder(tree.getRightChild())# 获取右子树操作数
if res1 and res2:#存在左右子树
return opers[tree.getRootVal()](res1,res2) #获取根节点对应的操作符 对 左右操作数进行运算 返回数值
else :
return tree.getRootVal
-
全括号中缀表达式
- 中序遍历
#中序表达式:生成全括号中缀表达式
def printexp(tree):
sVal = ""
if tree:
sVal = "("+printexp(tree.getLeftChild())
sVal = sVal + str(tree.getRootVal())
sVal = sVal + printexp(tree.getRightChild()+")")
return sVal
6.6 利用二叉堆实现优先级队列(FIFO)
-
队列有一个重要的变体,叫作优先级队列
- 优先级队列从头部移除元素,不过元素的逻辑顺序是由优先级决定的。优先级最高的元素在最前,优先级最低的元素在最后。
6.6.1 利用二叉堆实现优先级队列
- 二叉堆逻辑结构是二叉树,利用非嵌套的列表实现
- 最小Key排在队首为‘最小堆’,最大Key在队首为‘最大堆’
二叉堆操作
-
BinaryHeap() 新建一个空的二叉堆。
-
insert(k) 往堆中加入一个新元素。
-
findMin() 返回最小的元素,元素留在堆中。
-
delMin() 返回最小的元素,并将该元素从堆中移除。
-
isEmpty() 在堆为空时返回 True,否则返回 False。
-
size() 返回堆中元素的个数。
-
buildHeap(list) 根据一个key列表创建堆。
用非嵌套列表实现二叉堆
-
采用二叉树结构使二叉堆保持在对数水平(二叉树的平衡:左右子树拥有相同数量的节点(满二叉树))
-
采用完全二叉树近似实现平衡
- 完全二叉树:在完全二叉树中,除了最底层,其他每一层的节点都是满的。在最底层,我们从左往右填充节点,都连续集中在最左边。
如果节点的下标为P,左节点下标为2P,右节点下标为2P+1,其父节点下标为P//2
-
堆次序:任何一个节点X,其父节点P中的key均小于x中的key;
- 对于复合‘堆’性质的二叉树,其中任何一条路劲,均是一个已排序数列,根节点Key最小
二叉堆操作的实现
-
二叉堆初始化
-
采用一个列表来保存堆数据,其中表收下标为0的项无用。
-
class BinHeap:
def __init__(self):
self.heaplist = [] #空列表保存堆数据
self.currentSize = 0 #下标为0
-
insert(key)
- 为了保持完全二叉树结构,。如果新元素小于其父元素,就将二者交换。
def perUp(self,i):
while i //2 > 0:#根节点不为0
if self.heaplist[i] < self.heaplist[i//2]:#当前下标i对应的key 小于 父节点的key
temp = self.heaplist[i//2]
self.heaplist[i//2] = self.heaplist[i]
self.heaplist[i] = temp #与父节点进行交换
i = i//2 #沿着路径向上
def insert(self,k):
self.heaplist.append(k) #添加末尾
self.currentSize = self.currentSize+1 #列表长度加1
self.perUp(self.currentSize) #判断插入值 平衡完全二叉树
-
delMin()方法
- 移走整个堆最小的Key:根节点heapList[1],在保持平衡完全树的前提下
- 将新节点沿着一条路径“下沉”,直到比两个子节点都小(选择左右节点中较小的下沉)
def percDown(self,i): #下沉
while (i*2) <= self.currentSize: #左节点小于总列表长度 存在左子树
mc = self.minChild(i) #mc 为子节点的最小值
if self.heaplist[i] > self.heaplist[mc]: #如果输入的i 比最小值mc 大 需要交换
tmp = self.heaplist[i]
self.heaplist[i] = self.heaplist[mc]
self.heaplist[mc] = tmp
i = mc #沿着路径下层
def minChild (self,i):
if i*2 +1 > self.currentSize:#不存在右子树
return i*2 #直接返回左子树
else:
if self.heaplist[i*2] < self.heaplist[2*i+1]: #左节点的值小于右节点的值
return i*2 #左节点下标
else:
return 2*i+1 #有节点下标
def delMin(self):
retval = self.heaplist[1]#移走堆顶节点
self.heaplist[1] = self.heaplist[self.currentSize] #把二叉树最后一个节点移动到堆顶节点
self.currentSize = self.currentSize-1 #堆列表长度-1
self.heaplist.pop() #把最后一个数值弹出
self.percDown(1) #新移动到根节点的数值比较下沉
return retval #返回被移除的原堆顶节点
-
bulidHeap(lst)方法
-
从无序表生成“堆”
-
使用“下层”法,让总代价控制在O(n)
- 叶子节点不需要下沉,对叶子节点的父节点进行下沉
-
def buildHeap(self,lst):#用无序表生成堆
i = len(lst)//2 #用叶子节点的父节点做i 进行下沉操作
self.currentSize = len(lst)
self.heaplist = [0] + lst[:] #插入0
print(len(self.heaplist),i)
while (i>0):#存在父节点
print(self.heaplist,i)
self.percDown(i) #下沉
i = i-1 #向上移动到父子节点
print(self.heaplist,i)
6.7 二叉搜索树
ADT MAP
-
Map() 新建一个空映射。
-
put(key, val) 往映射中加入一个新的键–值对。如果键已经存在,就用新值替换旧值。
-
get(key) 返回 key 对应值。如果 key 不存在,则返回 None。
-
del 通过 del map[key]这样的语句从映射中删除键–值对。
-
len() 返回映射中存储的键–值对的数目。
-
in 通过 key in map 这样的语句,在键存在时返回 True,否则返回 False。
二叉搜索树的性质
- 比父节点小的都出现在左子树,比父节点大的出现在右子树
构造二叉搜索树 BST
[70,31,93,94,14,23,73]
首先70 成为 树根
- 31<70 ,放入左子树
- 93>70 ,放入右子树
- 94>70 ,放入右子树;94>93,放入父节点为93的右子树
- 14<70 ,放入左子树;14<31,放入父节点为31的左子树
- 23<70 ,放入左子树;23<31&23>14,放入父节点为14的右子树
- 73>70 ,放入右子树;73<93,放入父节点为93的左子树
实现二叉搜索树
- 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__()
- 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 hasLeftChild(self): #是否有左子节点
return self.leftChild
def hasRightChild(self):#是否有右子节点
return self.rightChild
def isLeftChild(self):#是否是其父节点左子节点
return self.parent and self.parent.leftChild == self
def isRightChild(self):#是否是其父节点右子节点
return self.parent and self.parent.rightChild == self
def isRoot(self):#是否是根节点
return not self.parent
def Leaf(self): #是否是叶节点
return not (self.leftChild or self.leftChild)
def hasAnyChildren(self):# 是否有子节点 (任何)
return self.rightChild or self.leftChild
def hasBothChildren(self):#是否同时拥有左右节点
return self.rightChild and self.leftChild
def replaceNodeData(self,key,val,lc,rc): #把左右父节点都更换
self.key = key
self.pyload = val
self.leftChild = lc
self.rightChild = rc
if self.hasLeftChild():
self.leftChild.parent = self
else:
self.rightChild.parent = self
-
put(key,val) 方法:插入key构建BST
- 首先判断二叉搜索树是否为空,是,让key成为根节点root
- 否则,调用递归函数_put(key,val,root) 来放置key
-
_put(key,val,currentNode) 辅助方法
-
如果key<currentNode,那么key插入到左子树
- 没有左子树,key就成为左子节点
-
如果key>currentNode,那么key插入到右子树
- 没有右子树,key就成为右子节点
-
def put(self,key,val):
if self.root:#存在根节点
self._put(key,val,self.root) #调用递归函数
else:
self.root = TreeNode(key,val)#让key成为根节点root
self.size = self.size + 1 #树的长度+1
def _put(self,key,val,currentNode):
if key < currentNode.key: #输入key小于当前key
if currentNode.hasLeftChild() :#当前key存在左子树
self._put(key,val,currentNode.leftChild) #key插入到当前节点左子树
else: #没有左子树
currentNode.leftChild = TreeNode(key,val,parent = currentNode) #key成为左子节点
else : #输入key大于当前key
if currentNode.hasRightChild():#当前key存在右子树
self._put(key,val,currentNode.rightChild)#key插入到当前节点右子树
else:
currentNode.rightChild = TreeNode(key,val,parent= currentNode) ##key成为右子节点
def __setitem__(self, key, value): #索引赋值
self.put(key,value)
BST.put的图示:currentNode的变化(灰色)
-
插入key=19
- currentNode从树根17开始
- 19>17 插入到右子树,curentNode移动到35
- 19<35 插入到左子树,currentNode 移动到29
- 19<29 插入到左子树,但29无左子节点,19作为29的新左子节点
- get(key) 通过key所在节点找到payload方法
def get(self,key):
if self.root:#存在树
res = self._get(key,self.root) #找到节点
if res: #找到节点res
return res.pyload
else:
return None
else:#空树
return None
def _get(self,key,currentNode):
if not currentNode:
return None
elif currentNode.key == key: #currentNode等于要找的Key
return currentNode
elif key < currentNode.key: #小于
return self._get(key,currentNode.leftChild) #在左子树上继续找
else:
return self._get(key,currentNode.rightChild) #在右子树上继续找
def __setitem__(self, key, value): #索引取值
return self.get(key)
def __contains__(self, key):
if self._get(key,self.root):
return True
else:
return False
__iter__迭代器:使用for迭代,实际上是递归函数yield对每次迭代的返回值,中序遍历的迭代
def __iter__(self):#迭代器
if self:
if self.hasLeftChild():
for elem in self.leftChild:
yield elem
yield self.key
if self.hasRightChild():
for elem in self.rightChild:
yield elem
-
delete(key) :删除节点
- 用_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 #树的长度-1
else:
raise KeyError("Error,key not in tree")
elif self.size ==1 and self.root.key == key:#树长度为1,且需查找值 与 根节点对应值相等
self.root = None #直接设置根节点为空
self.size = self.size - 1
else:
raise KeyError('Error, key not in tree')
-
delitem(self, key) :删除节点的特殊方法
- 实现del myZipTree['PKU']的语句操作
def __delitem__(self, key):#删除节点的特殊方法
self.delete(key)
BST.remove() :在删除节点后,还需要保证BST结构的性质
- 被删除节点无子节点 :直接删除
def remove(self,currentNode): #移除节点
"""被删除是叶子节点,没有子节点"""
if currentNode.isLeaf():#需要删除的节点为叶子节点,直接删除
if currentNode == currentNode.parent.leftChild: #如果被删除节点== 当前节点父节点的左子节点
currentNode.parent.leftChild = None #设置为空删除
else:
currentNode.parent.rightChild = None #左节点删除
elif currentNode.hasBothChild():
succ = currentNode.findSuccessor()
succ.spiceOut()
currentNode.key = succ.key
currentNode.payload = succ.pyload
- 有1个子节点:将这个唯一的子节点上移,替换被删除节点的位置
"""被删除节点有一个子节点 :左子节点"""
#1. 被删除的节点为左子节点
if currentNode.hasLeftChild():#被删除节点有唯一左子节点
if currentNode.isLeftChild():#且被删除节点是左节点
currentNode.leftChild.parent = currentNode.parent #被删除节点左子节点的父节点 变为 被删除节点的父节点
currentNode.parent.leftChild = currentNode.leftChild #被删除节点的父节点的左节点,变为被删除的左节点
#2.被删除的节点为右子节点 且有左子节点
elif currentNode.isRightChild():#被删除节点是右节点 同理
currentNode.rightChild.parent = currentNode.parent
currentNode.parent.rightChild = currentNode.rightChild
#3.被删除节点为根节点
else:
currentNode.replaceNodeData(currentNode.leftChild.key,
currentNode.leftChild.pyload,
currentNode.leftChild.leftChild,
currentNode.leftChild.rightChild)
#右子节点
else: #被删除节点是右子节点
if currentNode.isLeftChild(): #且被删除节点是左节点
currentNode.rightChild.parent = currentNode.parent
currentNode.parent.leftChild = currentNode.rightChild
elif currentNode.isRightChild(): #被删除节点存在右节点 同理
currentNode.rightChild.parent = currentNode.parent
currentNode.parent.rightChild = currentNode.rightChild
else:#被删除节点为根节点
currentNode.replaceNodeData(currentNode.rightChild.key,
currentNode.rightChild.payload,
currentNode.rightChild.leftChild,
currentNode.rightChild.rightChild)
-
有2个子节点
-
找到下一个Key节点,被删节点右子树中最小的值称为后继节点
- 如果节点有右子节点,那么后继节点就是右子树中最小的节点。
- 如果节点没有右子节点,并且其本身是父节点的左子节点,那么后继节点就是父节点。
- 如果节点是父节点的右子节点,并且其本身没有右子节点,那么后继节点就是除其本身外父节点的后继节点。
-
def findSuccessor(self):#寻找后继节点
succ = None
if self.hasRightChild():#如果节点有右子节点
succ = succ.rightChild().fidMin()#那么后继节点就是右子树中最小的节点
else: #目前不会遇到
if self.parent:
if self.isLeftChild():
succ = self.parent
else:
self.parent.rightChild = None
succ = self.parent.findSucceesor()
self.parent.rightChild = self
return succ
def findMin(self):
current = self
while current.hasRightChild(): #当前节点有右子节点 右子节点作为根 current
current = current.leftChild #找current的左子树 找到最左下的节点 返回
return current
def spliceOut(self): #摘除节点
if self.isLeaf(): #后继节点为叶节点,直接摘除
if self.isLeftChild():
self.parent.leftChild = None
else:
self.parent.rightChild = None
elif self.hasAnyChildren(): #目前不会遇到
if self.hasLeftChild():
if self.isLeftChild():
self.parent.leftChild = self.leftChild
else:
self.parent.rightChild = self.leftChild
self.leftChild.parent = self.parent
else:#后继节点有右子节点
if self.isLeftChild(): #如果后继节点本身左子节点
self.parent.leftChild = self.rightChild #把后继节点的左子节点挂到后继节点的父节点的左子节点上
else:
self.parent.rightChild = self.rightChild #目前不会遇到
self.rightChild.parent = self.parent
#被删除节点有两个个子节点
if currentNode.hasBothChildren():
succ = currentNode.findSuccessor() #找到被删除节点的后继节点
succ.spliceOut() #把后继节点提出
currentNode.key = succ.key
currentNode.payload = succ.payload
二叉搜索树算法分析(以put为例)
-
限制其性能的因素是二叉树的高度,高度收到数据项Key插入顺序的影响
- 如果key的列表是随机分布,大于小于根节点Key的值都大致相等
- BST的高度是log2n(n是节点个数)为平衡树
- put最差性能为O(log2n)
6.8 平衡二叉搜索树 AVL树
AVL树 能自动维持平衡
AVL 树实现映射抽象数据类型的方式与普通的二叉搜索树一样,唯一的差别就是性能。
实现AVL 树要记录每个节点的平衡因子。根据每个节点左右子树的高度来实现这将平衡因子,定义为左右子树的高度差。
balanceFactor = height(left SubTree)-height(right SubTree)
- 如果平衡因子>0,称为"左重 Left-heavy";<0,称为"右重 Right-heavy"
- 平衡因子 = 0 ,则称为平衡
如果在一个二叉搜索树中每一个节点的平衡因子都在(-1,0,1)之间,则称为平衡树
- 如果在操作过程中,有节点平衡因子超出范围,在保持BST性质的前提下,如何重新平衡
AVL树的性能
-
最不平衡的左重树
-
总节点:
- h = 1,N=1
- h = 2,N=2=1+1
- h = 3,N=4=1+2+1
- h = 4,N=7=1+2+3+1
-
-
类似 斐波那契数列
- 定义斐波那契数列Fi,利用Fi重写Ni
- 斐波那契数列性质:Fi/Fi-1趋向黄金分割,可写出Fi的通式
- 将Fi通式代入Nh中,得到Nh的通式
- 移项,两边以 2 为底取对数,求h
- 在任何时间,AVL 树的高度都等于节点数取对数再乘以一个常数(1.44)。对于搜索 AVL 树来说,这是一件好事,因为时间复杂度被限制为O (log N ) 。
6.8.1 AVL树的Python实现
-
作为BST,新key以叶节点插入AVL树中
-
叶节点平衡因子为0,无需重新平衡,但为影响父节点的平衡因子
- 作为左节点插入,父节点平衡因子+1
- 作为右节点插入,父节点平衡因子-1
-
这重影响会一直传递到根节点,或者某个父节点的平衡因子被调整为0,不再影响到上层平衡因子为止。
- 无论从-1,1,0,都不会改变子树的高度
-
### AVL树的实现
def _put(self, key, val, currentNode):
if key < currentNode.key: #输入key小于当前key
if currentNode.hasLeftChild() :#当前key存在左子树
self._put(key,val,currentNode.leftChild) #key插入到当前节点左子树
else: #没有左子树
currentNode.leftChild = TreeNode(key,val,parent = currentNode) #key成为左子节点
self.updateBalance(currentNode.leftChild) #调整因子
else : #输入key大于当前key
if currentNode.hasRightChild():#当前key存在右子树
self._put(key,val,currentNode.rightChild)#key插入到当前节点右子树
else:
currentNode.rightChild = TreeNode(key,val,parent= currentNode) ##key成为右子节点
self.updateBalance(currentNode.rightChild) #调整因子
def updateBalance(self,Node):
if Node.balanceFactor > 1 or Node.balanceFactor < -1: #平衡因子在(-1~1)之间
self.rebalance(Node) #重新平衡
return
if Node.parent != None:#存在父节点
if Node.isLeftChild():#插入的节点为左子节点 其父节点平衡因子+1
Node.parent.balanceFactor += 1
elif Node.isRightChild():#插入的节点为右子节点 其父节点平衡因子-1
Node.parent.balanceFactor -= 1
if Node.parent.balanceFactor != 0: #查看父节点平衡因子是否为0 不是0着调整父子节点平衡因子
self.updateBalance(Node.parent)
AVL树的Python实现:rebalance的重新平衡
-
右重树,根节点的平衡因子是-2 :做左旋
- 将右子节点(节点 B)提升为子树的根节点。
- 将旧根节点(节点 A)作为新根节点的左子节点。
- 若新根节点(B)原来右左子节点,则将此节点设为A的右子节点(A的右子节点一定为空)
-
左重树,根节点的平衡因子是2 :做右旋
- 将左子节点(节点 C)提升为子树的根节点。
- 将旧根节点(节点 E)作为新根节点的右子节点。
- 如果新根节点(节点 C)已经有一个右子节点(节点 D),将其作为新右子节点(节点 E)的左子节点。
## 左旋
def rotateLeft(self,rotRoot): #rotRoot 旧根
newRoot = rotRoot.rightChld #newRoot 新根为旧根的右子节点
rotRoot.rightChld = newRoot.leftChild #旧根的右子节点指向 新根的左子节点
if newRoot.leftChild.parent != None:#存在newRoot
newRoot.leftChild.parent = rotRoot #将新根的左子节点指向旧根
newRoot.leftChild = rotRoot.parent
if rotRoot.isRoot():#旧节点是树根
self.root = newRoot #确立新树根
else: #否则
if rotRoot.isLeftChild(): #如果旧节点是为左节点
rotRoot.parent.leftChild = newRoot #
else:#如果是右子节点
rotRoot.parent.rightChild = newRoot
newRoot.leftChild = rotRoot #新节点的左子节点指向旧节点
rotRoot.parent = newRoot #旧节点的父节点指向新节点
rotRoot.balanceFactor = rotRoot.balanceFactor +\
1 - min(newRoot.balanceFactor,0)
newRoot.balanceFactor = newRoot.balanceFactor + \
1 + max(rotRoot.balanceFactor,0)
##右旋
def rotateRight(self,rotRoot):#rotRoot旧根
newRoot = rotRoot.leftChild #newRoot 新根为旧根的左子节点
rotRoot.leftChild = newRoot.rightChild #旧根的左子节点指向 新根的右子节点
if newRoot.rightChild.parent != None:#存在新根且不为根节点
newRoot.rightChild.parent = rotRoot #将新根的右子节点指向旧根
newRoot.rightChild = rotRoot.parent #新根的右子节点为旧根的父节点
if rotRoot.isRoot():#旧节点为树根
self.root = newRoot #确立新树根
else:#否则
if rotRoot.isLeftChild():#如果就节点为左节点
rotRoot.parent.leftChild = newRoot
else:
rotRoot.parent.rightChild = newRoot
newRoot.rightChild = rotRoot #新节点的右子节点指向旧节点
rotRoot.balanceFactor = rotRoot.balanceFactor +\
1+max(newRoot.balanceFactor,0)
rotRoot.balanceFactor = newRoot.balanceFactor+\
1-min(rotRoot.balanceFactor,0)
左旋对平衡因子的影响
AVL树的实现:更复杂的情形
- 下图右重子树,单纯左旋无法实现平衡
-
解决办法:在左旋前检测右子节点的因子, 在右旋前检测左子节点的因子
- 右子节点左重,先右旋再左旋
- 左子节点右重,先左旋再右旋
def rebalance(self,node):
if node.balanceFactor < 0:
if node.rightChild.balanceFactor > 0:
self.rotateRight(node.rightChild)
self.rotateLeft(node)
else:
self.rotateLeft(node)
elif node.balanceFactor > 0:
if node.leftChild.balanceFactor < 0:
self.rotateLeft(node.leftChild)
self.rotateRight(node)
else:
self.rotateRight(node)
AVL树得实现:复杂度
-
put操作
- 因为新节点作为叶子节点插入,所以更新所有父节点的平衡因子最多需要log2n 次操作.每一层一次。如果树失衡了,恢复平衡最多需要旋转两次。每次旋转的时间复杂度是O(1) ,所以 put 操作的时间复杂度仍然是O(log2n)
-
get操作
- 通过维持树的平衡,可以保证 get 方法的时间复杂度为O(log2n)
总结
- 性能对比