栈
栈(stacks)是一种只能通过访问其一端来实现数据存储与检索的线性数据结构,具有后进先出(last in first out,LIFO)的特征。
利用Python列表实现栈的数据结构。
stack通常的操作:
Stack() 建立一个空的栈对象
push() 把一个元素添加到栈的最顶层
pop() 删除栈最顶层的元素,并返回这个元素
peek() 返回最顶层的元素,并不删除它
isEmpty() 判断栈是否为空
size() 返回栈中元素的个数
class Stack: """模拟栈""" def __init__(self): self.items = [] def isEmpty(self): return len(self.items)==0 def push(self, item): self.items.append(item) def pop(self): return self.items.pop() def peek(self): if not self.isEmpty(): return self.items[len(self.items)-1] def size(self): return len(self.items)
s=Stack() #创建栈对象 print(s.isEmpty()) #打印栈是否为空 s.push(3) #进栈 s.push('DataB') print(s.peek()) #打印栈顶元素 s.push(True) print(s.size()) #打印栈的大小 print(s.isEmpty()) s.push(6.3) print(s.pop()) #出栈 print(s.pop()) print(s.size())
True DataB 3 False 6.3 True
队列
队列(queue)是一种具有先进先出特征的线性数据结构,元素的增加只能在一端进行,元素的删除只能在另一端进行。能够增加元素的队列一端称为队尾,可以删除元素的队列一端则称为队首。
利用Python列表实现队列的数据结构。
queue通常的操作:
Queue() 定义一个空队列,无参数,返回值是空队列。
enqueue(item) 在队列尾部加入一个数据项,参数是数据项,无返回值。
dequeue() 删除队列头部的数据项,不需要参数,返回值是被删除的数据,队列本身有变化。
isEmpty() 检测队列是否为空。无参数,返回布尔值。
size() 返回队列数据项的数量。无参数,返回一个整数。
head() 返回队首元素,但并不删除该元素。
tail() 返回队尾元素,但并不删除该元素。
class Queue: """模拟队列""" def __init__(self): self.items = [] def isEmpty(self): return self.items == [] def enqueue(self, item): self.items.insert(0,item) def dequeue(self): return self.items.pop() def size(self): return len(self.items) def head(self): if self.queue != [] : return self.queue[0] else : return None def tail(self): if self.queue != [] : return self.queue[-1] else : return None
q=Queue() q.isEmpty() q.enqueue('dog') q.enqueue(4) q=Queue() q.isEmpty() q.enqueue(4) q.enqueue('dog') q.enqueue(True)
from collections import deque queue = deque(["Eric", "John", "Michael"]) queue.append("Terry") # Terry arrives queue.append("Graham") # Graham arrives queue.popleft() # The first to arrive now leaves queue.popleft() # The second to arrive now leaves queue # Remaining queue in order of arrival
Eric' 'John' deque(['Michael', 'Terry', 'Graham'])
树
1)树结构
树是一种非线性的数据结构,具有非常高的层次性。利用树存储数据,能够使用共有元素进行存储,在很大程度上节约存储空间。
树首先有且只有一个根节点,另外有N个不相交的子集,每个子集都是一个子树。如果树中的所有节点的子节点数目不超过2个,则该树为一个二叉树,二叉树要么是空树,要么是有左右两个子树。二叉树是一个有序树,即使只有一个子树,也要区分该子树的左子树还是右子树。二叉树的存储方式有两种,一种是顺序存储,另一种是链式存储。顺序存储中采用一维数组的存储方式;链式存储中,采用链表的存储方式。在链式存储中,树节点包含数据域、左子链域和右子链域。
逻辑上,二叉树有五种基本形态,分别为:
(a)空树 (b)只有一个根节点的二叉树 (c)只有左子树的二叉树 (d)只有右子树的二叉树 (d)完全二叉树(除最后一层外,每一层上的节点数均达到最大值;在最后一层上只缺右边的若干节点)
图1 二叉树
满二叉树是一种特殊的完全二叉树,指除最后一层无任何子节点外,每一层上的所有节点都有两个子节点的二叉树。
方法一:“嵌套列表”表示简单树
在用嵌套列表表示树时,我们使用 Python 的列表来编写这些函数。虽然把界面写成列表的一系列方法与我们已实现其他的抽象数据类型有些不同,但这样做比较有意思,因为它为我们提供一个简单、可以直接查看的递归数据结构。在列表实现树时,我们将存储根节点作为列表的第一个元素的值。列表的第二个元素的本身是一个表示左子树的列表。这个列表的第三个元素表示在右子树的另一个列表。为了说明这个存储结构,让我们来看一个例子。图 1 展示出一个简单的树以及相应的列表实现。
图 2 简单树
myTree = ['a', #root ['b', #left subtree ['d' [], []], ['e' [], []] ], ['c', #right subtree ['f' [], []], [] ] ]
然后可以使用索引来访问列表的子树。树的根是myTree[0]
,根的左子树是myTree[1]
,和右子树是myTree[2]
。下面的代码说明了如何用列表创建简单树。一旦树被构建,我们可以访问根和左、右子树。嵌套列表法一个非常好的特性就是子树的结构与树相同,本身是递归的。子树具有根节点和两个表示叶节点的空列表。列表的另一个优点是它容易扩展到多叉树。在树不仅仅是一个二叉树的情况下,另一个子树只是另一个列表。
myTree = ['a', ['b', ['d',[],[]], ['e',[],[]] ], ['c', ['f',[],[]], []] ] print(myTree) print('left subtree = ', myTree[1]) print('root = ', myTree[0]) print('right subtree = ', myTree[2])
下面定义一些函数,使得像使用列表一样操作树。请注意,不是去定义一个二叉树类,将编写的函数将只是操作列表使之类似于树。
构建二叉树,一个根节点和两个空子节点的列表:
def BinaryTree(r): return [r, [], []]
左子树添加到树的根,我们需要插入一个新的列表到根列表的第二个位置。我们必须注意,如果列表中已经有值在第二个位置,我们需要跟踪它,将新节点插入树中作为其直接的左子节点。Listing 1 显示了插入左子节点:
Listing 1
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
的代码类似于insertLeft
,如Listing 2 中。
Listing 2
def insertRight(root,newBranch): t = root.pop(2) if len(t) > 1: root.insert(2,[newBranch,[],t]) else: root.insert(2,[newBranch,[],[]]) return root
为了完善树的实现(参见Listing3),让我们写几个用于获取和设置根值的函数,以及获得左边或右边子树的函数。
Listing 3
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]
完整的嵌套列表表示树的代码:
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) setRootVal(l,9) print(r) insertLeft(l,11) print(r) print(getRightChild(getRightChild(r)))
方法二:“节点和引用”表示简单树
在这种情况下,将定义具有根及左右子树属性的类。这种表示更紧密地结合了面向对象的方式。
使用节点和引用,该树的结构类似于图 2 所示。
图3 使用节点和引用表示简单树
首先定义树节点类Node,再定义该类的构造函数,并对应add()函数用于向二叉树中添加一个数据为data的节点(如果该二叉树为空,则将新节点作为测试的根节点,否则将该节点添加为左孩子节点或右孩子节点):
#树节点类,3个属性 class Node(object): def _init_(self, data = -1, lchild = None, rchild = None): self.data = data self.lchild = lchild self.rchild = rchild
#BinaryTree类 class BinaryTree(object):
#构造函数 def _init_(self): self.root = None() #root是Node对象,表示二叉树的根节点
#判断空否,空返回True,否则为False
def isEmpty(self):
return True if self.root.data == -1 else False
#大小
def length(self):
return len(self.queue)
#添加节点 def add(self, data): node = Node(data) #创建新节点 if self.isEmpty(): self.root = node else: tree_node = self.root queue = [] #以列表存储二叉树 queue.append(self.root) while queue: #遍历二叉树 tree_node = queue.pop(0) if tree_node.lchild == None: #如果当前节点的左子节点为空,则将新节点作为当前节点的左子节点 tree_node.lchild = node return elif tree_node.rchild ==None: #如果当前节点的右子节点为空,则将新节点作为当前节点的右子节点 tree_node.rchild = node
return else: queue.append(tree_node.lchild) queue.append(tree_node.rchild)
#先序遍历
def pre_order(self, start): #start是开始遍历的节点
node = start
if node == None: #如果当前节点为空,则返回
return
print node.data, #打印当前节点数据
if node.lchild == None and node.rchild == None: #如果当前节点的左右子树均为空,则返回
return
self.pre_order(node.lchild) #从当前节点的左子树开始先序遍历
self.pre_order(node.rchild) #从当前节点的右子树开始先序遍历
#中序遍历
def in_order(self, start): #start是开始遍历的节点
node = start
if node == None: #如果当前节点为空,则返回
return
self.in_order(node.lchild) #从当前节点的左子树开始中序遍历
print node.data, #打印当前节点数据
self.in_order(node.rchild) #从当前节点的右子树开始中序遍历
#后序遍历
def post_order(self, start): #start是开始遍历的节点
node = start
if node == None: #如果当前节点为空,则返回
return
self.in_order(node.lchild) #从当前节点的左子树开始后序遍历
self.in_order(node.rchild) #从当前节点的右子树开始后序遍历
print node.data, #打印当前节点数据
if __name__ == '__main__': arr = [] for i in range(10): arr.append(i) print arr tree = BinaryTree() for i in arr: tree.add(i) print 'pre prder:' tree.pre_order(tree.root) print '\nin order:' tree.in_order(tree.root) print '\npost order:' tree.post_order(tree.root)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] pre order: 0 1 3 7 8 4 9 2 5 6 in order: 7 3 8 1 9 4 0 5 2 6 post order: 7 8 3 9 4 1 5 6 2 0
![](https://i-blog.csdnimg.cn/blog_migrate/efc32c9ff6bcfc129f6b1afb65111442.png)
用简单的节点和引用的类定义如Listing 4 所示。左右子树引用的是其他二叉树的实例。例如,当我们插入一个新的左子节点到树上时,我们创建了二叉树的另一个实例,修改了根节点的self leftChild
使之指向新的树。
Listing 4
注意Listing 4 中,构造函数需要得到一些类型的对象存储在根中。就像可以在列表中存储喜欢的任何一种类型,树的根对象可以指向任何一种类型。对于之前的例子,是将存储节点设为根值的名称。使用节点和引用来表示图 2 中的树,将创建二叉树类的 6 个实例。
接下来需要构建的根节点以外的函数。为了添加左子节点,将创建一个新的二叉树,并设置根的左属性以指向这个新对象。insertLeft
的代码Listing 5 所示。
Listing 5
必须考虑两种情况进行插入。第一种情况是,没有左子节点。当没有左子节点时,将新节点添加即可。第二种情况的特征是,当前存在左子节点。在第二种情况下,我们插入一个节点并将之前的子节点降一级。第二种情况是由else
语句在Listing 5的第 4 行进行处理。
对于insertRight
的代码必须考虑一个对称的情况。要么没有右子节点,要么必须插入根和现有的右子节点之间。插入代码Listing 6 所示。
Listing 6
为了完成一个简单的二叉树数据结构的定义,写出访问(参见Listing 7)左右子节点和根值的方法。
Listing 7
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
既然已经有了所有创建和操作二叉树的方法,再进一步检查它的结构。把每一个节点比作一个简单的树的根,并添加节点 B 和 C 作为子节点。
下面的代码就是创建树,并存储一些键值,为左右子节点赋值。注意,左右子节点和根都是同一个二叉树类的不同对象。正如之前树的定义中说的,能够把一个二叉树的任何子节点当成二叉树来做处理。
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())
2)遍历二叉树
A.解析树
解析树常常用于真实世界的结构表示,例如句子或数学表达式。
图4 一个简单句的解析树
图 4显示了一个简单句的层级结构。将一个句子表示为一个树,能通过利用子树来处理句子中的每个独立的结构。
图5 ((7+3)*(5−2)) 的解析树
如图 5 所示,能将一个类似于 ((7+3)*(5−2)) 的数学表达式表示出一个解析树。我们已经研究过全括号表达式,那么我们怎样理解这个表达式呢?我们知道乘法比加或者减有着更高的优先级。因为括号的关系,我们在做乘法运算之前,需要先计算括号内的加法或者减法。树的层级结构帮我们理解了整个表达式的运算顺序。在计算最顶上的乘法运算前,我们先要计算子树中的加法和减法运算。左子树的加法运算结果为 10,右子树的减法运算结果为 3。利用树的层级结构,一旦我们计算出了子节点中表达式的结果,我们能够将整个子树用一个节点来替换。运用这个替换步骤,我们得到一个简单的树,如图 6 所示。
图 6 ((7+3)*(5−2)) 的化简后的解析树
下面将更加详细地研究解析树。尤其是:
- 怎样根据一个全括号数学表达式来建立其对应的解析树
- 怎样计算解析树中数学表达式的值
- 怎样根据一个解析树还原数学表达式
建立解析树的第一步,将表达式字符串分解成符号保存在列表里。这里有四种符号需要我们考虑:左括号,操作符和操作数。我们知道读到一个左括号时,我们将开始一个新的表达式,因此我们创建一个子树来对应这个新的表达式。相反,每当我们读到一个右括号,我们就得结束这个表达式。另外,操作数将成为叶节点和他们所属的操作符的子节点。最后,我们知道每个操作符都应该有一个左子节点和一个右子节点。通过上面的分析我们定义以下四条规则:
- 如果当前读入的字符是
'('
,添加一个新的节点作为当前节点的左子节点,并下降到左子节点处。 - 如果当前读入的字符在列表
['+', '-', '/', '*']
中,将当前节点的根值设置为当前读入的字符。添加一个新的节点作为当前节点的右子节点,并下降到右子节点处。 - 如果当前读入的字符是一个数字,将当前节点的根值设置为该数字,并返回到它的父节点。
- 如果当前读入的字符是’)’,返回当前节点的父节点。
在编写 Python 代码之前,让我们一起看一个上述的例子:
对于(3+(4*5))表达式,可分解为如下的字符列表:['(', '3', '+', '(', '4', '*', '5' ,')',')']
。一开始,我们从一个仅包括一个空的根节点的解析树开始。如图 7,该图说明了随着每个新的字符被读入后该解析树的内容和结构。
图 7 解析树结构的步骤图
观察图 7:
- 创建一个空的树。
- 读如
(
作为第一个字符,根据规则 1,创建一个新的节点作为当前节点的左子结点,并将当前节点变为这个新的子节点。- 读入
3
作为下一个字符。根据规则 3,将当前节点的根值赋值为3
然后返回当前节点的父节点。- 读入
+
作为下一个字符。根据规则 2,将当前节点的根值赋值为+
,然后添加一个新的节点作为其右子节点,并且将当前节点变为这个新的子节点。- 读入
(
作为下一个字符。根据规则 1,创建一个新的节点作为当前节点的左子结点,并将当前节点变为这个新的子节点。- 读入
4
作为下一个字符。根据规则 3,将当前节点的根值赋值为4
然后返回当前节点的父节点- 读入
*
作为下一个字符。根据规则 2,将当前节点的根值赋值为*
,然后添加一个新的节点作为其右子节点,并且将当前节点变为这个新的子节点。- 读入
5
作为下一个字符。根据规则 3,将当前节点的根值赋值为5
然后返回当前节点的父节点- 读入
)
作为下一个字符。根据规则 4,我们将当前节点变为当前节点*
的父节点。- 读入
)
作为下一个字符。根据规则 4,我们将当前节点变为当前节点+
的父节点,因为当前节点没有父节点,所以我们已经完成解析树的构建。
通过上面给出的例子,很明显我们需要跟踪当前节点和当前节点的父节点。树提供给我们一个获得子节点的方法——通过getLeftChild
和getRightChild
方法,但是我们怎么样来跟踪一个节点的父节点呢?一个简单的方法就是在我们遍历整个树的过程中利用栈跟踪父节点。当我们想要下降到当前节点的子节点时,我们先将当前节点压入栈。当我们想要返回当前节点的父节点时,我们从栈中弹出该父节点。
通过上述的规则,使用栈和二叉树来操作,我们现在编写函数来创建解析树。解析树生成函数的代码如下所示。
from pythonds.basic.stack import Stack from pythonds.trees.binaryTree import BinaryTree 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 pt = buildParseTree("( ( 10 + 5 ) * 3 )") pt.postorder() #defined and explained in the next section
这四条建立解析树的规则体现在四个if
从句,它们分别在第 11,15,19,24 行。如上面所说的,在这几处你都能看到规则的代码实现,并需要调用一些BinaryTree
和Stack
的方法。这个函数中唯一的错误检查是在else
语句中,一旦我们从列表中读入的字符不能辨认,我们就会报一个ValueError
的异常。现在我们已经建立了一个解析树,我们能用它来干什么呢?第一个例子,我们写一个函数来计算解析树的值,并返回该计算的数字结果。为了实现这个函数要利用树的层级结构。重新看一下图 5,回想一下我们能够将原始的树替换为简化后的树(图 6)。这提示我们写一个通过递归计算每个子树的值来计算整个解析树的值。
就像我们以前实现递归算法那样,我们将从基点来设计递归计算表达式值的函数。这个递归算法的自然基点是检查操作符是否为叶节点。在解析树中,叶节点总是操作数。因为数字变量如整数和浮点数不需要更多的操作,这个求值函数只需要简单地返回叶节点中存储的数字就可以。使函数走向基点的递归过程就是调用求值函数计算当前节点的左子树、右子树的值。递归调用使我们朝着叶节点,沿着树下降。
为了将两个递归调用的值整合在一起,我们只需简单地将存在父节点中的操作符应用到两个子节点返回的结果。在图 6 中,我们能看到两个子节点的值,分别为 10 和 3。对他们使用乘法运算得到最终结果 30。
递归求值函数的代码如 Listing1 所示,我们得到当前节点的左子节点、右子节点的参数。如果左右子节点的值都是 None,我们就能知道这个当前节点是一个叶节点。这个检查在第 7 行。如果当前节点不是一个叶节点,查找当前节点的操作符,并用到它左右孩子的返回值上。
为了实现这个算法,我们使用了字典,键值分别为'+','-','*'
和'/'
。存在字典里的值是 Python 的操作数模块中的函数。这个操作数模块为我们提供了很多常用函数的操作符。当我们在字典中查找一个操作符时,相应的操作数变量被取回。既然是函数,我们可以通过调用函数的方式来计算算式,如function(param1,param2)
。所以查找opers['+'](2,2)
就等价于operator.add(2,2)
。
Listing 1
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()
最后,我们将在图 7中创建的解析树上遍历求值。当我们第一次调用求值函数时,我们传递解析树参数parseTree
,作为整个树的根。然后我们获得左右子树的引用来确保它们一定存在。递归调用在第 9 行。我们从查看树根中的操作符开始,这是一个'+'
。这个'+'
操作符找到operator.add
函数调用,且有两个参数。通常对一个 Python 函数调用而言,Python 第一件做的事情就是计算传给函数的参数值。通过从左到右的求值过程,第一个递归调用从左边开始。在第一个递归调用中,求值函数用来计算左子树。我们发现这个节点没有左、右子树,所以我们在一个叶节点上。当我们在叶节点上时,我们仅仅是返回这个叶节点存储的数值作为求值函数的结果。因此我们返回整数 3。
现在,为了顶级调用operator.add
函数,我们计算好其中一个参数了,但我们还没有完。继续从左到右计算参数,现在递归调用求值函数用来计算根节点的右子节点。我们发现这个节点既有左节点又有右节点,所以我们查找这个节点中存储的操作符,是'*'
,然后调用这个操作数函数并将它的左右子节点作为函数的两个参数。此时再对它的两个节点调用函数,这时发现它的左右子节点是叶子,分别返回两个整数 4 和 5。求出这两个参数值后,我们返回operator.mul(4,5)
的值。此时,我们已经计算好了顶级操作符'+'
的两个操作数了,所有需要做的只是完成调用函数operator.add(3,20)
即可。这个结果就是整个表达式树 (3+(4*5)) 的值,这个值是 23。
B.树的遍历
二叉树是一种非线性结构,遍历(traversal
)二叉树无法通过简单的循环实现。遍历二叉树就是要让树中所有节点被且仅被访问一次,即按一定规律排列成一个线性队列。二叉树包含3个部分,即根节点、左子树和右子树。根据3个部分的访问次序对二叉树的遍历进行分类,可以将遍历二叉树的方法分为3种类型,分别为先序遍历(preorder
),中序遍历(inorder
)和后序遍历(postorder
)。我们来给出它们的详细定义,然后举例看看它们的应用。
- 先序遍历
在先序遍历中,我们先访问根节点,然后递归使用先序遍历访问左子树,再递归使用先序遍历访问右子树。 - 中序遍历
在中序遍历中,我们递归使用中序遍历访问左子树,然后访问根节点,最后再递归使用中序遍历访问右子树。 - 后序遍历
在后序遍历中,我们先递归使用后序遍历访问左子树和右子树,最后访问根节点。
当用树来表示一本书,对于先序遍历,书是树的根节点,每一章是根节点的子节点,每一节是章节的子节点,每一小节是每一章节的子节点,以此类推。图8 是一本书只取了两章的一部分。虽然遍历的算法适用于含有任意多子树的树结构,但我们目前为止只谈二叉树。
图8 用树结构来表示一本书
由于用递归来编写遍历,先序遍历的代码异常的简洁优雅。Listing 2 给出了一个二叉树的先序遍历的 Python 代码。
Listing 2
也可以把先序遍历作为BinaryTree
类中的内置方法,这部分代码如 Listing 3 所示。注意这一代码从外部移到内部所产生的变化。一般来说,只是将tree
换成了self
。但也要修改代码的基点。内置方法在递归进行先序遍历之前必须检查左右子树是否存在。
Listing 3
内置和外置方法哪种更好一些呢?一般来说preorder
作为一个外置方法比较好,原因是,我们很少是单纯地为了遍历而遍历,这个过程中总是要做点其他事情。
后序遍历的代码如 Listing 4 所示,它除了将print
语句移到末尾之外和先序遍历的代码几乎一样。
Listing 4
我们已经见过了后序遍历的一般应用,也就是通过表达式树求值。我们再来看 Listing 1,我们先求左子树的值,再求右子树的值,然后将它们利用根节点的运算连在一起。假设我们的二叉树只存储表达式树的数据。我们来改写求值函数并尽量模仿后序遍历的代码,如 Listing 5 所示。
Listing 5
我们发现 Listing 5 的形式和 Listing 4 是一样的,区别在于 Listing 4 中我们输出键值而在 Listing 5 中我们返回键值。这使我们可以通过第 6 行和第 7 行将递归得到的值存储起来。之后我们利用这些保存起来的值和第 9 行的运算符一起运算。
在这节的最后我们来看看中序遍历。在中序遍历中,我们先访问左子树,之后是根节点,最后访问右子树。 Listing 6 给出了中序遍历的代码。我们发现这三种遍历的函数代码只是调换了输出语句的位置而不改动递归语句。
Listing 6
def inorder(tree): if tree != None: inorder(tree.getLeftChild()) print(tree.getRootVal()) inorder(tree.getRightChild())
当我们对一个解析树作中序遍历时,得到表达式的原来形式,没有任何括号。我们尝试修改中序遍历的算法使我们得到全括号表达式。只要做如下修改:在递归访问左子树之前输出左括号,然后在访问右子树之后输出右括号。修改的代码见 Listing 7。
Listing 7
我们发现printexp
函数对每个数字也加了括号,这些括号显然没必要加。
链表
链表是一种非连续、非顺序的存储方式,链表由一系列节点组成,每个节点包括两部分,一部分为数据,另一部分是指向下一个节点的指针域。链表分单向链表、单向循环链表、双向链表、双向循环链表。
链表由一系列不必在内存中相连的结构构成,这些对象按线性顺序排序。每个结构含有表元素和指向后继元素的指针。最后一个单元的指针指向NULL。为了方便链表的删除与插入操作,可以为链表添加一个表头。
图9 具有表头的链表
删除操作可以通过修改一个指针来实现。
图10 链表删除元素
插入操作需要执行两次指针调整。
图11 链表插入元素
1. 单向链表的实现
1.1 Node实现
每个Node(链表节点类)分为两部分/属性。一部分/_item对应链表的元素,称为数据域;另一部分/_next为一指针,指向下一个Node。
class Node(): __slots__=['_item','_next'] #限定Node实例的属性 def __init__(self,item): self._item=item self._next=None #Node的指针部分默认指向None def getItem(self): return self._item def getNext(self): return self._next def setItem(self,newitem): self._item=newitem def setNext(self,newnext): self._next=newnext
1.2 SinglelinkedList的实现
单向链表类初始函数,_head属性指向链表头部,_size用于存储链表中节点的数量,初始化链表为空表。
1.3 检测链表是否为空
若_head属性为None,则返回True,否则返回False。
1.4 add在链表前端添加元素
首先创建一个节点temp,然后将temp节点的next属性指向当前链表的头部,最后将当前链表的头部指向temp节点。
1.5 append在链表尾部添加元素
1.6 search检索元素是否在链表中
1.7 index索引元素在链表中的位置
1.8 remove删除链表中的某项指定元素
1.9 insert链表中插入元素
全部代码