树结构在计算机领域使用十分广泛。在操作系统源程序中,树和森林被用来构造文件系统。我们看到的window和linux等文件管理系统都是树型结构。在编译系统中,如C编译器源代码中,二叉树的中序遍历形式被用来存放C 语言中的表达式。在游戏设计领域,许多棋类游戏的步骤都是按树型结构编写。这一篇我们就来了解下树,并实现一下最基本的二叉树。
树(tree),和我们熟悉的顺序表一样,也是一种抽象的数据结构。不过顺序表是依次首尾相连,而树代表的是一对多的对应关系。如下图所示,是Linux的文件系统的一部分,典型的树形结构
其结构有点像倒挂的树,根在上,叶在下。其具有如下几个特点
同时还有一些术语要了解下
root
节点的度为4上面说的是树的逻辑结构,下面看下物理存储结构。
顺序表存储
从根一直到页,逐层从左到右放进顺序表。例如上面的树就会如下存放
['root','etc','home','var','proc','issue','passwd'...]
链表存储
链表方式,每个节点除了存储自身的内容,还有存储其子节点的地址。这种方式比较普遍。
因为有发散关系,树的结构可以很复杂,我们只研究比较有规律的典型结构,二叉树。
二叉树(binary tree),的每个节点最多含有两个子树,例如下图
二叉树还有几种特殊形式
下面用代码实现二叉树,并完成添加节点,遍历节点两种操作。这里的添加结点采用从左至右,每层所有子节点添加完毕以后再添加下一层的方式。而遍历节点又会有深度优先和广度优先这两种区别。
下面我们依次来看。
采用链表方式存储的节点类如下
class Node:
"""binary tree node"""
def __init__(self, value, lchild=None, rchild=None):
self.value = value
self.lchild = lchild
self.rchild = rchild
每个节点除了存储自身的值,还通过self.lchild
和self.rchild
分别存储左节点和右节点的地址。默认地址都是None
,表示没有子节点。
树类在初始的时候只需要有一个根节点的地址即可
class Binary_Tree:
"""binary tree"""
def __init__(self, root=None):
self.root = root
默认不带参数的初始是一个空树。
添加节点采取从左至右每层依次添加的顺序,每层填满了再添加到下一层。
根据树的特性,这时候可以利用一个小技巧,就是利用队列的方式,首先把根节点放入队列,从列表中读取节点的时候就将该节点的子节点从左至右依次放入队列中,有点类似于前面说到的顺序表存储结构。一直读取到某个节点的子节点有一个或都不存在时,将新的节点做为该节点的子节点。
temp = [self.root]
while temp:
nextNode = temp.pop(0)
if nextNode.lchild is None:
nextNode.lchild = node
return
elif nextNode.rchild is None:
nextNode.rchild = node
return
else:
temp.append(nextNode.lchild)
temp.append(nextNode.rchild)
从临时列表的尾部放元素,从头部取元素,达到队列的效果。下面考虑下特殊情况,就是树为空的情况,直接把新的节点做为根节点即可,于是完整实现如下
def add(self, node):
if self.root is None:
self.root = node
return
temp = [self.root]
while temp:
nextNode = temp.pop(0)
if nextNode.lchild is None:
nextNode.lchild = node
return
elif nextNode.rchild is None:
nextNode.rchild = node
return
else:
temp.append(nextNode.lchild)
temp.append(nextNode.rchild)
在进行遍历的时候,如果是按照添加时候的顺序,从上至下从左至右依次读取,就叫做广度优先遍历。
思路和添加元素时候一样的,也是利用队列来完成,不过这时候要一直循环到队列为空。
def breadth_travel(self):
if self.root is None:
return
temp = [self.root]
while temp:
nextNode = temp.pop(0)
print(nextNode.value, end=' ')
if nextNode.lchild is None:
continue
elif nextNode.rchild is None:
temp.append(nextNode.lchild)
continue
else:
temp.append(nextNode.lchild)
temp.append(nextNode.rchild)
这里为了方便打印在同一行,使用了end=' '
。
深度优先遍历指的是先把根节点一边的子树遍历到最后一层,再去遍历另一边的子树。而在对子树进行遍历的时候采用递归的方式。
根据父节点在两个子节点间的位置分为三种情况:父节点-左子节点-右子节点的先序遍历,左子节点-父节点-右子节点的中序遍历,以及左子节点-右子节点-父节点的后序遍历。
使用递归来实现非常简单
def preorder(self):
self.__preorder(self.root)
def __preorder(self, node):
if node is None:
return
print(node.value, end=' ')
self.__preorder(node.lchild)
self.__preorder(node.rchild)
为了实现递归必须要传递一个根节点的地址进去,但是在进行遍历的时候不希望用户带参数进行操作,于是写了两个方法。
遍历过程以下面这张图为例进行说明
首先打印根节点1,然后是左子树。左子树同样先打印根节点2,再去处理下一层的左子树打印4,接着是8。之后是4这个子树的右子树9,然后是2这个子树的右子树5。然后是1这个根节点的右子树3,之后是3的左子树6,最后是7。所以理论的遍历结果会是[1,2,4,8,9,5,3,6,7]
,下面验证下
if __name__ == '__main__':
tree = Binary_Tree()
tree.add(Node(1))
tree.add(Node(2))
tree.add(Node(3))
tree.add(Node(4))
tree.add(Node(5))
tree.add(Node(6))
tree.add(Node(7))
tree.add(Node(8))
tree.add(Node(9))
tree.breadth_travel()
print('')
tree.preorder()
结果符合预期
1 2 3 4 5 6 7 8 9
1 2 4 8 9 5 3 6 7
中序遍历只是在修改下打印顺序即可
def inorder(self):
self.__inorder(self.root)
def __inorder(self, node):
if node is None:
return
self.__inorder(node.lchild)
print(node.value, end=' ')
self.__inorder(node.rchild)
遍历过程以下面这张图为例进行说明
首先处理根节点的左子树,而左子树也是先处理下一层子树,一直到8,然后是8的父节点4和右子节点9。之后是上一层的父节点2和右子节点5。然后是根节点1和右子树。右子树中也是先左子树6,然后是其父节点3和右子节点7。于是理论结果为[8,4,9,2,5,1,6,3,7]
验证下
if __name__ == '__main__':
tree = Binary_Tree()
tree.add(Node(1))
tree.add(Node(2))
tree.add(Node(3))
tree.add(Node(4))
tree.add(Node(5))
tree.add(Node(6))
tree.add(Node(7))
tree.add(Node(8))
tree.add(Node(9))
tree.breadth_travel()
print('')
tree.preorder()
print('')
tree.inorder()
结果符合预期
1 2 3 4 5 6 7 8 9
1 2 4 8 9 5 3 6 7
8 4 9 2 5 1 6 3 7
后续排列就不分析了,直接给出代码和结果
def postorder(self):
self.__postorder(self.root)
def __postorder(self, node):
if node is None:
return
self.__postorder(node.lchild)
self.__postorder(node.rchild)
print(node.value, end=' ')
验证结果为
8 9 4 5 2 6 7 3 1
这里指的当然是深度优先遍历,因为广度优先本身就是按照从上往下的顺序打印的。
如果要反向推到,从遍历结果到树的原型。首先必须要有2种遍历结果,同时其中一个必须是中序遍历。为什么这么说呢?因为只有中序遍历可以帮助我们找到根节点。
假如有先序结果和中序结果如下
1 2 4 8 9 5 3 6 7
8 4 9 2 5 1 6 3 7
首先通过先序确定根节点是1,然后通过中序确定左子树和右子树。
接着又可以通过先序确定左子树的根节点2,如法炮制对中序结果进行分段,一直到最后。
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# @author: xiaofu
# @date: 2020-Jul-24
class Node:
"""binary tree node"""
def __init__(self, value, lchild=None, rchild=None):
self.value = value
self.lchild = lchild
self.rchild = rchild
class Binary_Tree:
"""binary tree"""
def __init__(self, root=None):
self.root = root
def add(self, node):
if self.root is None:
self.root = node
return
temp = [self.root]
while temp:
nextNode = temp.pop(0)
if nextNode.lchild is None:
nextNode.lchild = node
return
elif nextNode.rchild is None:
nextNode.rchild = node
return
else:
temp.append(nextNode.lchild)
temp.append(nextNode.rchild)
def breadth_travel(self):
if self.root is None:
return
temp = [self.root]
while temp:
nextNode = temp.pop(0)
print(nextNode.value, end=' ')
if nextNode.lchild is None:
continue
elif nextNode.rchild is None:
temp.append(nextNode.lchild)
continue
else:
temp.append(nextNode.lchild)
temp.append(nextNode.rchild)
def preorder(self):
self.__preorder(self.root)
def inorder(self):
self.__inorder(self.root)
def postorder(self):
self.__postorder(self.root)
def __preorder(self, node):
if node is None:
return
print(node.value, end=' ')
self.__preorder(node.lchild)
self.__preorder(node.rchild)
def __inorder(self, node):
if node is None:
return
self.__inorder(node.lchild)
print(node.value, end=' ')
self.__inorder(node.rchild)
def __postorder(self, node):
if node is None:
return
self.__postorder(node.lchild)
self.__postorder(node.rchild)
print(node.value, end=' ')
if __name__ == '__main__':
tree = Binary_Tree()
tree.add(Node(1))
tree.add(Node(2))
tree.add(Node(3))
tree.add(Node(4))
tree.add(Node(5))
tree.add(Node(6))
tree.add(Node(7))
tree.add(Node(8))
tree.add(Node(9))
tree.breadth_travel()
print('')
tree.preorder()
print('')
tree.inorder()
print('')
tree.postorder()
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。