1、定义树结构
树结构和自然界中树类似也有根、枝、叶,但是计算机中树结构的根在顶部而叶在底部。
树结构并不少见,如生物学上的分类树、类Unix系统中的文件系统、html文件中的标签树等,从中可总结出树结构的一些特点:
- 树是分层的即树有层次结构
- 一个节点(node)的所有子节点(children)和另一个节点的子节点相互独立
- 每个叶节点(leaf node)都是独一无二的
- 移动一个子树(subtree)到树的其它位置,不影响更下层的层次结构(hierarchy)
到此,对树结构有一个简单认识后,来学习树结构涉及的基本术语:
节点(Node):节点是树的基本组成部分。也可称它为“键(key)”;节点也可以有附加信息,称为“负荷(payload)”。虽然负荷信息在树算法中不是核心,但在使用树结构的应用中却至关重要。
边(Edge):边是树的另一个基本组成部分。边连接两个节点,表明这两个节点存在联系。每个节点(除根节点)只有一条和其它节点相连的入边,每个节点可以有多个出边。
根节点(Root):根节点是树中唯一一个没有入边的节点。如类Unix中的‘/’
路径(Path):路径是由边连接起来的节点的有序表。
子节点(Children)、父节点(Parent)、兄弟节点(Sibling):节点A有四个出边,分别连接节点B、C、D、E。节点B、C、D、E的入边连接自相同的节点A,则节点A是节点B、C、D、E的父节点;节点B、C、D、E是节点A的子节点;节点B、C、D、E是兄弟节点。
子树(Subtree):子树是一个父节点和它所有的后代节点组成的节点和边的集合。
叶节点(Leaf Node):没有子节点的节点就是叶节点。
层数(Level):节点层数是指从根节点到该节点路径中的边树。如根节点层数是0.注:层数=节点树-1
高度(Height):树的高度=树中节点的最大层数
接着,将用两种方式定义树,第一种使用节点和边,第二种使用递归。
定义一:树是节点和连接节点的边的集合,有如下特征:
- 有一个根节点
- 除根节点外的每一个节点n,都通过一条边与另一个节点p相连,p是n的父节点
- 根节点到每一个节点的路径唯一
- 如果树中每个节点都最多有两个子节点。则该树是二叉树
定义二:A tree 要么为empty 要么包含a root 和0个或多个subtree,每个子树也是a tree。每个子树的root通过边和父树的root相连。
2、通过嵌套列表实现树
在列表实现树时,列表的第一个元素存储根节点的值,第二个元素是一个表示其左子树的列表,第三个值是表示其右子树的另一个列表。
上图是一个简单的树,如下是列表实现:
tree = ['a', #根节点 ['b', #左子树 ['d',[],[]], ['e',[],[]]], ['c', #右子树 ['f',[],[]], []] ]
从列表实现代码可看出嵌套列表的好处,
- 子树的结构与树的结构相同,这个结构自身就是递归的。有一个根植和两个空列表的子树是叶节点
- 它容易扩展到多叉树。在树不仅仅是二叉树的情况下,另一个子树只是另一个列表
现在,我们将构造一些函数,通过操作list使之能像树一样工作。
def BinaryTree(r): return [r,[],[]]
如上,构造了一个列表:一个根节点和两个表示子节点空列表。向树的根添加一个左子树需要在根列表的第二个位置insert一个新list。我们必须要注意,如果第二个位置已经有元素了,我们需要跟踪它并将其取出作为我们添加的列表的左子节点。
def insertLeft(root,newBranch): t = root.pop(1) if len(t) > 1: root.insert(1,[newBranch,t,[]]) else: root.insert(1,[newBranch,[],[]]) return root
注意插入左子节点前,我们首先应获取对应于当前左子节点的列表(可能为空)。然后,添加新的左子节点,将原来的左子节点作为新节点的左子节点。这是我们能将新节点插到树中的任意位置。insertRight与上面类似:
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 getRoot(root): return root[0] def setRootVal(root,newVal): root[0] = newVal def getLeftChild(root): return root[1] def getRightChild(root): return root[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 getRoot(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) print(r) insertLeft(r,4) print(r) insertLeft(r,5) print(r) insertRight(r,6) print(r) insertRight(r,7) print(r) left = getLeftChild(r) print(left) setRootVal(left,9) print(r) insertLeft(left,11) print(r) print(getRightChild(getRightChild(r)))
[3, [], []] [3, [4, [], []], []] [3, [5, [4, [], []], []], []] [3, [5, [4, [], []], []], [6, [], []]] [3, [5, [4, [], []], []], [7, [], [6, [], []]]] [5, [4, [], []], []] [3, [9, [4, [], []], []], [7, [], [6, [], []]]] [3, [9, [11, [4, [], []], []], []], [7, [], [6, [], []]]] [6, [], []]
3、节点和引用
第二种实现树的方式是节点和引用。我们定义具有根值和左右子树属性的类,它的结构类似于下图:
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') print(r.getRootVal()) print(r.getLeftChild()) r.insertLeft('b') print(r.getLeftChild()) print(r.getLeftChild().getRootVal()) r.insertRight('c') print(r.getRightChild()) print(r.getRightChild().getRootVal()) r.getRightChild().setRootVal('hello') print(r.getRightChild().getRootVal())
a None <__main__.BinaryTree object> b <__main__.BinaryTree object> c hello
4、解析树(parse tree)
如上图,将数学表达式((7+3)∗(5−2))表示成了解析树。树的层次结构能帮我们理解整个表达式的运算顺序。在运算顶层的乘法前,先要运算子树中的加法和减法,左子树加法运算结果是10,右子树减法运算结果是3. 利用树的层次结构,一旦我们计算出子节点中表达式的结果,我们就能够用一个节点替换整个子树。运用这种替换步骤,我们会得到一个简化的树,如下:
后面会更详细的讨论解析树,尤其是:
- 如何根据一个全括号数学表达式来建立对应的解析树
- 如何计算存在解析树中的数学表达式的值
- 如何根据解析树恢复出原先的数学表达式
建立解析树的第一步需要把表达式字符串分解成字符列表。一共有四种不同的字符:左括号、右括号、操作符、操作数。每当读到一个左括号就表示开始一个新的表达式,因此,我们应该创建一个新树来对应该表达式。相反,当读到右括号时表示结束一个表达式。操作数将作为叶节点并且是它们所属的操作符的子节点。最后,每个操作符都有一个左子节点和一个右子节点。通过分析后,我们定义四条如下规则:
- 若当前字符是‘(’,添加一个新节点作为作为当前节点的左子节点并且当前节点下降为左子节点
- 若当前字符在['+','-','/','*']中,将当前节点的根值设为当前字符。添加新节点作为当前节点的右子节点并且当前节点下降为右子节点
- 若当前字符是数字,将当前节点的根值设为该数字并且当前节点回退到父节点
- 若当前字符是‘)’,则回退到当前节点的父节点
以 (3+(4∗5))为例讲解,先转为字符列表['(', '3', '+',
'(', '4', '*', '5' ,')',')'] 从一个仅包括一个空的根节点的解析树开始:
- 创建一个空树
- 读入 ( 为首字符。根据规则1,创建一个新节点作为当前节点的左子节点,并将当前节点变为这个新的子节点
- 读入 3 为下一个字符。根据规则3,将当前节点的根值设为3并倒退至父节点
- 读入 + 为下一个字符。由规则2,将当前节点的根值设为+并添加一个新节点为右子节点。新的右子节点变为当前节点
- 读入 ( 为下一个字符。由1,创建一个新节点为当前节点的左子节点。新的左子节点变为当前节点
- 读入4为下一个字符。由3,将当前节点的根值设为4. 把4的父节点作为当前节点
- 读入*为下一个字符。由2,将当前节点的根值设为*并创建一个新的右子节点。新的右子节点变为当前节点
- 读入5为下一个字符。由3,将当前节点的根值设为5 把5的父节点作为当前节点
- 读入)为下一个字符。由4,把 * 的父节点作为当前节点
- 读入)为下一个字符。由4,把+的父节点作为当前节点。此时,+没有父节点,因此结束
注:因为操作都是在当前节点上操作,所以在每次操作结束后,都要设置好当前节点
从以上例子可知,在构建解析树过程中,我们需要保持对当前节点和当前节点的父节点的追踪。树的连接方式给我们提供了获得一个节点的子节点的方法---getLeftChild和getRightChild,但是怎样追踪一个节点的父节点?可以使用堆栈在遍历树的过程中保持对父节点的追踪。每当我们要下降到当前节点的子节点时,我们先将当前节点压入栈中。当要返回当前节点的父节点时,从栈中弹出父节点。
使用堆栈和二叉树构建解析树:
class Stack(object): def __init__(self): self.stack = [] def push(self,obj): self.stack.append(obj) def pop(self): return self.stack.pop() 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 def parseTree(strExp): splitL = strExp.split() stack = Stack() pTree = BinaryTree('') stack.push(pTree) currentTree = pTree for i in splitL: if i == '(': currentTree.insertLeft('') stack.push(currentTree) currentTree = currentTree.getLeftChild() elif i not in ['+','-','*','/',')']: currentTree.setRootVal(int(i)) currentTree = stack.pop() elif i in ['+','-','*','/']: currentTree.setRootVal(i) currentTree.insertRight('') stack.push(currentTree) currentTree = currentTree.getRightChild() elif i == ')': currentTree = stack.pop() else: raise ValueError return pTree pt = parseTree('( ( 10 + 5 ) * 3 )')
通过递归函数计算解析树表示的表达式的值:
import operator def evaluate(pTree): #传入解析树 op = {'+':operator.add,'-':operator.sub,'*':operator.mul,'/':operator.truediv} left = pTree.getLeftChild() #获取左子节点 right = pTree.getRightChild() #获取右子节点 if left and right: #如果不是叶节点即left和right不是操作数,执行该语句 fn = op.get(pTree.getRootVal()) #获取操作符 return fn(evaluate(left),evaluate(right)) #递归调用,直至叶节点 else: #left 和 right都是None,则为叶节点即操作数,并返回叶节点的根值 return pTree.getRootVal() print(evaluate(pt))