数据结构C语言版-树

548 阅读16分钟

6. 第四章:树

6.1 树的基本概念

6.1.1 树是递归定义的结构

树(tree)是n(n>=0)个节点的有限集合,当n=0时,为空树;n>0时,为非空树。任意一棵非空树,满足以下两个条件:
1)有且仅有一个称为根的节点;
2)除根节点以外,其余节点可分为m(m>0)个互不相交的有限集,其中每一个集合本身又是一棵树,并且称为根的子树(subtree)。

该定义是从集合论的角度给出的树的递归定义,即把树的节点看作一个集合。除了树根以外,其余节点分为m个互不相交的集合,每一个集合又是一棵树。

6.1.2 结点

  • 根节点:树只有一个根结点
  • 结点的度:结点拥有的子树的数量
    • 度为0:叶子结点或者终端结点
    • 度不为0:分支结点或者非终端结点
    • 分支结点除去根结点也称为内部结点

6.1.3 树的度:树中所有结点的度数的最大值

6.1.4 结点关系

  • 祖先结点
    • 根结点到该结点的唯一路径的任意结点
  • 子孙结点
    • 节点的子树中的所有节点都称为该节点的子孙。
  • 双亲结点
    • 根结点到该结点的唯一路径上最接近该结点的结点
  • 孩子结点
    • 节点的子树的根称为该节点的孩子
  • 兄弟结点
    • 有相同双亲结点的结点

6.1.5 层次,高度,深度,树的高度

  • 层次:根为第一层,它的孩子为第二层,以此类推
  • 结点的深度:根结点开始自顶向下累加
  • 结点的高度:叶节点开始自底向上累加
  • 树的高度(深度):树中结点的最大层数

6.1.6 树的性质

有序树——节点的各子树从左至右有序,不能互换位置。 无序树——节点各子树可互换位置。 森林——由m(m>=0)棵不相交的树组成的集合。

  • 1.树中的结点数等于所有结点的度数加1。
    • 证明:不难想象,除根结点以外,每个结点有且仅有一个指向它的前驱结点。也就是说每个结点和指向它的分支一一对应。假设树中一共有b个分支,那么除了根结点,整个树就包含有b个结点,所以整个树的结点数就是这b个结点加上根结点,设为n,则n=b+1。而分支数b也就是所有结点的度数,证毕。
  • 2.度为m的树中第i层上至多有m^(i−1)个结点(i≥1)。
    • 证明:(数学归纳法)首先考虑i=1的情况:第一层只有根结点,即一个结点,i=1带入式子满足。假设第i-1层满足这个性质,第i-1层最多有m^(i-2)个结点。..........i-1层………又因为树的度为m,所以对于第i-1层的每个结点,最多有m个孩子结点。所以第i层的结点数最多是i-1层的m倍,所以第i层上最多有m^(i-1)个结点。
  • 3.高度为h的m叉树至多有(m^h-1)/(m-1)个结点
  • 4.具有n个结点的m叉树的最小高度为

6.2 树的存储结构

6.2.1 顺序存储结构

顺序存储采用一段连续的存储空间,因为树中节点的数据关系是一对多的逻辑关系,不仅要存储数据元素,还要存储它们之间的逻辑关系。顺序存储分为双亲表示法、孩子表示法和双亲孩子表示法

  • 双亲表示法:用一组连续的存储空间存储树的结点,同时在每个结点中,用一个变量存储该结点的双亲结点在数组中的位置。

  • 孩子表示法:孩子表示法是指除了存储数据元素之外,还存储其所有孩子的存储位置下标
  • 双亲孩子表示法:双亲孩子表示法是指除了存储数据元素之外,还存储其双亲和所有孩子的存储位置下标

