树由若干节点,以及两两连接节点的边组成,并具有以下性质:
树是:
首先我们尝试用Python List来实现二叉树数据结构。
递归的嵌套列表实现二叉树,由具有3个元素的列表实现: [root, left, right]
对于二叉树,根是myTree[0],左子树myTree[1],右子树myTree[2]。
嵌套列表法的优点:子树的结构与树相同,是一种递归数据结构;很容易扩展到多叉树,仅需要增加列表元素即可。
嵌套列表法代码:
def BinaryTree(r): # 创建仅有根节点的二叉树
return [r, [], []]
def insertLeft(root,newBranch): # 将新节点插入树中作为其直接的左子节点
t = root.pop(1)
if len(t) > 1:
root.insert(1,[newBranch,t,[]])
else:
root.insert(1,[newBranch,[],[]])
return root
def insertRight(root,newBranch): # 将新节点插入树中作为其直接的右子节点
t = root.pop(2)
if len(t) > 1:
root.insert(2,[newBranch,[],t])
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]
代码运行示例:
每个节点保存根节点的数据项,以及指向左右子树的链接。
节点链接法代码:
class BinaryTree:
def __init__(self,rootObj):
self.key = rootObj # 成员key保存根节点数据项
self.leftChild = None # 成员leftChild保存指向左子树的引用
self.rightChild = None # 成员rightChild保存指向右子树的引用
def insertLeft(self,newNode):
if self.leftChild == None:
self.leftChild = BinaryTree(newNode)
else:
t = BinaryTree(newNode)
t.leftChild = self.leftChild
self.leftChild = t
def insertRight(self,newNode):
if self.rightChild == None:
self.rightChild = 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
代码运行示例:
从左到右扫描全括号表达式的每个单词,依据规则建立解析树。
如果当前单词是 “(”:为当前节点添加一个新节点作为其左子节点,当前节点下降为这个新节点;
如果当前单词是操作符 “+, -, *, /”:将当前节点的值设为此符号,为当前节点添加一个新节点作为其右子节点,当前节点下降为这个新节点;
如果当前单词是操作数:将当前节点的值设为此数,当前节点上升到父节点;
如果当前单词是 “)”:则当前节点上升到父节点。
创建树过程中关键的是对当前节点的跟踪,我们可以用一个栈来记录跟踪父节点。当前节点下降时,将下降前的节点push入栈;当前节点需要上升至父节点时,上升到pop出栈的节点即可。
建立表达式解析树代码实现:
def buildParseTree(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 ['+','-','*','/',')']: # 操作数
currentTree.setRootVal(int(i))
parent = pStack.pop() # 出栈上升
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
由于二叉树BinaryTree是一个递归数据结构,自然可以用递归算法来处理。
求值递归函数evaluate,可从树的底层子树开始,逐步向上层求值,最终得到整个表达式的值。
求值递归函数evaluate的递归三要素:
一个增加程序可读性的技巧:函数引用
import operator
op = operator.add
n = op(1,2)
表达式解析树求值代码实现:
import operator
def evaluate(parseTree):
opers = {'+':operator.add, '-':operator.sub,
'*':operator.mul, '/':operator.truediv}
# 缩小规模:
leftC = parseTree.getLeftChild()
rightC = parseTree.getRightChild()
if leftC and rightC:
fn = opers[parseTree.getRootVal()]
return fn(evaluate(leftC),evaluate(rightC)) # 递归调用
else:
return parseTree.getRootVal() # 基本结束条件
按照对节点访问次序的不同来区分3种遍历:
前序遍历的例子:
树的遍历:递归算法代码
# 树的遍历:递归算法代码
# 前序遍历:
def preorder(tree):
if tree:
print(tree.getRootVal())
preorder(tree.getLeftChild())
preorder(tree.getRightChild())
# 后序遍历:
def postorder(tree):
if tree:
postorder(tree.getLeftChild())
postorder(tree.getRightChild())
print(tree.getRootVal())
# 中序遍历:
def inorder(tree):
if tree:
inorder(tree.getLeftChild())
print(tree.getRootVal())
inorder(tree.getRightChild())
在BinaryTree类里实现前序遍历,需要加入子树是否为空的判断:
def preorder(self):
print(self.key)
if self.leftChild:
self.leftChild.preorder()
if self.rightChild:
self.rightChild.preorder()
后序遍历:表达式求值
# 后序遍历:表达式求值
def postordereval(tree):
opers = {'+':operator.add, '-':operator.sub,
'*':operator.mul, '/':operator.truediv}
res1 = None
res2 = None
if tree:
res1 = postordereval(tree.getLeftChild()) # 左子树
res2 = postordereval(tree.getRightChild()) # 右子树
if res1 and res2:
return opers[tree.getRootVal()](res1,res2) # 根节点
else:
return tree.getRootVal()
队列有一种变体称为“优先队列”。优先队列的出队跟队列一样从队首出队;
但在优先队列内部,数据项的次序却是由“优先级”来确定:
1. 高优先级的数据项排在队首,而低优先级的数据项排在后面。
2. 这样,优先队列的入队操作就比较复杂,需要将数据项根据其优先级尽量挤到队列前方。
为了使堆操作能保持在对数水平上,就必须采用二叉树结构。
同样,如果要使操作始终保持在对数数量级上,就必须始终保持二叉树的平衡,树根左右子树拥有相同数量的节点。
或者采用“完全二叉树”的结构来近似实现平衡。完全二叉树,叶节点最多只出现在最底层和次底层,而且最底层的叶节点都连续集中在最左边,每个内部节点都有两个子节点,最多可有1个节点例外。
完全二叉树由于其特殊性,可以用非嵌套列表以简单的方式实现,如下图:如果节点的下标为p,那么其左子节点下标为2p,右子节点为2p+1,其父节点下标为p//2。
任何一个节点x,其父节点p中的key均小于x中的key。这样,符合“堆”性质的二叉树,其中任何一条路径,均是一个已排序数列,根节点的key最小。
二叉堆初始化,采用一个列表来保存堆数据,其中表首下标为0的项无用,但为了后面代码可以用到简单的整数乘除法,仍保留它。
insert(key)方法:首先,为了保持完全二叉树的性质,新key应该添加到列表末尾。新key加在列表末尾,显然无法保持堆次序,虽然对其它路径的次序没有影响,但对于其到根的路径可能破坏次序。需要将新key沿着路径来“上浮”到其正确位置。注意:新key的上浮不会影响其他路径节点的堆次序。
delMin()方法:移走整个堆中最小的key,根节点heapList[1],为了保持完全二叉树的性质,只用最后一个节点来代替根节点。同样,这操作还是破坏了堆次序。解决方法:将新的根节点沿着一条路径下沉,直到比两个子节点都小。下沉的路径选择:如果比子节点大,那么选择较小的子节点交换下沉。
buildHeap(lst)方法:从无序表生成堆。用下沉法,能将总代价控制在O(n)。
代码实现:
# 二叉堆操作的实现
class BinHeap:
def __init__(self):
self.heapList = [0]
self.currentSize = 0
def percUp(self,i):
while i//2 > 0:
if self.heapList[i] < self.heapList[i//2]:
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
self.percUp(self.currentSize) # 新key上浮
def percDown(self,i):
while (i * 2) <= self.currentSize:
mc = self.minChild(i)
if self.heapList[i] > self.heapList[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[i*2+1]:
return i*2
else: # 返回较小的
return i*2+1
def delMin(self):
retval = self.heapList[1] # 移走堆顶
self.heapList[1] = self.heapList[self.currentSize]
self.currentSize = self.currentSize - 1
self.heapList.pop()
self.percDown(1) # 新顶下沉
return retval
def buildHeap(self,alist):
i = len(alist) // 2 # 从最后节点的父节点开始,因叶节点无需下沉
self.currentSize = len(alist)
self.heapList = [0] + alist[:]
print(len(self.heapList),i)
while (i > 0):
print(self.heapList,i)
self.percDown(i)
i = i-1
print(self.heapList,i)
put(key,val)方法:插入key构造BST。首先看BST是否为空,如果一个节点都没有,那么key成为根节点root,否则,就调用一个递归函数_put(key, val, root)来放置key。
_put(key,val,self.root)的流程:如果key比currentNode小,那么_put到左子树,但如果没有左子树,那么key就成为左子节点;如果key比currentNode大,那么_put到右子树,但如果没有右子树,那么key就成为右子节点。
BST.get方法:在树中找到key所在的节点取到payload。
TreeNode类中的__iter__迭代器,迭代器函数中用了for迭代,实际上是递归函数,yield是对每次迭代的返回值。
BST.delete方法:用_get找到要删除的节点,然后调用remove来删除,找不到则提示错误。
从BST中remove一个节点,还要求仍然保持BST的性质,分以下三种情况:这个节点没有子节点;这个节点有1个子节点;这个节点有2个子节点。
被删节点有1个子节点,解决方法:将这个唯一的子节点上移,替换掉被删节点的位置。
代码实现:
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__()
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,currentNode):
if key < currentNode.key:
if currentNode.hasLeftChild():
self._put(key,val,currentNode.leftChild) # 递归左子树
else:
currentNode.leftChild = TreeNode(key,val,parent=currentNode)
else:
if currentNode.hasRightChild():
self._put(key,val,currentNode.rightChild) # 递归右子树
else:
currentNode.rightChild = TreeNode(key,val,parent=currentNode)
def __setitem__(self, k, v):
self.put(k,v)
def get(self,key):
if self.root:
res = self._get(key,self.root) # 递归函数
if res:
return res.payload # 找到节点
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.leftChild)
else:
return self._get(key,currentNode.rightChild)
def __getitem__(self, key):
return self.get(key)
def __contains__(self, key):
if self._get(key,self.root):
return True
else:
return False
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
else:
raise KeyError('Error, key not in tree')
def __delitem__(self,key):
self.delete(key)
def remove(self,currentNode):
if currentNode.isLeaf(): #leaf
if currentNode == currentNode.parent.leftChild:
currentNode.parent.leftChild = None
else:
currentNode.parent.rightChild = None
elif currentNode.hasBothChildren(): #interior
succ = currentNode.findSuccessor()
succ.spliceOut()
currentNode.key = succ.key
currentNode.payload = succ.payload
else: # this node has one child
if currentNode.hasLeftChild():
if currentNode.isLeftChild(): # 左子节点删除
currentNode.leftChild.parent = currentNode.parent
currentNode.parent.leftChild = currentNode.leftChild
elif currentNode.isRightChild(): # 右子节点删除
currentNode.leftChild.parent = currentNode.parent
currentNode.parent.rightChild = currentNode.leftChild
else: # 根节点删除
currentNode.replaceNodeData(currentNode.leftChild.key,
currentNode.leftChild.payload,
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)
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 isLeaf(self):
return not (self.rightChild 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,value,lc,rc):
self.key = key
self.payload = value
self.leftChild = lc
self.rightChild = rc
if self.hasLeftChild():
self.leftChild.parent = self
if self.hasRightChild():
self.rightChild.parent = self
def findSuccessor(self):
succ = None
if self.hasRightChild():
succ = self.rightChild.findMin()
else:
if self.parent:
if self.isLeftChild():
succ = self.parent
else:
self.parent.rightChild = None
succ = self.parent.findSuccessor()
self.parent.rightChild = self
return succ
def findMin(self):
current = self
while current.hasLeftChild(): # 到左下角
current = current.leftChild
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
# 中序遍历的迭代:
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
AVL树能在key插入时一直保持平衡。AVL是发明者名字的缩写。
AVL树的实现中,需要对每个节点跟踪“平衡因子 balance factor”参数。
平衡因子是根据节点的左右子树的高度来定义的,确切地说,是左右子树的高度差:balanceFactor = height(leftSubTree) - height(rightSubTree)。如果平衡因子大于0,称为“左重left-heavy”,小于零称为“右重right-heavy”,平衡因子等于0,则称作平衡。
如果一个二叉查找树中每个节点的平衡因子都在-1, 0, 1之间,则把这个二叉搜索树称为平衡树。
在平衡树操作过程中,有节点的平衡因子超出此范围,则需要一个重新平衡的过程。
分析AVL树最差情况下的性能:
即平衡因子为1或者-1。下图为平衡因子为1的左重AVL树,树的高度从1开始,来看看问题规模(总节点数N)和比对次数(树的高度h)之间的关系如何?
观察上图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 + Nh-1 + Nh-2 ,观察这个通式,很接近斐波那契。
对上图打红色问号的式子存在疑问,两边同时取对数,等号左边应该是log(Nh + 1)才对啊,希望有大神看到能为我解答困惑!
首先,作为BST,新key必定以叶节点形式插入到AVL树中。
叶节点的平衡因子是0,其本身无需重新平衡,但会影响其父节点的平衡因子:作为左子节点插入,则父节点平衡因子会增加1;作为右子节点插入,则父节点平衡因子会减少1.
这种影响可能随着其父节点到根节点的路径一直传递上去,直到:传递到根节点为止;或者某个父节点的平衡因子被调整到0,不再影响上层节点的平衡因子为止。
rebalance重新平衡:
如上图,是一个右重子树A的左旋转(并保持BST性质)。将右子节点B提升为子树的根,将旧根节点A作为新根节点B的左子节点;如果新根节点B原来有左子节点,则将此节点设置为A的右子节点(A的右子节点一定有空)。
更复杂的情况:如下图左重的子树右旋转。旋转后,新根节点将旧根节点作为右子节点,但是新根节点原来已有右子节点,需要将原有的右子节点重新定位;原有的右子节点D改到旧根节点E的左子节点,同样,E的左子节点在旋转后一定有空。
AVL树算法代价:
推荐使用散列表和AVL树。
==================================================================
以上均为个人学习笔记总结,学习代码见week18
课程名称:数据结构与算法Python版_北京大学_中国大学MOOC(慕课)
课程主页: http://gis4g.pku.edu.cn/course/pythonds/