树是一种分层结构,在日常生活中极为常用,越接近顶部的层越普遍,越接近底部的层越独特
树
术语介绍
节点Node: 组成树的基本部分每个节点具有名称,或“键值”节点还可以保存额外数据项,数据项根据不同的应用而变。
边Edge : 边是组成树的另一个基本部分每条边恰好连接两个节点,表示节点之间具有关联,边具有出入方向;
每个节点(除根节点)恰有一条来自另一节点的入边;
每个节点可以有多条连到其它节点的出边。
子节点Children: 入边均来自于同一个节点的若干节点,称为这个节点的子节点
父节点Parent : 一个节点是其所有出边所连接节点的父节点
兄弟节点Sibling: 具有同一个父节点的节点之间称为兄弟节点
子树Subtree: 一个节点和其所有子孙节点,以及相关边的集合
叶节点Leaf: 没有子节点的节点称为叶节点
层级Level: 从根节点开始到达一个节点的路径,所包含的边的数量,称为这个节点的层级。
高度: 树中所有节点的最大层级称为树的高度
树上面有一个树根,树根我们可以看作是子问题。它跟递归是天然的一个很搭配的一个数据结构。假设树根叫root,剩余的每一个点都有一个父亲结点(比如H的父节点是D),以及D结点的子结点是H和I,树还可以有子树sub-tree(比如B有DHI和EJ两个子树),子结点也分为左节点和右节点(比如C的左节点是F,右节点是G),以及兄弟结点(比如FG互为兄弟)。一棵树通常有N层,我们可以把根叫作第0层,也可以叫第1层,不同的教程定义不一样,但是从根往后,每一层层数加1,数一共有N层,则称这棵树的高度为N
树的定义
树由若干节点以及两两连接节点的边组成,并有如下性质:
- 其中一个节点被设定为根;
- 每个节点n(除根节点),都恰连接一条来自节点p的边,p是n的父节点;
- 每个节点从根开始的路径是唯一的
对于树,我们还可以使用这样的定义:
- 树是空集;或者由根节点及0或多个子树构成(其中子树也是树),每个子树的根到根节点具有边相连。
二叉树
我们知道一棵树的结点所对应的孩子的数量是随机的,如果一棵树每一个结点的孩子数量都不超过2,我们把这样的树称为二叉树(binary tree)
像上图中的二叉树,每一个结点恰好都有两个孩子,最后一层都是叶子的二叉树我们称为满二叉树。
看上面这张图,图中根节点的左子树是满的,都是右子树最后一层是不满的,我们称这样的二叉树为完全二叉树。
完全二叉树由于其特殊性,可以用非嵌套列表,以简单的方式实现,具有很好性质;如果节点的下标为,那么其左子节点下标为,右子节点为,其父节点下标为
树的编程实现
嵌套列表实现二叉树
递归的嵌套列表实现二叉树,由具有3个元素的列表实现:
- 第1个元素为根节点的值;
- 第2个元素是左子树(所以也是一个列表);
- 第3个元素是右子树(所以也是一个列表)。
[root, left, right]
mytree = ['A', ['B', ['D', [], []], ['E', [], []]], ['C', ['F', [], []], []]]
嵌套列表法的优点
-
子树的结构与树相同,是一种递归数据结构,很容易扩展到多叉树,仅需要增加列表元素即可
我们通过定义一系列函数来辅助操作嵌套列表
- BinaryTree:创建仅有根节点的二叉树
def BinaryTree(r):
return [r, [], []]
- insertLeft/insertRight:将新节点插入树中作为其直接的左/右子节点
倘若原来的根节点已存在左/右子节点,那么原来的左/右子节点将作为新结点的左/右子节点
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
- get/setRootVal:则取得或返回根节点
def getRootVal(root):
return root[0]
def setRootVal(root, newVal):
root[0] = newVal
- getLeft/RightChild:返回左/右子树
def getLeftChild(root):
return root[1]
def getRightChild(root):
return root[2]
结点链接法实现树
每个节点保存根节点的数据项,以及指向左右子树的链接
定义一个BinaryTree类
- 成员key保存根节点数据项
- 成员left/rightChild则保存指向左/右子树的引用(同样是BinaryTree对象)
class BinaryTree:
def __init__(self, rootObj):
self.key= rootObj
self.leftChild, self.rightChild = None, None
同样的,我们可以定义一些方法来对BinaryTree进行操作
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
树的应用
语法解析
-
程序设计语言的编译
- 词法、语法检查
- 从语法树生成目标代码
-
自然语言处理
- 机器翻译、语义理解
表达式解析
表达式解析介绍
我们还可以将表达式表示为树结构
- 叶节点保存操作数,内部节点保存操作符
由于括号的存在,需要计算*的话,就必须先计算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 出栈的节点即可!
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 = parent.pop()
else: # 表达式错误
return ValueError
return eTree
求解析树的值
-
创建了表达式解析树,可用来进行求值
-
由于二叉树BinaryTree是一个递归数据结构,自然可以用递归算法来处理
-
求值递归函数evaluate
-
由前述对子表达式的描述,可从树的底层子树开始,逐步向上层求值,最终得到整个表达式的值
-
求值函数evaluate的递归3要素:
- 基本结束条件: 叶节点是最简单的子树,没有左右子节点,其根节点的数据项即为子表达式树的值
- 缩小规模: 将表达式树分为左子树、右子树,即为缩小规模
- 调用自身: 分别调用evaluate计算左子树和右子树的值,然后将左右子树的值依根节点的操作符进行计算,从而得到表达式的值
-
拓展知识:
通过上述创建解析树的代码我们可以看见,代码通过使用大量的if语句对表达式解析判别,可读性欠佳,为了增强代码的可读性,我们引入一个名为operator的模块,operator模块提供了一套与Python的内置运算符对应的高效率函数。
import operator
op = operator.add
print(op)
print(operator.add(1,2))
print(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.getRootVa1()]
# 递归调用
return fn(evaluate(leftC), evaluate(rightC))
else:
# 基本结束条件
return parseTree.getRootVal()
二分查找树(BST)
下面我们来试试用二叉查找树保存key , 实现key的快速搜索
相关操作
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都出现在右子树。
插入顺序不同,生成的二叉查找树也不同
二叉查找树的实现
需要用到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__()
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
put(key, val)方法
首先看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.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) # 递归右子树
setitem()
def __setitem__(self, k, v):
self.put(k, v)
get()
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)
getitem()和__contains__()
def __getitem__(self, key):
return self.get(key)
def __contains__(self, item):
if self._get(key, self.root):
return True
else:
return False
__iter__迭代器
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
迭代器函数中用了for迭代,实际上是递归函数yield是对每次迭代的返回值中序谝历的迭代
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
else:
raise KeyError('Error, key not in tree')
deliten
def __delitem__(self, key):
self.delete(key)
在delete中,最复杂的是找到key对应的节点之后的remove节点方法。保持BST的性质,分以下3种情形:
- 这个节点没有子节点
if currentNode.isLeaf(): # leaf
if currentNode == currentNode.parent.leftChild:
currentNode.parent.leftChild = None
else:
currentNode.parent.rightChild = None
-
这个节点有1个子节点
-
解决:将这个唯一的子节点上移,替换掉被删节,点的位置
- 被删节点的子节点是左?还是右子节点?
- 被删节点本身是其父节点的左?还是右子节点?
- 被删节点本身就是根节点?
-
else: # this node has one child
if currentNode.hasLeftChild(): # 左子节点
if currentNode.isLeftChild():
currentNode.IeftChild.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)
- 这个节点有2个子节点
这时无法简单地将某个子节点上移替换被删节点,但可以找到另一个合适的节点来替换被删节点,这个合适节点就是被删节点的下一个key值节点,即被删节点右子树中最小的那个,称为“后继”
elif currentNode.hasBothChildren(): # interior
succ = currentNode.findSuccessor()
succ.spliceOut()
currentNode.key = sucC.key
currentNode.payload = sucC.payload
寻找“后继”结点
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 splice0ut(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
二分查找招数算法分析
其性能决定因素在于二叉搜索树的高度,(最大层次) ,而其高度又受数据项key插入顺序的影响。如果key的列表是随机分布的话,那么大于和小于根节点key的键值大致相等BST的高度就是 ( n是节点的个数),而且,这样的树就是平衡树,put方法最差性能为。
二叉树的遍历
-
对一个数据集中的所有数据项进行访问的操作称为“遍历Traversal”
-
线性数据结构中,对其所有数据项的访问比较简单直接按照顺序依次进行即可
-
树的非线性特点,使得遍历操作较为复杂
我们按照对节点访问次序的不同来区树的遍历
前序遍历pre-order
前序遍历的顺序是根---左子树---右子树
为什么说树和递归很搭配呢?我们在做树的遍历时,我们可以对一个树进行拆分,上面这棵树我们就可以根据先序遍历的规则拆分成树根A、左子树(以B为树根的子树)、右子树(以C为树根的子树),各自又分别对左子树和右子树做先序遍历,中序遍历和后序遍历相似。
def preorder(tree):
if tree:
print (tree.getRootVal()) # 访问根节点
preorder(tree.getLeftChild())
preorder(tree.getRightChild())
中序遍历In-order
中序遍历的顺序是左子树---根---右子树
def inorder(tree):
if tree != None:
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()
后序遍历Post-order
后序遍历的顺序是左子树---右子树---根
def postorder(tree):
if tree != None:
postorder(tree.getLeftChild())
postorder(tree.getRightChild())
print(tree.getRootVal())
回顾前述的表达式解析树求值,实际上也是一个后序遍历的过程,下面我们采用后序遍历法重写表达式求值代码:
import operator
def postirdereval(tree):
opers = {'+':operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv}
res1 = None
res2 = None
if tree:
res1 = postirdereval(tree.getLeftChild()) # 左子树
res2 = postirdereval(tree.getRightChild()) # 右子树
if res1 and res2:
return opers[tree.getRootVal()](res1, res2) # 根节点
else:
return tree.getRootVal()
层次序
层次序无序使用递归
层次序就是根据数的层数从上往下遍历
总结
- 先序、中序、后序一般用递归来求
- 树的先序遍历又称树的深度优先遍历
- 层次序一般借助队列来求
- 树的层序遍历又称树的广度优先遍历
当队列不为空,我们就把队头出队作为根,队列为空时,换下一层放元素
while (队列不为空)
取对头
扩层
优先队列
“优先队列”是队列有一种变体
操作系统中执行关键任务的进程或用户特别指定进程在调度队列中靠前
优先队列的出队跟队列一样从队首出队;但在优先队列内部,数据项的次序却是由“优先级”来确定:
高优先级的数据项排在队首,而低优先级的数据项则排在后面。这样,优先队列的入队操作就比较复杂,需要将数据项根据其优先级尽量挤到队列前方。
思考:有什么方案可以用来实现优先队列?出队和入队的复杂度大概是多少?
二叉堆
二叉堆的定义
实现优先队列的经典方案是采用二叉堆数据结构,二叉堆能够将优先队列的入队和出队复杂度都保持在。二叉堆的有趣之处在于,其逻辑结构上像二叉树,却是用非嵌套的列表来实现的。最小key排在队首的称为“最小堆min heap”,反之,最大key排在队首的是“最大堆max heap”
二叉堆的操作
BinaryHeap():创建一一个空二叉堆对象;
insert(k):将新key加入到堆中;
findMin():返回堆中的最小项,最小项仍保留在堆中;
delMin():返回堆中的最小项,同时从堆中删除;
isEmpty():返回堆是否为空;
size():返回堆中key的个数;
buildHeap(list):从一个key列表创建新堆
二叉堆实现
为了使堆操作能保持在对数水平上,就必须采用二叉树结构;同样,如果要使操作始终保持在对数数量级上,就必须始终保持二叉树的 “平衡”
树根左右子树拥有相同数量的节点
为了保持树的“平衡”,我们采用完全二叉树的结构近似实现
堆次序
任何一个节点x ,其父节点p中的key均小于x中的key,这样,符合“堆”性质的二叉树,其中任何一条路径,均是一个已排序数列,根节点的key最小
二叉堆的实现
二叉堆初始化
采用一个列表来保存堆数据,其中表首下标为0的项无用,但为了后面代码可以用到简单的整数乘除法,仍保留它。
class BinHeap:
def __init__(self):
self.heapList = [0]
self.currentSize = 0
insert(key)方法
首先,为了保持“完全二叉树”的性质,新key应该添加到列表末尾。
新key加在列表末尾,显然无法保持“堆”次序,虽然对其它路径的次序没有影响,但对于其到根的路径可能破坏次序
需要将新key沿着路径来“上浮”到其正确位置
注意:新key的 “上浮”不会影响其它路径节点的“堆”次序
def percUp(self, i):
while i // 2 < 0:
if self.heapList[i] < self.heapList[i // 2]:
tmp = self.heapList[i // 2]
self.heapList[i // 2] = self.heapList[i] #父节点交换
self.heapList[i] = tmp
i = i // 2 # 沿路劲向上
def insert(self, k):
self.heapList.append(k) # 添加到末尾
self.currentSize = self.currentSize + 1
self.percUp(self.currentSize) # 结点上浮
delMin()方法
移走整个堆中最小的key:根节点heapList[1]为了保持“完全二叉树”的性质,只用最后一个节点来代替根节点
同样的,为了保持“堆”的次序,我们需要将新的根节点沿着一条路径“下沉”
“下沉”路径的选择:如果比子节点大,那么选择较小的子结点路径进行选择,可减少操作次数
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(se1f):
retval = self.heapList[1] # 移走堆顶
self.heapList[1] = self.heapList[self.currentSize]
self.currentSize = self.currentSize - 1
self.heapList.pop()
self.percDown(1) # 新顶下沉
return retval
buildHeap(lst)方法:从无序表生成“堆”
我们最自然的想法是:用insert(key)方法,将无序表中的数据项逐个insert到堆中,但这么做的总代价是其实,用“下沉”法,能够将总代价控制在
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)