目录
- 一、什么是树
- 1.1 树结构相关术语
- 1.2 树的定义
- 二、树的实现
- 2.1 嵌套列表实现
- 2.2 链表实现
- 三、树的应用:表达式解析
- 3.1 解析树(语法树)
- 3.2 建立表达式解析树
- 3.3 利用表达式解析树求值
- 四、树的遍历 Tree Traversals
- 4.1 树的三种遍历
- 4.2 利用后序遍历进行表达式求值
- 4.3 利用中序遍历生成全括号中缀表达式
一、什么是树
树是一种基本的“非线性”数据结构。跟自然界中的树一样, 数据结构树也分为:根、 枝和叶等三个部分。一般数据结构的图示把根放在上方,叶放在下方。
分类树的三个特征:
- 分类体系是层次化的
- 一个节点的子节点与另一个节点的子节点相互之间是隔离、独立的
- 每一个叶节点都具有唯一性
1.1 树结构相关术语
术语 | 说明 |
根Root | 树中唯一一个没有入边的节点 |
路径Path | 由边依次连接在一起的节点的有序列表 |
子节点Children | 入边均来自于同一个节点的若干节点, 称为这个节点的子节点 |
父节点Parent | 一个节点是其所有出边所连接节点的父节点 |
兄弟节点Sibling | 具有同一个父节点的节点之间称为兄弟节点 |
子树Subtree | 一个节点和其所有子孙节点, 以及相关边的集合 |
叶节点Leaf | 没有子节点的节点称为叶节点 |
层级Level | 从根节点开始到达一个节点的路径,所包含的边的数量, 称为这个节点的层级 |
高度 | 树中所有节点的最大层级称为树的高度 |
1.2 树的定义
树由若干节点, 以及两两连接节点的边组成, 并有如下性质:
- 其中一个节点被设定为根;
- 每个节点n(除根节点),都恰连接一条来自节点p的边, p是n的父节点;
- 每个节点从根开始的路径是唯一的,如果每个节点最多有两个子节点,这样的树称为“二叉树”
二、树的实现
2.1 嵌套列表实现
递归的嵌套列表实现二叉树, 由具有3个元素的 List 列表实现:
- 第1个元素为根节点的值;
- 第2个元素是左子树(所以也是一个列表);
- 第3个元素是右子树(所以也是一个列表)。
【例】实现一个6节点的二叉树
根是myTree[0],左子树myTree[1],右子树myTree[2],子树的结构与树相同,是一种递归数据结构。
我们通过定义一系列函数来辅助操作嵌套列表
函数 | 含义 |
BinaryTree | 创建仅有根节点的二叉树 |
insertLeft/insertRight | 将新节点插入树中作为其直接的左/右子节点 |
get/setRootVal | 则取得或返回根节点 |
getLeft/RightChild | 返回左/右子树 |
【代码】:
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]
r = BinaryTree(3)
insertLeft(r,4)
insertLeft(r,5)
insertRight(r,6)
insertRight(r,7)
l = getLeftChild(r)
print(l) #[5, [4, [], []], []]
setRootVal(l,9)
print(r) #[3, [9, [4, [], []], []],[7, [], [6, [], []]]]
insertLeft(l,11)
print(r) #[3, [9, [11, [4, [], []], []], []], [7, [], [6, [], []]]]
print(getRightChild(getRightChild(r)))#[6, [], []]
图示结果如下:
2.2 链表实现
同样可以用节点链接法来实现树,每个节点保存根节点的数据项,以及指向左右子树的链接。
- 成员key保存根节点数据项
- 成员left/rightChild则保存指向左/右子树的引用(同样是BinaryTree对象)
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)
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
r = BinaryTree('a')
r.insertLeft('b')
r.insertRight('c')
r.getRightChild().setRootVal('hello')
r.getLeftChild().insertRight('d')
上述代码用图表示如下:
三、树的应用:表达式解析
3.1 解析树(语法树)
将树用于表示语言中句子, 可以分析句子的各种语法成分, 对句子的各种成分进行处理。
- 语法分析树
主谓宾,定状补 - 程序设计语言的编译
词法、语法检查
从语法树生成目标代码 - 自然语言处理
机器翻译、语义理解 - 我们还可以将表达式表示为树结构
叶节点保存操作数,内部节点保存操作符
由于括号的存在,需要计算*的话,就必须先计算7+3和5-2,表达式层次决定计算的优先级:越底层的表达式,优先级越高。树中每个子树都表示一个子表达式:
我们可以利用树结构实现如下功能: - 从全括号表达式构建表达式解析树
- 利用表达式解析树对表达式求值
- 从表达式解析树恢复原表达式的字符串形式
3.2 建立表达式解析树
(1) 全括号表达式要分解为单词Token列表
其单词分为括号“() ”、操作符“±*/”和操作数“0~9”这几类,左括号就是表达式的开始,而右括号是表达式的结束。
分解为单词表
(2)创建表达式解析树过程
创建空树,当前节点为根节点
读入’(’, 创建了左子节点,当前节点下降
读入’3’,当前节点设置为3, 上升到父节点
读入’+’,当前节点设置为+, 创建右子节点,当前节点下降
读入’(’, 创建左子节点,当前节点下降
读入’4’,当前节点设置为4, 上升到父节点
读入’’,当前节点设置为, 创建右子节点,当前节点下降
读入’5’,当前节点设置为5, 上升到父节点
读入’)’, 上升到父节点
读入’)’,再上升到父节点
总结:从左到右扫描全括号表达式的每个单词,依据规则建立解析树。
- 如果当前单词是"(":为当前节点添加一个新节点作为其左子节点, 当前节点下降为这个新节点;
- 如果当前单词是操作符"+,-,/,":将当前节点的值设为此符号,为当前节点添加一个新节点作为其右子节点, 当前节点下降为这个新节点;
- 如果当前单词是操作数:将当前节点的值设为此数, 当前节点上升到父节点;
- 如果当前单词是")":则当前节点上升到父节点。
从图示过程中我们看到, 创建树过程中关键的是对当前节点的跟踪:
- 创建左右子树可调用insertLeft/Right
- 当前节点设置值,可以调用setRootVal
- 下降到左右子树可调用getLeft/RightChild
但是, 上升到父节点,这个没有方法支持!我们可以用一个栈来记录跟踪父节点:
- 当前节点下降时,将下降前的节点push入栈
- 当前节点需要上升到父节点时,上升到pop出栈的节点即可!
【代码】:
def buildParseTree(fpexp):
flist = fpexp.split()
pStack = Stack()
eTree = BinaryTree('')
pStack.push(eTree) #入栈下降
for i in fplist:
if i == '(': #表达式开始
currentTree.insertLeft('')
pStack.push(currentTree) #入栈下降
currentTree = currrentTree.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
3.3 利用表达式解析树求值
由前述对子表达式的描述,可从树的底层子树开始,逐步向上层求值(递归算法),最终得到整个表达式的值。
【递归三要素】
- **基本结束条件:**叶节点是最简单的子树,没有左右子节点,其根节点的数据项即为子表达式树的值;
- **缩小规模:**将表达式树分为左子树、右子树,即为缩小规模;
- **调用自身:**分别调用evaluate计算左子树和右子树的值,然后将左右子树的值依根节点的操作符进行计算,从而得到表达式的值.
import operator
def evaluate(parseTree):
opers = {'+':operator.add, '-':operator.sub,'*':operator.mul,'/':operator.truediv}
#缩小规模
leftC = praseTree.getLeftChild()
rightC = parseTree.getRightChild()
if leftC and rightC:
fn = opers[parseTree.getRootVal()]
#递归调用
return fn(evaluate(leftC),evaluate(rightC))
else:
return parseTree.getRootVal() #基本结束条件
四、树的遍历 Tree Traversals
4.1 树的三种遍历
对一个数据集中的所有数据项进行访问的
操作称为“遍历Traversal”,线性数据结构中, 对其所有数据项的访问比较简单直接,按照顺序依次进行即可。树的非线性特点, 使得遍历操作较为复杂,我们按照对节点访问次序的不同来区分3种遍历:
- 前序遍历(preorder):先访问根节点,再递归地前序访问左子树、最后前序访问右子树;(根左右)
ABDGHCEIF - 中序遍历(inorder):先递归地中序访问左子树,再访问根节点,最后中序访问右子树;(左根右)
GDHBAEICF - 后序遍历(postorder):先递归地后序访问左子树,再后序访问右子树,最后访问根节点。(左右根)
GHDBIEFCA
【代码】
def preorder(tree):
if tree:
print(tree.getRootVal())
preorder(tree.getLeftChild())
preorder(tree.getRightChild())
def posorder(tree):
if tree !=None:
posorder(tree.getLeftChild())
posorder(tree.getRightChild())
print(tree.getRootVal())
def inorder(tree):
if tree !=None:
inorder(tree.getLeftChild())
print(tree.getRootVal())
inorder(tree.getRightChild())
4.2 利用后序遍历进行表达式求值
采用后序遍历法重写表达式求值代码
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() #基本结束条件
4.3 利用中序遍历生成全括号中缀表达式
下列代码中对每个数字也加了括号,请自行修改代码去除。
def printexp(tree):
sVal = ""
if tree:
sVal = '(' + printexp(tree.getLeftChild())
sVal += str(tree.getRoolVal())
sVal += printexp(tree.getRightChild()) +')'
return sVal