数据结构与算法(python):树结构

参考自 MOOC数据结构与算法Python版

一、什么是树

树是一种基本的“非线性”数据结构。跟自然界中的树一样, 数据结构树也分为:根、 枝和叶等三个部分。一般数据结构的图示把根放在上方,叶放在下方。
分类树的三个特征:

  1. 分类体系是层次化的
  2. 一个节点的子节点与另一个节点的子节点相互之间是隔离、独立的
  3. 每一个叶节点都具有唯一性

1.1 树结构相关术语

术语说明
根Root树中唯一一个没有入边的节点
路径Path由边依次连接在一起的节点的有序列表
子节点Children入边均来自于同一个节点的若干节点, 称为这个节点的子节点
父节点Parent一个节点是其所有出边所连接节点的父节点
兄弟节点Sibling具有同一个父节点的节点之间称为兄弟节点
子树Subtree一个节点和其所有子孙节点, 以及相关边的集合
叶节点Leaf没有子节点的节点称为叶节点
层级Level从根节点开始到达一个节点的路径,所包含的边的数量, 称为这个节点的层级
高度树中所有节点的最大层级称为树的高度

1.2 树的定义

树由若干节点, 以及两两连接节点的边组成, 并有如下性质:

  • 其中一个节点被设定为根;
  • 每个节点n(除根节点),都恰连接一条来自节点p的边, p是n的父节点;
  • 每个节点从根开始的路径是唯一的,如果每个节点最多有两个子节点,这样的树称为“二叉树”
    在这里插入图片描述

二、树的实现

2.1 嵌套列表实现

递归的嵌套列表实现二叉树, 由具有3个元素的 List 列表实现:

  1. 第1个元素为根节点的值;
  2. 第2个元素是左子树(所以也是一个列表);
  3. 第3个元素是右子树(所以也是一个列表)。

[ r o o t , l e f t , r i g h t ] [root, left, right] [root,left,right]
【例】实现一个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”这几类,左括号就是表达式的开始,而右括号是表达式的结束。
( 3 + ( 4 ∗ 5 ) ) (3+(4*5)) (3+(45))分解为单词表
[ ′ ( ′ , ′ 3 ′ , ′ + ′ , ′ ( ′ , ′ 4 ′ , ′ ∗ ′ , ′ 5 ′ , ′ ) ′ , ′ ) ′ ] ['(', '3', '+', '(', '4', '*', '5',')', ')'] [(,3,+,(,4,,5,),)]

(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 利用表达式解析树求值

由前述对子表达式的描述,可从树的底层子树开始,逐步向上层求值(递归算法),最终得到整个表达式的值。
【递归三要素】

  1. **基本结束条件:**叶节点是最简单的子树,没有左右子节点,其根节点的数据项即为子表达式树的值;
  2. **缩小规模:**将表达式树分为左子树、右子树,即为缩小规模;
  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种遍历:

  1. 前序遍历(preorder):先访问根节点,再递归地前序访问左子树、最后前序访问右子树;(根左右)
    ABDGHCEIF
  2. 中序遍历(inorder):先递归地中序访问左子树,再访问根节点,最后中序访问右子树;(左根右)
    GDHBAEICF
  3. 后序遍历(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
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值