以上3种表示法的优缺点如下。 双亲表示法只记录了每个节点的双亲,无法直接得到该节点的孩子;孩子表示法可以得到该节点的孩子,但是无法直接得到该节点的双亲,而且由于不知道每个节点到底有多少个孩子,因此只能按照树的度(树中节点的最大度)分配孩子空间,这样做可能会浪费很多空间。双亲孩子表示法是在孩子表示法的基础上,增加了一个双亲域,可以快速得到节点的双亲和孩子,其缺点和孩子表示法一样,可能浪费很多空间。

6.2.2 链式存储结构

由于树中每个节点的孩子数量无法确定,因此在使用链式存储时,孩子指针域不确定分配多少个合适。如果采用“异构型”数据结构,每个节点的指针域个数按照节点的孩子数分配,则数据结构描述困难;如果采用每个节点都分配固定个数(如树的度)的指针域,则浪费很多空间。可以考虑两种方法存储:一种是采用邻接表的思路,将节点的所有孩子存储在一个单链表中,称为孩子链表表示法;另一种是采用二叉链表的思路,左指针存储第一个孩子,右指针存储右兄弟,称为孩子兄弟表示法

  • 孩子表示法:把每个结点的孩子结点排列起来存储成一个单链表。所以n个结点就有n个链表;如果是叶子结点,那这个结点的孩子单链表就是空的;然后n个单链表的的头指针又存储在一个顺序表(数组)中。


image.png

  • 孩子兄弟表示法:顾名思义就是要存储孩子和孩子结点的兄弟,具体来说,就是节点除了存储数据元素之外,还有两个指针域lchild和rchild,被称为二叉链表。lchild

存储第一个孩子地址,rchild存储右兄弟地址。
image.png

image.png

6.3 二叉树

6.3.1 定义

二叉树(binary tree)是n(n>=0)个节点构成的集合,它或为空树(n=0),或满足以下两个条件:
1)有且仅有一个称为根的节点;
2)除根节点以外,其余节点分为两个互不相交的子集T1和T2,分别称为T的左子树和右子树,且T1和T2本身都是二叉树。

二叉树是一种特殊的树,它最多有两个子树,分别为左子树和右子树,二者是有序的,不可以互换。也就是说,二叉树中不存在度大于2的节点。

6.3.2 二叉树的五种基本形态:

  • 1.空树
  • 2.只有一个根结点
  • 3.根结点只有左子树
  • 4.根结点只有右子树
  • 5.根结点既有左子树又有右子树

image.png

6.3.3 特殊二叉树

  • 1.斜树

  • 2.满二叉树:

image.png

  • 3.完全二叉树

image.png

6.3.4 二叉树的性质

  • 1.非空二叉树上叶子结点数等于度为2的结点数加1
  • 2.非空二叉树上第层上至多有个结点(K≥1)
  • 3.深度度为的二叉树至多有个结点(H≥1)
  • 4.具有个节点的完全二叉树的深度必为

6.4 二叉树的存储结构

顺序存储

  • 二叉树的顺序存储结构就是用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的结点元素。

链式存储

二叉树每个结点最多两个孩子,所以设计二叉树的结点结构时考虑两个指针指向该结点的两个孩子。
image.png
image.png

一般情况下,二叉树采用二叉链表存储即可,但是在实际问题中,如果经常需要访问双亲节点,二叉链表存储则必须从根出发查找其双亲节点,为了解决这一问题,可以增加一个指向双亲节点的指针域,这样每个节点就包含3个指针域,分别指向两个孩子节点和双亲节点,还包含一个数据域,用来存储节点信息。这种存储方式称为三叉链表
image.png
image.png

6.5 二叉树的创建

递归创建二叉树有两种方法,分别是询问法补空法

1)询问法

每次输入节点信息后,询问是否创建该节点的左子树,如果是,则递归创建其左子树,否则其左子树为空;询问是否创建该节点的右子树,如果是,则递归创建其右子树,否则其右子树为空。
算法步骤

  • 1)输入节点信息,创建一个节点T。
  • 2)询问是否创建T的左子树,如果是,则递归创建其左子树,否则其左子树为NULL。
  • 3)询问是否创建T的右子树,如果是,则递归创建其右子树,否则其右子树为NULL。

