文章目录
一、树的概念
1.定义
树是一种抽象数据结构(ADT),用来模拟具有树状结构性质的数据集合。
树是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。
2.特点:
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每个非根节点有且只有一个父节点;
- 除了根节点,每个子节点可以分为多个不相交的子树。
如图,
A 节点就是 B 节点的父节点,B 节点是 A 节点的子节点。B、C、D 这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫作根节点,也就是图中的节点 E。我们把没有子节点的节点叫作叶子节点或者叶节点,比如图中的 G、H、I、J、K、L 都是叶子节点。
3.度量概念:
高度(Height):节点到叶子节点的最长路径。
树的高度即根节点的高度。
深度(Depth):根节点到这个节点所经历的边的个数。
层数(Level):节点的深度+1。
如下图所示,
4.种类:
- 无序树:树中任意节点的子节点之间没有顺序关系,也称为自由树。
- 有序树:树中任意节点的子节点之间有顺序关系。
又进一步分为,- 二叉树:每个节点最多含有两个子树的树称为二叉树。
- 完全二叉树:对于一颗二叉树,假设其深度为d(d>1)。除了第d层外,其它各层的节点数目均已达最大值,且第d层所有节点从左向右连续地紧密排列,这样的二叉树被称为完全二叉树,其中满二叉树的定义是所有叶节点都在最底层的完全二叉树。
- 平衡二叉树(AVL树):当且仅当任何节点的两棵子树的高度差不大于1的二叉树。
- 排序二叉树(二叉查找树、二叉搜索树、有序二叉树,Binary Search Tree)。
- 霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树,常用于信息编码。
- B树:一种对读写操作进行优化的自平衡的二叉查找树,能够保持数据有序,拥有多余两个子树。
- 二叉树:每个节点最多含有两个子树的树称为二叉树。
5.树的应用场景:
- xml,html等的结构其实就是树的结构,编写的适合会用到树;
- 路由协议使用了树的算法;
- mysql数据库索引;
- 文件系统的目录结构;
- 很多经典的AI算法其实都是树搜索,此外机器学习中的decision tree也是树结构。
二、二叉树及其概念
1.二叉树定义
二叉树每个节点最多有两个子节点,分别是左子节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只有左子节点,有的节点只有右子节点。
如下图,
其中,编号 2 的二叉树中,叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫作满二叉树。
编号 3 的二叉树中,叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫作完全二叉树。
2.存储与表示
(1)顺序存储:
将数据结构存储在固定的数组中。
特点:在遍历速度上有一定的优势,但因所占空间比较大,是非主流二叉树。
如图
(2)链式存储:
每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。
特点:只要找到根节点,就可以通过左右子节点的指针,把整棵树都串起来。是常用的存储方式。
如图
三、二叉树的实现
1.节点的实现
通过Node类中定义三个属性,分别为item本身的值,还有litem和ritem。
class Node(object):
'''节点类'''
def __init__(self,item,litem = None,ritem=None):
self.item = item
self.litem = litem
self.ritem = ritem
2.树的创建和添加
创建一个树的类,并给一个root根节点,一开始为空,随后添加节点。
class Tree(object):
'''树类'''
def __init__(self,root=None):
self.root = root
def add(self,item):
'''添加节点'''
#创建节点
node = Node(item)
#如果树是空的,则对根节点赋值
if self.root == None:
self.root = node
else:
queue = []
# 根节点放到队列中
queue.append(self.root)
#对所有的节点进行层次遍历
while queue:
#弹出队列的第一个元素
cur_node = queue.pop(0)
if cur_node.litem == None:
cur_node.litem = node
return
elif cur_node.ritem == None:
cur_node.ritem = node
return
else:
#如果左右子树都不为空,加入队列继续判断
queue.append(cur_node.litem)
queue.append(cur_node.ritem)
添加实际上是按照广度优先顺序添加的。
3.树的遍历
树的遍历是树的一种重要操作。
遍历是指对树中所有结点的信息的访问,即依次对树中每个结点访问一次且仅访问一次,我们把这种对所有节点的访问称为遍历(traversal)。
树的两种重要的遍历方式是深度优先遍历和广度优先遍历,深度优先一般用递归,广度优先一般用队列。
一般情况下能用递归实现的算法大部分也能用堆栈来实现。
广度优先遍历(层次遍历)
从树的root开始,从上到下从从左到右遍历整个树的节点。
实现如下,
def breadth_travel(self):
'''广度优先遍历'''
if self.root == None:
return
else:
queue = []
# 根节点放到队列中
queue.append(self.root)
while queue:
#当前节点
node = queue.pop(0)
print(node.item,end=' ')
if node.litem != None:
queue.append(node.litem)
if node.ritem != None:
queue.append(node.ritem)
深度优先遍历
对于一颗二叉树,沿着树的深度遍历树的节点,尽可能深地搜索树的分支。
深度遍历有重要的三种方法,它们之间的不同在于访问每个节点的次序不同。这三种遍历分别为先序遍历(preorder)、中序遍历(inorder)和后序遍历(postorder)。
先序遍历:
先访问根节点,然后递归使用先序遍历访问左子树,再递归使用先序遍历访问右子树,即
根节点->左子树->右子树
实现如下
def preorder(self,node):
'''递归实现先序遍历'''
if node == None:
return
print(node.item,end=' ')
self.preorder(node.litem)
self.preorder(node.ritem)
中序遍历:
递归使用中序遍历访问左子树,然后访问根节点,最后再递归使用中序遍历访问右子树,即
左子树->根节点->右子树
实现如下
def inorder(self,node):
'''递归实现中序遍历'''
if node == None:
return
self.inorder(node.litem)
print(node.item,end=' ')
self.inorder(node.ritem)
后序遍历:
在后序遍历中,我们先递归使用后序遍历访问左子树和右子树,最后访问根节点,即
左子树->右子树->根节点
实现如下
def postorder(self,node):
'''递归实现后序遍历'''
if node == None:
return
self.postorder(node.litem)
self.postorder(node.ritem)
print(node.item, end=' ')
先序、终须、后序的记忆方式可根据根节点的遍历先后进行区别,最先遍历即为先序遍历,中间遍历则为中序遍历,最后遍历则为后序遍历。
完整实现代码如下,
class Node(object):
'''节点类'''
def __init__(self,item,litem = None,ritem=None):
self.item = item
self.litem = litem
self.ritem = ritem
class Tree(object):
'''树类'''
def __init__(self,root=None):
self.root = root
def add(self,item):
'''添加节点'''
#创建节点
node = Node(item)
#如果树是空的,则对根节点赋值
if self.root == None:
self.root = node
else:
queue = []
# 根节点放到队列中
queue.append(self.root)
#对所有的节点进行层次遍历
while queue:
#弹出队列的第一个元素
cur_node = queue.pop(0)
if cur_node.litem == None:
cur_node.litem = node
return
elif cur_node.ritem == None:
cur_node.ritem = node
return
else:
#如果左右子树都不为空,加入队列继续判断
queue.append(cur_node.litem)
queue.append(cur_node.ritem)
def breadth_travel(self):
'''广度优先遍历'''
if self.root == None:
return
else:
queue = []
# 根节点放到队列中
queue.append(self.root)
while queue:
#当前节点
node = queue.pop(0)
print(node.item,end=' ')
if node.litem != None:
queue.append(node.litem)
if node.ritem != None:
queue.append(node.ritem)
def preorder(self,node):
'''递归实现先序遍历'''
if node == None:
return
print(node.item,end=' ')
self.preorder(node.litem)
self.preorder(node.ritem)
def inorder(self,node):
'''递归实现中序遍历'''
if node == None:
return
self.inorder(node.litem)
print(node.item,end=' ')
self.inorder(node.ritem)
def postorder(self,node):
'''递归实现后序遍历'''
if node == None:
return
self.postorder(node.litem)
self.postorder(node.ritem)
print(node.item, end=' ')
if __name__ == '__main__':
t = Tree()
t.add(0)
t.add(1)
t.add(2)
t.add(3)
t.add(4)
t.add(5)
t.add(6)
t.add(7)
t.add(8)
t.breadth_travel()
print()
t.preorder(t.root)
print()
t.inorder(t.root)
print()
t.postorder(t.root)
打印结果为
0 1 2 3 4 5 6 7 8
0 1 3 7 8 4 2 5 6
7 3 8 1 4 0 5 2 6
7 8 3 4 1 5 6 2 0
显然,4种遍历方法得到的结果都不完全一致。
再用下图进行展示,
4.遍历的分析
如果知道了树的先序、后序遍历结果中的一种,并且也知道中序遍历的结果,可以根据遍历结果进行反推:
(1)根据先序遍历的第一个元素或后序遍历的最后一个元素即可知道原树的根节点,再在中序遍历结果中从根节点断开,左边即为左子树,右边即为右子树;
(2)根据分出的左右子树将先序或后序除开根节点的部分分成两个部分,即为左子树、右子树,对每个子树再进行(1)中的操作,即递归,直到每个子树的大小为1为止。
使用这种方法的前提是必须知道中序遍历的结果,和前序、后序遍历结果中的一种。
大家也可以关注我的公众号:Python极客社区,在我的公众号里,经常会分享很多Python的文章,而且也分享了很多工具、学习资源等。另外回复“电子书”还可以获取十本我精心收集的Python电子书。