C++ 数据结构 3:树和二叉树
1 树
1.1 定义
由一个或多个(n ≥ 0)结点组成的有限集合 T,有且仅有一个结点称为根(root),当 n > 1 时,其余的结点分为 m (m ≥ 0)个互不相交的有限集合T1,T2,…,Tm。每个集合本身又是棵树,被称作这个根的子树 。
1.2 结构特点
-
非线性结构,有一个直接前驱,但可能有多个直接后继(1:n)
-
树的定义具有递归性,树中还有树。
-
树可以为空,即节点个数为0。
1.3 术语
-
根:即根结点(没有前驱)
-
叶子:即终端结点(没有后继)
-
森林:指m棵不相交的树的集合(例如删除A后的子树个数)
-
有序树:结点各子树从左至右有序,不能互换(左为第一)
-
无序树:结点各子树可互换位置。
-
双亲:即上层的那个结点(直接前驱) parent
-
孩子:即下层结点的子树 (直接后继) child
-
兄弟:同一双亲下的同层结点(孩子之间互称兄弟)sibling
-
堂兄弟:即双亲位于同一层的结点(但并非同一双亲)cousin
-
祖先:即从根到该结点所经分支的所有结点
-
子孙:即该结点下层子树中的任一结点
-
结点:即树的数据元素
-
结点的度:结点挂接的子树数(有几个直接后继就是几度)
-
结点的层次:从根到该结点的层数(根结点算第一层)
-
终端结点:即度为 0 的结点,即叶子
-
分支结点:除树根以外的结点(也称为内部结点)
-
树的度:所有结点度中的最大值(Max{各结点的度})
-
树的深度(或高度):指所有结点中最大的层数(Max{各结点的层次})
上图中的结点数= 13,树的度= 3,树的深度= 4
1.4 树的表示法
1.4.1 广义表表示法
用广义表表示法表示上图:
中国(河北(保定,石家庄),广东(广州,东莞),山东(青岛,济南))
根作为由子树森林组成的表的名字写在表的左边
1.4.2 左孩子右兄弟表示法
左孩子右兄弟表示法可以将一颗多叉树转化为一颗二叉树:
节点的结构:
节点有两个指针域,其中一个指针指向子节点,另一个指针指向其兄弟节点。
1.5 树的结构
1.5.1 逻辑结构
一对多(1:n),有多个直接后继(如家谱树、目录树等等),但只有一个根结点,且子树之间互不相交。
1.5.2 存储结构
树的存储仍然有两种方式:
- 顺序存储
可规定为:从上至下、从左至右将树的结点依次存入内存。
重大缺陷:复原困难(不能唯一复原就没有实用价值)。
- 链式存储
可用多重链表:一个前趋指针,n个后继指针。
细节问题:树中结点的结构类型样式该如何设计?
即应该设计成“等长”还是“不等长”?
缺点:等长结构太浪费(每个结点的度不一定相同);
不等长结构太复杂(要定义好多种结构类型)。
以上两种存储方式都存在重大缺陷,应该如何解决呢?
计算机实现各种不同进制的运算是通过先研究最简单、最有规律的二进制运算规律,然后设法把各种不同进制的运算转化二进制运算。树的存储也可以通过先研究最简单、最有规律的树,然后设法把一般的树转化为这种简单的树,这种树就是 二叉树。
2 二叉树
2.1 定义
n(n ≥ 0)个结点的有限集合,由 一个根结点 以及 两棵互不相交的、分别称为左子树和右子树的 二叉树 组成 。
2.2 逻辑结构
一对二(1:2)
2.3 基本特征
-
每个结点最多只有两棵子树(不存在度大于2的结点)
-
左子树和右子树次序不能颠倒(有序树)
2.4 基本形态
2.5 二叉树的性质
-
性质1: 在二叉树的第 i 层上至多有 \({2^{i-1}}\) 个结点(i > 0)
-
性质2: 深度为 k 的二叉树至多有 \({2^{k-1}}\) 个结点(k > 0)
-
性质3: 对于任何一棵二叉树,若 2 度的结点数有 n2 个,则叶子数(n0)必定为 n2+1 (即 n0 = n2 + 1)
满二叉树:一棵深度为k 且有 \({2^{k-1}}\) 个结点的二叉树。
特点:每层都“充满”了结点
完全二叉树:深度为 k 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应。
-
性质4: 具有 n 个结点的完全二叉树的深度必为 \({log_{2}n} +{1}\)
-
性质5: 对完全二叉树,若从上至下、从左至右编号,则编号为i 的结点,其左孩子编号必为 2i,其右孩子编号必为 2i+1。其双亲的编号必为i/2(i=1 时为根,除外)
使用此性质可以使用完全二叉树实现树的顺序存储。
2.2 二叉树的表示
2.2.1 二叉链表 表示法
一般从根结点开始存储。相应地,访问树中结点时也只能从根开始。
存储结构:
结点数据类型定义:
typedef struct BiTNode
{
int data;
struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;
2.2.2 三叉链表 表示法
存储结构:
每个节点有三个指针域,其中两个分别指向子节点(左孩子,右孩子),还有一共指针指向该节点的父节点。
结点数据类型定义:
//三叉链表
typedef struct TriTNode
{
int data;
//左右孩子指针
struct TriTNode *lchild, *rchild;
struct TriTNode *parent;
}TriTNode, *TriTree;
2.2.3 双亲 表示法
存储结构:
每个节点都由一个数据结构组成,每个节点顺序排放在数组中。
结点数据类型定义:
//双亲链表
#define MAX_TREE_SIZE 100
typedef struct BPTNode
{
int data; // 数据
int parentPosition; //指向双亲的指针,数组下标
char LRTag; //左右孩子标志域
}BPTNode;
typedef struct BPTree
{
//因为节点之间是分散的,需要把节点存储到数组中
BPTNode nodes[100];
int num_node; //节点数目
//根结点的位置,注意此域存储的是父亲节点在数组的下标
int root;
}BPTree;
2.3 二叉树的遍历
定义:指按某条搜索路线 遍访每个结点且不重复。
遍历方法:
牢记一种约定,对每个结点的查看都是“先左后右” 。
限定先左后右,树的遍历有三种实现方案:
DLR LDR LRD
先 (根)序遍历 中 (根)序遍历 后(根)序遍历
-
DLR:先序遍历,即先根再左再右
-
LDR:中序遍历,即先左再根再右
-
LRD: 后序遍历,即先左再右再根
注:“先、中、后”的意思是指访问的结点 D 是先于子树出现还是后于子树出现。
2.3.1 先序遍历
PreOrder(NODE *root )
{
if (root) //非空二叉树
{
printf(“%d”,root->data); //访问D
PreOrder(root->lchild); //递归遍历左子树
PreOrder(root->rchild); //递归遍历右子树
}
}
2.3.2 中序遍历
InOrder(NODE *root)
{
if(root !=NULL)
{
InOrder(root->lchild);
printf(“%d”,root->data);
InOrder(root->rchild);
}
}
2.3.3 后序遍历
PostOrder(NODE *root)
{
if(root !=NULL)
{
PostOrder(root->lchild);
PostOrder(root->rchild);
printf(“%d”,root->data);
}
}
2.3.4 三种遍历的本质
从前面的三种遍历算法可以知道:如果将printf语句抹去,除去printf的遍历算法:
XXX (NODE *root)
{
if(root)
{
XXX(root->lchild);
XXX(root->rchild);
}
}
从递归的角度看,这三种算法是完全相同的,或者说这三种遍历算法的访问路径是相同的,只是访问结点的时机不同。
从虚线的出发点到终点的路径上,每个结点经过 3 次。
-
第 1 次经过时访问=先序遍历
-
第 2 次经过时访问=中序遍历
-
第 3 次经过时访问=后序遍历
2.4 二叉树的创建
2.4.1 先序和中序 创建树
算法
-
通过先序遍历找到根结点A,再通过A在中序遍历的位置找出左子树,右子树
-
在A的左子树中,找左子树的根结点(在先序中找),转步骤1
-
在A的右子树中,找右子树的根结点(在先序中找),转步骤1
练习1:
先序遍历结果:A D E B C F
中序遍历结果:D E A C F B
解:
练习2:
先序遍历结果:A B D H K E C F I G J
中序遍历结果:H K D B E A I F C G J
解:
2.4.2 # 号法 创建树
什么是 # 号法创建树:# 创建树,让树的每一个节点都变成度数为 2 的树
例子1: 124###3##
解:
例子2:先序遍历:A B D H # K # # # E # # C F I # # # G # J # #,请画出树的形状
解:
# 号法编程实践:
利用前序遍历来建树:
Bintree createBTpre( )
{ Bintree T; char ch;
scanf(“%c”,&ch);
if(ch==’#’) T=NULL;
else
{ T=( Bintree )malloc(sizeof(BinTNode));
T->data=ch;
T->lchild=createBTpre();
T->rchild=createBTpre();
}
return T;
}
使用后序遍历的方式销毁一棵树, 先释放叶子节点,在释放根节点:
void BiTree_Free(BiTNode* T)
{
BiTNode *tmp = NULL;
if (T!= NULL)
{
if (T->rchild != NULL) BiTree_Free(T->rchild);
if (T->lchild != NULL) BiTree_Free(T->lchild);
if (T != NULL)
{
free(T);
T = NULL;
}
}
}
3 霍夫曼树
3.1 概念
组建一个网络,耗费最小 WPL(树的带权路径长度)最小,称为 霍夫曼树。
从树中一个节点到另一个节点之间的分支构成两个节点之间的路径,路径上的分支数目称作 路径长度。
例子:
如下图的二叉树 a 中,根节点到节点 D 的路径长度为 4,二叉树 b 中根节点到节点 D 的路径长度为 2。树的路径长度就是从树根到每一节点的路径长度之和。二叉树 a 的树路径长度就为 1+1+2+2+3+3+4+4=20。二叉树 b 的树路径长度为 1+2+3+3+2+1+2+2=16。
3.2 霍夫曼树的构造
对于文本”BADCADFEED”的传输而言,因为重复出现的只有“ABCDEF”这6个字符,因此可以用下面的方式编码:
A | B | C | D | E | F |
---|---|---|---|---|---|
000 | 001 | 010 | 011 | 100 | 101 |
B A D C A D F E E D 001 000 011 010 000 011 101 100 100 011
接收方可以根据每 3 个 bit 进行一次字符解码的方式还原文本信息。这样的编码方式需要 30 个 bit 位才能表示 10 个字符那么当传输一篇 500 个字符的情报时,需要 15000 个 bit 位,在战争年代,这种编码方式对于情报的发送和接受是很低效且容易出错的。如何提高收发效率?
A | B | C | D | E | F |
---|---|---|---|---|---|
01 | 1001 | 101 | 00 | 11 | 1000 |
B A D C A D F E E D 1001 01 00 101 01 00 1000 11 11 00
准则:任一字符的编码都不是另一个字符编码的前缀!
也就是说:每一个字符的编码路径,都不包含另外一个字符的路径。
3.2.1 构建规则
-
给定 n 个数值{ v1, v2, …, vn}。
-
根据这 n 个数值构造二叉树集合 F = { T1, T2, …, Tn},Ti 的数据域为 vi,左右子树为空。
-
在 F 中选取两棵根结点的值最小的树作为左右子树构造一棵新的二叉树,这棵二叉树的根结点中的值为左右子树根结点中的值之和
-
在 F 中删除这两棵子树,并将构造的新二叉树根节点加入 F 中
-
重复 3 和4,直到 F 中只剩下一个树为止。
这棵树即 霍夫曼树
3.2.2 特点
-
所用的字符都作为叶子节点出现。
-
根到每个字符的路径都不重复,也不存在重叠的现象。
3.2.3 特点
-
霍夫曼树是一种特殊的二叉树。
-
霍夫曼树应用于信息编码和数据压缩领域。
-
霍夫曼树是现代压缩算法的基础。