2)补空法

补空法是指如果左子树或右子树为空时,则用特殊字符补空,如“#”,然后按照根、左子树、右子树的顺序,得到先序遍历序列,根据该序列递归创建二叉树。
算法步骤

  • 1)输入补空后的二叉树先序遍历序列。
  • 2)如果ch=='#',T=NULL;否则创建一个新节点T,令T->data=ch;递归创建T的左子树;递归创建T的右子树。

6.6 二叉树的遍历

二叉树的遍历就是按某条搜索路径访问二叉树中的每个节点一次且只有一次。访问的含义很广,如输出、查找、插入、删除、修改、运算等,都可以称为访问。

6.6.1 先序遍历:

先序遍历是指先访问根,然后先序遍历左子树,再先序遍历右子树,即DLR。
算法步骤
如果二叉树为空,则空操作,否则:

  • 1)访问根节点;
  • 2)先序遍历左子树;
  • 3)先序遍历右子树。

先序遍历秘籍:访问根,先序遍历左子树,左子树为空或已遍历才可以遍历右子树。

  • 递归

image.png

  • 非递归

6.6.2 中序遍历:

中序遍历是指中序遍历左子树,然后访问根,再中序遍历右子树,即LDR。
算法步骤
如果二叉树为空,则空操作,否则:

  • 1)中序遍历左子树;
  • 2)访问根节点;
  • 3)中序遍历右子树。

中序遍历秘籍:中序遍历左子树,左子树为空或已遍历才可以访问根,中序遍历右子树。

  • 递归

  • 非递归

6.6.3 后序遍历:

后序遍历是指后序遍历左子树,后序遍历右子树,然后访问根,即LRD。
算法步骤
如果二叉树为空,则空操作,否则:

  • 1)后序遍历左子树;
  • 2)后序遍历右子树;
  • 3)访问根节点。

后序遍历秘籍:后序遍历左子树,后序遍历右子树,左子树、右子树为空或已遍历才可以访问根。

  • 递归

  • 非递归

6.6.4 层次遍历:

二叉树的遍历除一般的先序遍历、中序遍历和后序遍历这3种遍历之外,还有另一种遍历方式——层次遍历,即按照层次的顺序从左向右进行遍历。

  • 若树为空,则什么都不做直接返回。否则从树的第一层开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。

6.7 线索二叉树

二叉树是非线性数据结构,而遍历序列是线性序列,二叉树遍历实际上是将一个非线性结构进行线性化的操作。根据线性序列的特性,除了第一个元素外,每一个节点都有唯一的前驱,除了最后一个元素外,每一个节点都有唯一的后继。(如没有特殊说明,本笔记中的前驱和后继是指直接前驱和直接后继。)而根据遍历序列的不同,每个节点的前驱和后继也不同。采用二叉链表存储时,只记录了左、右孩子的信息,无法直接得到每个节点的前驱和后继。

N个结点的二叉链表,每个结点都有指向左右孩子的结点指针,所以一共有2N个指针,而N个结点的二叉树一共有N-1条分支,也就是说存在2N-(N-1)=N+1个空指针。比如左图二叉树中有6个结点,那么就有7个空指针。

大量的空余指针能否利用起来?


每个节点还是两个指针域,如果节点有左孩子,则lchild指向左孩子,否则lchild指向其前驱;如果节点有右孩子,则rchild指向右孩子,否则rchild指向其后继。那么怎么区分到底存储的是左孩子和右孩子,还是前驱和后继信息呢?为了避免混淆,增加两个标志域ltag和rtag,节点的结构体如图所示。
image.png
image.png

指向前驱和后继的指针称为线索,加上线索的二叉链表就称为线索链表,相应的二叉树就称为线索二叉树 对二叉树以某种次序遍历使其变为线索二叉树的过程就叫做线索化

