第五章 树与二叉树
5.1 树的基本概念
5.1.1 树的定义
树是n个结点的有限集。当n=0时,称为空树。在任意一棵非空树中应满足:
- 有且仅有一个称为根的结点
- 当n>1时,其余结点可分为m个互不相交的有限集,其中每个集合本身又是一棵树,并且称为根的子树
树的定义是递归的,树本身也是一种递归的数据结构。其作为一种逻辑结构,同时也是一种分层结构。树适合表示具有层次结构的数据。
5.1.2 基本术语
- 度。树中一个结点的孩子个数称为该结点的度,树中结点的最大度数称为树的度。
- 路径和路径长度。树中两个结点之间的路径室友这两个结点之间所经过的结点序列构成,二路径长度是路径上所经过的边的个数。
- 森林。森林是m棵互不相交的树的集合。
5.1.3 树的性质
树 中 结 点 等 于 所 有 结 点 的 度 数 之 和 加 1 , 即 总 边 数 + 1 = 度 数 之 和 度 为 m 的 树 中 , 第 i 层 上 至 多 有 m i − 1 个 结 点 高 度 为 h 的 m 叉 树 至 多 有 m h − 1 m − 1 个 结 点 具 有 n 个 结 点 的 m 叉 树 最 小 高 度 为 ⌈ log m ( n ( m − 1 ) + 1 ) ⌉ \begin{aligned} &树中结点等于所有结点的度数之和加1,即 总边数+1=度数之和\\ &度为 m 的树中,第 i 层上至多有 m^{i-1}个结点\\ &高度为 h 的 m 叉树至多有 \frac{m^h-1}{m-1}个结点\\ &具有 n 个结点的 m 叉树最小高度为\lceil \log_m(n(m-1)+1)\rceil \end{aligned} 树中结点等于所有结点的度数之和加1,即总边数+1=度数之和度为m的树中,第i层上至多有mi−1个结点高度为h的m叉树至多有m−1mh−1个结点具有n个结点的m叉树最小高度为⌈logm(n(m−1)+1)⌉
5.2 二叉树的概念
5.2.1 二叉树的定义及其主要特性
-
二叉树的定义
二叉树是一种特殊的树形结构,特点是每个结点至多只有两棵子树,但其度可以小于2;并且二叉树的子树有左右之分,即使树中结点只有一棵子树,也要区分其是左子树还是右子树。
-
几个特殊的二叉树
-
满二叉树
一 棵 高 度 为 h , 且 含 有 2 h − 1 个 结 点 的 二 叉 树 称 为 满 二 叉 树 。 根 结 点 从 1 开 始 编 号 , 若 结 点 编 号 为 i , 其 双 亲 为 ⌋ ⌊ i / 2 ⌋ 其 左 孩 子 为 2 i , 右 孩 子 为 2 i + 1 。 \begin{aligned} &一棵高度为h,且含有2^h-1个结点的二叉树称为满二叉树。\\ &根结点从1开始编号,若结点编号为 i,其双亲为\rfloor ⌊i/2⌋ 其左孩子为2i,右孩子为2i+1。\\ \end{aligned} 一棵高度为h,且含有2h−1个结点的二叉树称为满二叉树。根结点从1开始编号,若结点编号为i,其双亲为⌋⌊i/2⌋其左孩子为2i,右孩子为2i+1。 -
完全二叉树
若 结 点 编 号 i ≤ ⌊ n / 2 ⌋ , 则 结 点 为 分 支 结 点 , 否 则 为 叶 子 系 结 点 。 叶 子 结 点 只 可 能 在 层 次 最 大 的 两 层 上 出 现 。 最 有 有 一 个 度 为 1 的 结 点 , 且 该 结 点 只 有 左 孩 子 。 若 n 为 奇 数 , 则 每 个 分 支 结 点 都 有 左 右 孩 子 \begin{aligned} &若结点编号i≤⌊n/2⌋,则结点为分支结点,否则为叶子系结点。\\ &叶子结点只可能在层次最大的两层上出现。最有有一个度为1的结点,且该结点只有左孩子。\\ &若n为奇数,则每个分支结点都有左右孩子\\ \end{aligned} 若结点编号i≤⌊n/2⌋,则结点为分支结点,否则为叶子系结点。叶子结点只可能在层次最大的两层上出现。最有有一个度为1的结点,且该结点只有左孩子。若n为奇数,则每个分支结点都有左右孩子 -
二叉排序树
左子树上所有结点的关键字都小于根结点,右子树上的所有结点关键字都大于根结点。
-
平衡二叉树
树上任一结点的左子树和右子树的深度之差不超过1。
-
-
二叉树的性质
-
非 空 二 叉 树 的 叶 子 结 点 数 等 于 度 为 2 的 结 点 数 + 1 , 即 n 0 = n 2 + 1 非空二叉树的叶子结点数等于度为2的结点数+1,即n0=n2+1 非空二叉树的叶子结点数等于度为2的结点数+1,即n0=n2+1
-
非 空 二 叉 树 上 第 k 层 至 多 有 2 k − 1 个 结 点 非空二叉树上第 k 层至多有2^{k-1}个结点 非空二叉树上第k层至多有2k−1个结点
-
高 度 为 h 的 二 叉 树 至 多 有 2 h − 1 个 结 点 高度为 h 的二叉树至多有2^h-1个结点 高度为h的二叉树至多有2h−1个结点
-
结 点 数 为 n 的 二 叉 树 有 ( 2 n ) ! n ! ( n + 1 ) ! 种 形 态 ( 卡 特 兰 数 ) 结点数为n的二叉树有\frac{(2n)!}{n!(n+1)!}种形态(卡特兰数) 结点数为n的二叉树有n!(n+1)!(2n)!种形态(卡特兰数)
-
-
完全二叉树的性质
-
结 点 i 的 双 亲 编 号 为 ⌊ i 2 ⌋ , 即 当 i 为 偶 数 时 ( 左 孩 子 ) , 其 双 亲 的 编 号 为 i / 2 , 当 i 为 奇 数 时 , 其 双 亲 编 号 为 ( i − 1 ) / 2 。 结点 i 的双亲编号为\lfloor \frac{i}{2} \rfloor,即当 i 为偶数时(左孩子),其双亲的编号为 i/2,当i为奇数时,其双亲编号为(i-1)/2。 结点i的双亲编号为⌊2i⌋,即当i为偶数时(左孩子),其双亲的编号为i/2,当i为奇数时,其双亲编号为(i−1)/2。
-
推 论 : 具 有 n 个 结 点 的 完 全 二 叉 树 , 编 号 最 大 的 分 支 节 点 为 ⌊ n 2 ⌋ 推论:具有n个结点的完全二叉树,编号最大的分支节点为\lfloor \frac{n}{2}\rfloor 推论:具有n个结点的完全二叉树,编号最大的分支节点为⌊2n⌋
-
结 点 i 所 在 层 次 为 ⌈ log 2 [ i ( 2 − 1 ) + 1 ] ⌉ = ⌊ log 2 i ⌋ + 1 结点 i 所在层次为\lceil \log_2[i(2-1)+1]\rceil=\lfloor \log_2i\rfloor + 1 结点i所在层次为⌈log2[i(2−1)+1]⌉=⌊log2i⌋+1
-
5.2.2 二叉树的存储结构
-
顺序存储结构
适合存储完全二叉树和满二叉树。对于一般的空结点,需要添加空结点。
-
链式存储结构
typedef struct BiTNode{ ElemType data; struct BiTNode *lchild = nullptr,*rchild = nullptr;//左孩子、右孩子指针 }BiTNode,*BiTree;
5.3 二叉树的遍历和线索二叉树
5.3.1 二叉树的遍历
以该二叉树为例
-
先序遍历
操作过程:若二叉树为空,则什么也不做;否则,
- 访问根节点
- 先序遍历左子树
- 先序遍历右子树
代码实现:
//先序遍历 void visit(BiTree T){ std::cout << T->data << ' '; } void PreOrder(BiTree T){ if (T != nullptr){ visit(T); PreOrder(T->lchild); PreOrder(T->rchild); } }
-
中序遍历
操作过程:若二叉树为空,则什么也不做;否则,
- 中序遍历左子树
- 访问根节点
- 中序遍历右子树
代码实现:
void InOrder(BiTree T){ if (T != nullptr){ InOrder(T->lchild); visit(T); InOrder(T->rchild); } }
-
后序遍历
操作过程:若二叉树为空,则什么也不做;否则,
- 后序遍历左子树
- 后序遍历右子树
- 访问根节点
代码实现:
void PostOrder(BiTree T){ if (T != nullptr){ PostOrder(T->lchild); PostOrder(T->rchild); visit(T); } }
-
层次遍历
层次遍历就是一层一层往下遍历,如上图二叉树的层次遍历结果为1 2 3 4 5 6.
算法思想:
1、首先将二叉树的根节点进入队列中,判断队列不为NULL。
2、打印输出该节点存储的元素。
3、判断节点如果有孩子(左孩子、右孩子),就将孩子进入队列中。
4、循环以上操作,直到 BT->lchild == NULL、BT->rchild=NULL。代码实现:
void LevelOrder(BiTree T){ queue<BiTree> queue; BiTree p; queue.push(T); while (!queue.empty()){ p = queue.front(); visit(p); queue.pop(); if (p->lchild != nullptr) queue.push(p->lchild); if (p->rchild != nullptr) queue.push(p->rchild); } }
-
由遍历序列构造二叉树
给定一个二叉树,该二叉树的前、中、后、层序遍历肯定是唯一的,但是给定一个前、中、后、层序遍历并不能确定唯一的二叉树
也就是:若只给出一棵二叉树的前、中、后、层序遍历中的一种不能确定一棵二叉树
只有给出其中两种才能确定一棵二叉树(且一定要有中序)
-
先序+中序遍历
在先序遍历序列中,第一个结点一定是二叉树的根节点;而在中序遍历中,根节点必然将中序序列分割成两个子序列,前一个子序列是根结点的左子树的中序序列,后一个子序列是根结点的右子树的中序序列。根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。在先序序列中,左子序列的第一个结点是左子树的根结点,右子序列的第一个结点是右子树的根节点。如此递归下去便能唯一地确定这棵二叉树。
BiTNode *PreInCreate(int pre[], int in[], int s1, int t1, int s2, int t2){ //s1,t1为先序遍历序列的第一个结点和最后一个结点的下标 //s2,t2为中序遍历序列的第一个结点和最后一个结点的下标 //初始时s1=s2=0, t1=t2=n-1 BiTNode *root = (BiTNode *)malloc(sizeof(BiTNode)); //建立根结点 root->data = pre[s1]; //根结点 int i; for(i=s2; in[i]!=root->data; i++); //找根结点在中序遍历序列中的位置 int llen = i-s2; //左子树长度 int rlen = t2-i; //右子树长度 if(llen != 0) //递归建立左子树 root->lchild = PreInCreate(pre, in, s1+1, s1+llen, s2, s2+llen-1); else //左子树为空 root->lchild = nullptr; if(rlen != 0)//递归建立右子树 root->rchild = PreInCreate(pre, in, t1-rlen+1, t1, t2-rlen+1, t2); else //右子树为空 root->rchild = nullptr; return root; }
-
中序+后序遍历
因为后序序列的最后一个结点就如同先序序列的第一个结点,可以将中序序列分割成两个子序列,然后采用类似的方法递归地进行划分,进而得到一棵二叉树。
BiTNode *PostInCreate(int post[], int in[], int s1, int t1, int s2, int t2){ //s1,t1为后序遍历序列的第一个结点和最后一个结点的下标 //s2,t2为中序遍历序列的第一个结点和最后一个结点的下标 //初始时s1=s2=0, t1=t2=n-1 BiTNode *root = (BiTNode *)malloc(sizeof(BiTNode)); //建立根结点 root->data = post[t1]; //根结点 int i; for(i=s2; in[i]!=root->data; i++); //找根结点在中序遍历序列中的位置 int llen = i-s2; //左子树长度 int rlen = t2-i; //右子树长度 if(llen != 0) //递归建立左子树 root->lchild = PostInCreate(post, in, s1, s1+llen-1, s2, s2+llen-1); else //左子树为空 root->lchild = nullptr; if(rlen != 0) //递归建立右子树 root->rchild = PostInCreate(post, in, t1-rlen, t1-1, t2-rlen+1, t2); else //右子树为空 root->rchild = nullptr; return root; }
-
测试代码:
void TreeTest(){
BiTNode root;
root.data = 1;
BiTNode t1;
t1.data = 2;
NodeInsert(root,t1,1);
BiTNode t2;
t2.data = 3;
NodeInsert(root,t2,0);
BiTNode t3;
t3.data = 4;
NodeInsert(t1,t3,0);
BiTNode t4;
t4.data = 5;
NodeInsert(t2,t4,0);
BiTNode t5;
t5.data = 6;
NodeInsert(t3,t5,1);
cout<< "先序遍历结果:";
PreOrder(&root);
cout << endl;
cout<< "中序遍历结果:";
InOrder(&root);
cout << endl;
cout<< "后序遍历结果:";
PostOrder(&root);
cout << endl;
cout<< "层次遍历结果:";
LevelOrder(&root);
cout << endl;
}
运行结果:
5.3.2 线索二叉树
-
线索二叉树的基本概念
传统的二叉链表存储仅能体现一种父子关系,不能直接得到节点在遍历中的前驱或后继。而且浪费了较多存储空间,我们可以利用这些存储空间来存储该结点的前驱和后继关系,这样就可以像单链表一样方便地遍历二叉树。
规定:若无左子树,令lchild指向其前驱结点;若无右子树,令rchild指向其后继结点。并增加两个标志域标识指针域是指向左(右)孩子还是指向前驱(后继)。
存储结构如下:
typedef struct ThreadNode{ ElemType data;//数据元素 struct ThreadNode *lchild = nullptr,*rchild = nullptr;//左、右孩子指针 int ltag,rlag;//左、右线索标志 }ThreadNode,*ThreadTree;
-
中序线索二叉树的构造
二叉树的线索化是将二叉链表中的空指针改为指向前驱或后继的线索。因此线索化的实质就是遍历一次二叉树。
以中序线索二叉树为例。附设指针pre指向刚刚访问过的结点,指针p指向正在访问的结点,即pre指向p的前驱。在中序遍历的过程中,检查p的左指针是否为空,若为空就将它指向p;检查p的右指针是否为空,若为空就将它指向p;
//通过中序遍历对二叉树进行线索化 void InThread(ThreadTree &p,ThreadTree &pre){ if (p != nullptr){ InThread(p->lchild,pre);//递归,线索化左子树 if (p->lchild == nullptr){ p->lchild = pre; p->ltag = 1; } if (pre != nullptr && pre->rchild == nullptr){ pre->rchild = p; pre->rtag = 1; } pre = p; InThread(p->rchild,pre); } } //通过中序遍历建立线索二叉树 void CreateInThread(ThreadTree T){ ThreadTree pre = nullptr; if (T != nullptr){ InThread(T,pre); pre->rchild = nullptr; pre->rtag = 1; } }
-
中序线索二叉树的遍历
分两步,1.获取中序遍历的首节点。2.获取中序线索化二叉树中当前节点的后继节点。只要完成这两步就可以遍历中序线索化二叉树。
//求第一个结点 ThreadNode *FirstNode(ThreadNode *p){ while (p->ltag == 0) p = p->lchild; return p; } //求p的后继 ThreadNode *NextNode(ThreadNode *p){ if (p->rtag == 0) return FirstNode(p->rchild); else return p->rchild; } //遍历 void Inorder(ThreadNode *T){ for (ThreadNode *p = FirstNode(T);p!= nullptr;p=NextNode(p)) visit(p); }
-
先序线索二叉树和后序线索二叉树
略
5.4 树、森林
5.4.1 树的存储结构
-
双亲表示法
采用一组连续空间来储存每个结点,同时在每个结点中增设一个伪指针,指示其双亲结点在数组中的位置。根结点的下标为0,其伪指针域为-1。该存储结构可以很快得到每个结点的双亲位置,但求结点孩子时需要遍历整个结构。
#define ElemType int #define MAX_TREE_SIZE 100 typedef struct {//树的结点定义 ElemType data;//数据元素 int parent;//双亲位置域 }PTNode; typedef struct {//树的类型定义 PTNode nodes[MAX_TREE_SIZE];//双亲表示 int n;//结点数 }PTree;
-
孩子表示法
为每个结点创建一个链表,将该结点的孩子都用单链表接起来。再将所有结点顺序存储在一个数组中,数组中每个元素不但储存结点,还设置一个指针域,指向该结点的孩子链表。n个结点就有n个孩子链表(叶子结点的孩子链表为空表)。
这种方式寻找子女的操作非常直接,而寻找双亲的操作需要遍历所有孩子链表。 -
孩子兄弟表示法
以二叉链表作为树的存储结构。
二叉树的左指针指向其第一个孩子,右指针指向其下一个兄弟。沿着右指针可以找到所有兄弟结点。
最大优点是可以方便实现树到二叉树的转换,易于找到结点的孩子。缺点是查找双亲结点比较麻烦,可以添加一个parent域指向父结点来解决。typedef struct CSNode{ ElemType data; struct CSNode *firstchild = nullptr,*nextbling = nullptr; }CSNode,*CSTree;
5.4.2 树、森林与二叉树的转换
树转化为二叉树
二叉树和树都可以用二叉链表作为存储结构,给定一棵树,可以找到唯一一棵二叉树与之对应。
对于一棵树,每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟。
这种规则下,根结点只有左孩子。
森林转化为二叉树
先将森林中的每一棵树转换为二叉树,由于任何一棵树对应的二叉树右子树必空,只需把所有二叉树的根结点用其右指针连接起来即可,即将所有树的根结点视为兄弟结点。
二叉树转化为森林
若二叉树非空,则二叉树根的右子树棵视为其余树形成的二叉树,将其与根断开,以此类推,把所有子树释放。再将每棵二叉树依次转换成树,就得到了原森林。二叉树转换成树或森林也是唯一的。
5.4.3 树和森林的遍历
树的遍历
- 先根遍历
若树非空,先访问根结点,再依次遍历根结点的每棵子树。
先根遍历的遍历序列与对应二叉树的先序序列相同 - 后根遍历(中根遍历)
若树非空,先依次遍历根结点的每棵子树,再访问根结点。
后根遍历的遍历序列与对应二叉树的中序序列相同 - 层次遍历
森林的遍历
-
先序遍历森林
若森林非空,按如下规则进行遍历:
- 访问森林中第一棵树的根结点
- 先序遍历第一棵树中根结点的子树森林
- 先序遍历其余树的森林
-
中序遍历森林
若森林非空,按如下规则进行遍历 (实际上就是依次后根遍历森林中的每一棵树) :
- 中序遍历森林中第一棵树的根结点的子树森林
- 访问第一棵树的根结点
- 中序遍历其余树的森林
树和森林的遍历与二叉树遍历的对应关系
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先序遍历 | 先序遍历 |
后根遍历 | 中序遍历 | 中序遍历 |
5.5 树与二叉树的应用
5.5.1 哈夫曼树和哈夫曼编码
-
哈夫曼树的定义
为树中结点赋予一个数值,成为该结点的权。从根结点到任意结点的路径长度 l l l(经过的边数)与该节点上权值 w w w的乘积称为该结点的带权路径长度。树中所有叶结点的带权路径长度之和称为树的带权路径长度。即 WPL =
∑ i = 1 n w i l i \sum_{i=1}^nw_il_i i=1∑nwili
在含有n个带权叶结点的二叉树中,WPL最小的二叉树称为哈夫曼树,也称最优二叉树。 -
哈夫曼树的构造
给定n个权值分别为 w 1 , w 2 . . . w n w_1,w_2…w_n w1,w2…wn的结点,构造算法如下:
1.将n个结点分别作为n棵仅含一个结点的二叉树,构成森林F;
2.构造一个新结点,从F中选取两棵根节点权值最小的树作为新节点的左右子树,新结点的权值置为左右子树根节点权值之和;
3.从F中删除刚才选出的两棵树,同时将新得到的树加入F中;
4.重复步骤2、3,直到F仅剩一棵树。从构造过程可以看出哈夫曼树具有如下特点:
- 每个初始结点都称为叶结点,且权值越小的结点到根节点的路径越长。
- 构造过程新建了n-1个结点,因此哈夫曼树的总结点树为2n-1。
- 哈夫曼树中不存在度为1的结点。
-
哈夫曼编码
若允许不同字符用不等长的二进制位表示,称这种编码为可变长度编码。
若任何一个编码都不是其余编码的前缀,则称这种编码为前缀编码。
利用哈夫曼树可以设计出总长度最短的二进制前缀编码。