第六章:树
树(Tree):是n(n≥0)个结点的有限集。n=0时称为空树,在任意一颗非空树中:
- 有且只有一个特定的称为根(root)的结点
- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集,其中每一个集合本身又是一颗树,并且称为根的子树(SubTree)
注:根结点是唯一的,子树没有个数限制且是互不相交的
结点的分类:
树的结点:包含一个数据 + 若干子树分支
结点的度(Degree): 结点所拥有子树的个数,度为0的结点为叶结点(Leaf)或终端结点;度不为0的结点称为分支结点或非终端结点,除根结点外也称为内部结点。
树的度: 是树内各结点的度的最大值
结点之间的关系
孩子(Child):结点的子树称为该结点的孩子,该结点称为孩子的双亲(Parent),同一个双亲的孩子之间互称为兄弟(Sibling),从根结点到该结点所经分支结点称为该结点的祖先,以某一结点为根的子树中任意结点都为该结点的子孙。
树的其他概念
层次(Level): 从根结点定起,根为第一层,根的孩子为第二层,若某结点在m层,则它的孩子在m+1层。
深度(Deep)或高度: 树中结点的最大层称为树的深度。
有序树、无序树: 将树中结点的各子树看出从左到右有序,不能互换则为有序树,否则为无序树。
森林(Forest): 是m(m≥0)颗互不相交的树的集合。
树的存储结构
树的表示方法:
- 双亲表示法,结点可以没有孩子,但一定会有双亲,在每个节点中附设一个指示其双亲结点到链表中的位置
-
孩子表示法,由于树中每个结点可能有多颗子树,可以考虑多重链表,及每个结点有多个指针域,其中每个指针指向一颗子树的根结点。
-
孩子兄弟表示法,若一个结点第一个孩子存在则一定是唯一的,它的右兄弟存在也是唯一的,用两个指针分别指向该结点的第一个孩子和此结点的右兄弟。这个方法最大好处就是将复杂树变成了一颗二叉树。
二叉树
二叉树(BinaryTree): 是n个结点的有限集合,该集合或者为空集(空二叉树),或者有一个根结点和两颗互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
二叉树的特点:
- 每个结点最多有两颗树,二叉树的度≤2
- 左子树与右子树不能换顺序,有序树
- 某结点只有一颗树时也区分是左子树还是右子树
特殊二叉树
斜树: 一定是斜的,结点都在左子树的叫左斜树,结点都在右侧的叫右斜树,结点的个数与二叉树的深度相同。
满二叉树: 所有分支结点都存在左右子树,叶子结点都在同一层上的树称满二叉树。
完全二叉树: 对一颗有n结点的二叉树编号,如果变化为i(1≤i≤n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中的位置完全相同,则这颗树称为完全二叉树。
满二叉树一定是完全二叉树,但完全二叉树不一定是满的。完全二叉树的一些特点:
- 叶子结点只出现在最下两层
- 最下层的叶子一定集中在左部连续位置
- 倒数第二层若有叶子结点,则一定都在右部连续位置
- 如果结点度为1,则一定是只有左孩子,不存在右孩子
- 同样结点数的二叉树,完全二叉树的深度最小
二叉树的性质
- 在二叉树的第i层至多有2^(i-1)个结点
- 深度为k的二叉树至多有2^k-1个结点(k≥1),等比数列求和
- 任意一颗二叉树,如果终端结点数为n0,度为2的二叉树为n1,则n0=n1+1
- 具有n个结点的完全二叉树的深度为[log2(n)]+1 取整
- 对于一颗有n个结点的完全二叉树的结点按照层编号,对任意结点i(1≤i≤n)
- 如果i=1,则结点是二叉树的根,无双亲;如果i>1,则其双亲结点为[i/2]
- 如果2i>n,则结点无左孩子(结点i为叶子结点);否则其左孩子是结点2i
- 如果2i+1>n,则结点无右孩子;否则其右孩子是结点2i+1
二叉树的存储结构
二叉树的顺序存储结构,对于完全二叉树,因其结构的特点,相应的下标对应相同的位置
将树存入数组中:
二叉链表,二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域,称这样的链表为二叉链表。
data为数据域,lchild与rchild分别为指针域,分别指向左孩子与右孩子
遍历二叉树
二叉树的遍历(TraversingBinaryTree),是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
二叉树的遍历方法:
- 前序遍历:非空二叉树,先根结点,再左子树,最后右子树
- 中序遍历:非空二叉树,先左子树,再根结点,最后右子树
- 后续遍历:非空二叉树,先左子树,再右子树,最后根结点
- 层次遍历:非空二叉树,从树的第一层开始,即根结点开始
二叉树的建立与遍历
以下是用python代码实现二叉树的建立与遍历操作:
class BTree(object): # 创建二叉树结点对象
def __init__(self, vale):
self.vale = vale
self.left = None
self.right = None
def set_left(self, vale):
self.left = BTree(vale)
return self.left
def set_right(self, vale):
self.right = BTree(vale)
return self.right
def show_vale(self):
print(self.vale)
def preorder_print_tree(atree): # 前序遍历
if atree == None:
return
print(atree.vale)
preorder_print_tree(atree.left)
preorder_print_tree(atree.right)
def inorder_print_tree(atree): # 中序遍历
if atree == None:
return
inorder_print_tree(atree.left)
print(atree.vale)
inorder_print_tree(atree.right)
def postorder_print_tree(atree): # 后续遍历
if atree == None:
return
postorder_print_tree(atree.left)
postorder_print_tree(atree.right)
print(atree.vale)
if __name__ == '__main__':
# 从root建立根结点
root = BTree('root')
A = root.set_left('A')
B = root.set_right('B')
C = A.set_left('C')
D = A.set_right('D')
E = B.set_left('E')
F = B.set_right('F')
# print(preorder_print_tree(root))
# print(inorder_print_tree(root))
print(postorder_print_tree(root))
二叉树的遍历三种方式,主要区别在先打印结点vale,还是递归,通过不同顺序组合实现前序遍历,中序遍历或后续遍历,二叉树的遍历运用了递归,在二叉树的建立过程中也可以使用递归,但由于python不对地址进行操作,相比之下C/C++代码对实现树的操作更灵活。
线索二叉树
线索二叉树(ThreadedBinaryTree):将叶子结点中的左右孩子指针用来存储,结点的前驱和后继指针,充分利用地址空间,我们把这种指向前驱和后继的指针称作线索,加上线索的二叉树为线索链表(双链表结构),相应的二叉树称为线索二叉树。
对二叉树进行线索化的实质就是将二叉树中链表为空的指针改为指向前驱或后继的线索。
树、森林与二叉树的转换
- 树转二叉树:
- 加线。在所有兄弟结点之间加一条线
- 去线。树中每一个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
- 层次调整。以树根为轴心,旋转一定角度。第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子
- 森林转二叉树
- 把每颗树转换为二叉树
- 第一颗树不动,从第二颗开始依次把后一颗树的根结点作为上一颗树的右孩子,用线连起来。
- 二叉树转树、二叉树转森林,上述过程的逆过程,《大话数据结构》P197
赫尔曼树
线,删除它与其他孩子结点之间的连线
3. 层次调整。以树根为轴心,旋转一定角度。第一个孩子是二叉树结点的左孩子,兄弟转换过来的孩子是结点的右孩子
- 森林转二叉树
- 把每颗树转换为二叉树
- 第一颗树不动,从第二颗开始依次把后一颗树的根结点作为上一颗树的右孩子,用线连起来。
- 二叉树转树、二叉树转森林,上述过程的逆过程,《大话数据结构》P197
赫尔曼树
将二叉树简化成叶子结点带权的二叉树,从一个结点到另一个结点所经过的分支数目称做路径长度,带权路径长度和最小的二叉树叫做赫夫曼树。引申出赫夫曼编码。《大话数据结构》P205