注意:如果在考试当中只要求绘图,则没必要按照程序执行的过程进行线索化,可以直接写出遍历序列。根据该遍历序列的先后顺序,对所有的空指针域进行线索化,左指针为空,则令其指向前驱;右指针为空,则令其指向后继。

例如,一棵二叉树如图6-143所示,对其中序线索化的过程如下。 image.png 首先写出二叉树的中序遍历序列,即DBEAFGC,然后按照该遍历序列,对所有的空指针进行线索化。 D的左指针为空,但在中序遍历序列中,D是第一个元素,没有前驱,赋值为NULL。D的右指针为空,中序遍历序列中D的后继是B,因此D的右指针指向B节点。同理,从中序遍历序列中可以很清楚地知道每个节点的前驱和后继,分别对所有节点的空指针进行线索化即可,如图6-144所示。 image.png

6.8 哈夫曼树和哈夫曼编码

1)编码尽可能短。我们可以让使用频率高的字符编码较短,使用频率低的编码较长。这种方法可以提高压缩率,节省空间,也能提高运算和通信速度,即频率越高,编码越短。 2)不能有二义性。

1952年,数学家D. A. Huffman提出了用字符在文件中出现的频率(即用0、1串)表示各字符的最佳编码方式,称为哈夫曼编码(Huffman code)。哈夫曼编码很好地解决了 上述两个关键问题,被广泛地应用于数据压缩,尤其是远距离通信和大容量数据存储,常用的JPEG图片就是采用哈夫曼编码压缩的。 哈夫曼编码的基本思想是以字符的使用频率作为权来构建一棵哈夫曼树,然后利用哈夫曼树对字符进行编码。哈夫曼树是通过将所要编码的字符作为叶子节点,将该字符在文件中的使用频率作为叶子节点的权值,以自底向上的方式,做n−1次“合并”运算构造出来的。

算法的描述如下:

  • 1)将这N个结点分别作为N棵仅含一个结点的二叉树,构成森林F。
  • 2)构造一个新结点,并从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
  • 3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
  • 4)重复步骤2)和3),直至F中只剩下一棵树为止。

6.9. 二叉树与树与森林

6.9.1 树与二叉树

如何将一棵树转化成二叉树?

树的孩子兄弟表示法与二叉树的二叉链表表示法都是用到两个指针

  • 将孩子兄弟表示法理解成二叉链表

树转换成二叉树的手动模拟方法:

  • ①将同一结点的各个孩子用线串连起来
  • ②将每个结点的子树分支,从左往右,除了第一个以外全部删除
  • 例子

如何将一棵二叉树转化成树?

二叉树转换成树的手动模拟方法:

  • ①将二叉树从上到下分层,并调节成水平方向。(分层方法:每遇到左孩子则为一层)
  • ②找到每一层的双亲结点,方法为它的上一层相连的那个结点就是双亲结点。例如bcd这一层,与它相连的上一层结点即为a,所以bcd这三个结点的双亲结点都是a.
  • ③将每一层结点和其双亲结点相连,同时删除该双亲结点各个孩子结点之间的联系。
  • 例子

6.9.2 森林与二叉树

森林:森林是m(m≥0)棵互不相交的树的集合

如何将森林转换成二叉树?

森林转换成树的手动模拟方法:

  • ①将森林中每棵树都转换成二叉树
  • ②将第二棵树作为第一棵树的根结点的右子树,将第三棵树作为第二棵树的根结点的右子树..依次类推
  • 例子

如何将二叉树转换成森林?

二叉树转换成森林的手动模拟方法:

  • 反复断开二叉树根结点的右孩子的右子树指针,直到不存在根结点有右孩子的二叉树为止。
  • 例子

6.9.3 树与森林的遍历

  • 先序:先访问根结点,再访问根结点的每棵子树。 访问子树也是按照先序的要求
  • 后序:先访问根结点的每棵子树,再访问根结点。 访问子树也是按照先序的要求
  • 树的先序遍历等于它对应二叉树的先序遍历,后序遍历等于它对应的二叉树的中序遍历

例子

6.10 树的小结

image.png