数据结构-树
树是一种一对多的数据结构,每一个数据有一个前置,但是可能有多个后置。如下图:
数据按照某种次序放在树上,会大大提升查找时的性能。比如对只有两个分支的树,第一层1个数据,第二层2个数据,第三层四个数据,第N层2n-1个数据。一千个数据要十层,一百万数据要20层。
这个对数级的查找,用有序的序列不是也一样吗?对。不过,序列的插入删除性能很低,因为要逐个移动数据。而用树形结构,插入删除性能也不错。
树之遍历
我们先手工构建一棵二叉树,然后在树上遍历所有元素。
先看树的结构怎么表达,一棵树,由节点组成,一个节点,有数据和左子树和右子树。我们这么表示:
class TreeNode(object):
def __init__(self,data=None):
self.left = None
self.right = None
self.data = data
left是左边的子树,right是右边的子树。可见树是一种递归型的定义。
对于如下的一棵树,我们可以这么构造:
root = TreeNode("a")
root.left = TreeNode("b")
root.right = TreeNode("c")
root.left.left = TreeNode("d")
root.left.right = TreeNode("e")
root.right.right = TreeNode("f")
现在我们手头有了一颗树,我们再看怎么遍历(Traversal)。
我们根据树的递归定义,从a开始,接下来是左树b,然后对b同样操作,到d,再到e,左树完成后,接着数右边的子树c,然后对c同样操作,直到f。
这是一种深度优先的搜索,分成本节点N,左子树L,右子树三部分R。所以遍历的次序可以组合成NLR,LNR和LRN三种。分别称为前序遍历、中序遍历和后序遍历(前中后是指本节点的次序)。
我们试一下下面的代码:
def preorder(node):
if node is None:
return
print(node.data)
preorder(node.left)
preorder(node.right)
def inorder(node):
if node is None:
return
inorder(node.left)
print(node.data)
inorder(node.right)
def postorder(node):
if node is None:
return
postorder(node.left)
postorder(node.right)
print(node.data)
print("------preorder--------")
preorder(root)
print("------inorder--------")
inorder(root)
print("------postorder--------")
postorder(root)
运行结果:
------preorder--------
a b d e c f
------inorder--------
d b e a c f
------postorder--------
d e b f c a
你们自己对着这棵树按照这个次序走一遍直观了解前中后序。我在这里只演示前序遍历。核心代码为下面几行,是递归调用:
print(node.data)
preorder(node.left)
preorder(node.right)
调用时,最先传入的是根节点a。程序先打印本节点,我们用红心标识它走过了,如图:
接下来,对a的左树进行递归。程序执行preorder(node.left),传入的是节点b。程序先打印本节点,我们用红心标识它走过了,如图:
接下来,对b的左树进行递归。程序执行preorder(node.left),传入的是节点d。程序先打印本节点,我们用红心标识它走过了,如图:
接下来,对d的左树进行递归。程序执行preorder(node.left),传入的是None,返回。然后对d的右树进行递归。程序再执行preorder(node.right),传入的是None,返回。
这次返回到上层递归,回到了b节点。对b的右树进行递归。程序执行preorder(node.right),传入的是节点e。程序先打印本节点,我们用红心标识它走过了,如图:
接下来,对e的左树进行递归。程序执行preorder(node.left),传入的是None,返回。然后对e的右树进行递归。程序再执行preorder(node.right),传入的是None,返回。
这次返回到上层递归,回到了b节点。这个时候,b节点的左右树都处理完了。返回。
这次返回的上层递归是回到了a节点。对a的右树进行递归。程序执行preorder(node.right),传入的是节点c。程序先打印本节点,我们用红心标识它走过了,如图:
接下来,对c的左树进行递归。程序执行preorder(node.left),传入的是None,返回。然后对c的右树进行递归。程序再执行preorder(node.right),传入的是f。程序先打印本节点,我们用红心标识它走过了,如图:
接下来,对f的左树进行递归。程序执行preorder(node.left),传入的是None,返回。然后对f的右树进行递归。程序再执行preorder(node.right),传入的是None,返回。
这次返回到上层递归,回到了c节点。这个时候,c节点的左右树都处理完了。返回。
这次返回到更上层的递归,回到了a节点。这个时候,a节点的左右树都处理完了。返回。
程序结束。
按照刚才的遍历路径,打印出a b d e c f。
还可以按照广度优先来遍历,从a开始,接下来是a的下一层b和c,这样第二层的节点走完了,就对b和c同样操作。这个程序不用到递归,而是用到一个队列,队列初始为空,第一步把树的根节点a放进队列里面,然后对整个队列里面的逐个元素执行循环:
从队列里面弹出第一个元素,打印,如果这个元素的节点有左子树,则把左子树的根节点放进队列末尾,再看如果这个元素的节点有右子树,则把右子树的根节点放进队列末尾。
一步步演示如下:
第一步放树根,也就是a节点,队列为:
a
弹出第一个元素,为节点a。这时队列为空。
打印a。然后看到a有左树也有右树,所以分别放进队列,如下:
b c
弹出第一个元素,为节点b。这时队列里面还有一个c节点。
打印b。然后看到b有左树也有右树,所以分别放进队列,如下:
c d e
弹出第一个元素,为节点c。这时队列里面还有两个节点d和e。
打印c。然后看到c没有左树但是有右树,所以把右树放进队列,如下:
d e f
弹出第一个元素,为节点d。这时队列里面还有两个节点e和f。
打印d。然后看到d没有左树也没有右树,什么也不做,如下:
e f
弹出第一个元素,为节点e。这时队列里面还有一个节点f。
打印e。然后看到e没有左树也没有右树,什么也不做,如下:
f
弹出第一个元素,为节点f。这时队列为空。
打印e。然后看到e没有左树也没有右树,什么也不做。
队列空了,程序结束。
代码如下:
def bft(node):
if node is None:
return
q=[]
q.append(node)
while len(q)>0:
node = q.pop(0)
print(node.data)
if not node.left is None:
q.append(node.left)
if not node.right is None:
q.append(node.right)
print("------bft--------")
bft(root)
运行结果为:
------bft--------
a b c d e f