一、树的概念
一个结点有一个前驱和多个后继结点,这种数据结构是树形结构
1、树的定义
树(Tree)是n个结点的集合,集合中有一个称为根(root)的特殊节点,在根节点分布着一些互相不相交的集合,每个集合又是一个树,这些树称为根节点的子树
![345db45a8cc8b5fef9b366a234fdef87.png](https://img-blog.csdnimg.cn/img_convert/345db45a8cc8b5fef9b366a234fdef87.png)
(1)空集合也是树
(2)单个结点是一棵树,树根就是该结点本身
(3)若某树有多个结点,则每个节点都可以看成是根结点
(4)在一棵树中,有且仅有一个结点没有前驱,这个结点就是根节点
(5)除根结点外,其余每个结点有且仅有一个前驱,如图B的前驱为A,不能有其它前驱
(6)每个结点可以有任意多个后继例如:A有B、C、D三个后继
2、树的相关术语
(1)父节点、子节点、兄弟结点
每个节点的子树的根称为该结点的儿子(子结点),相应的,该结点被称为子结点的父亲(父节点),具有同一父结点的的结点称为兄弟结点,例如结点A是B、C、D的父结点,相应的,结点B、C、D就是结点A的子结点,而结点B、C、D之间是兄弟结点。对这种层次关系进行扩展,可得出祖先结点和子孙结点等关系
(2)结点的度
一个结点的子树的数量称为该结点的 "度"。例如:结点A有3个子树,因此结点A的"度"是3;而结点B、C、D的"度"分别为1、2、2
(3)树的度
一棵树的度是指该树中结点的最大度数,上图树的度为3(结点A的度为3)
4)叶结点和分支结点
树中度为零的结点称为叶节点或终端节点。树中度不为零的结点称为分支结点或非终端节点。节点E、J、K、L、M、N为叶节点
(5)节点的层数
树是一种层次结构,每个节点都是处在一定的层次上
(6)树的深度
一棵树中节点的最大层数称为树的深度。例如,图中所示的树的深度为4
(7)有序树和无序树
若树中各节点的子树(兄弟节点)是按一定次序从左向右安排的,称为有序树,否则称为无序树
(8)森林
森林是m(m>=0)课互不相交的树的集合。如果删去图中所示树的根节点A,留下的3棵子树就构成了一个森林
3、树的表示
树的表示方法很多,常用的方法是用括号:先将根节点放入一对圆括号中,然后把它的子树由左至右的顺序放入括号中,而对子树也采用同样的方法处理;同层子树与它的根节点用圆括号括起来,同层子树之间用逗号隔开,最后用闭括号括起来
( A( B(E) ) ,(C(F(J)),(G (K,L))),(D(H),(I(M,N))))
二、二叉树的概念
在树的应用中,二叉树特别重要,这是因为处理树的许多算法应用到二叉树时变得非常简单,而任意的树又可以方便的转换为对应的二叉树。因此,只需要定义二叉树的算法,然后将其它树转换为二叉树,即可方便的对所有树进行操作
1、二叉树的定义
一个二叉树是n个结点的集合,此集合要么是空集,要么是由一个根节点加上分别称为左子树和右子树的两个互不相交的二叉树
二叉树任意节点最多只能有两个子节点;如果只有一个子节点,可以是左子树也可以是右子树。因此,二叉树有5种基本形态:
(1)空
没有任何节点
(2)仅有根节点
只有根节点,没有子节点
(3)仅有左子树
(4)仅有右子树
(5)左右子树都有
![52de8c35524b537353d6139e76a583ab.png](https://img-blog.csdnimg.cn/img_convert/52de8c35524b537353d6139e76a583ab.png)
2、二叉树和树的两个主要差别如下:
(1)树中节点的最大度数没有限制,而二叉树节点的最大度数为2
(2)树的节点无左右之分,而二叉树的节点有左右之分,因为二叉树有左右之分,所以二叉树是有序树
3、特殊的二叉树
(1)满二叉树
在二叉树中,除最下一层的叶子节点外,每层的节点都有2个子节点,就构成了满二叉树
![17aad994e2b508cdae4cb1eb27dd7862.png](https://img-blog.csdnimg.cn/img_convert/17aad994e2b508cdae4cb1eb27dd7862.png)
(2)完全二叉树
除二叉树最后一层外,其它各层的节点数都达到最大个数,且最后一层从左向右的叶节点连续存在,只缺右侧若干节点,就是完全二叉树
![203da5f6c276f77691d7f91cc59084d8.png](https://img-blog.csdnimg.cn/img_convert/203da5f6c276f77691d7f91cc59084d8.png)
4、二叉树的性质
(1)在二叉树中,第i层的节点总数最多为
![4543af173fc96e2f49ed93eaf5623b01.png](https://img-blog.csdnimg.cn/img_convert/4543af173fc96e2f49ed93eaf5623b01.png)
个
(2)深度为k的二叉树最多有
![c6da4e9065e0e8b5aeac640aeb140364.png](https://img-blog.csdnimg.cn/img_convert/c6da4e9065e0e8b5aeac640aeb140364.png)
个节点(k>=1)
(3)对于一个二叉树,如果其叶节点数为n0,而度为2的节点总数为n2,则n0=n2+1
(4)具有n个节点的完全二叉树的深度k为:k = [log2n ] + 1
(5)有n个节点的完全二叉树各节点如果用顺序方式存储,对任意节点i,有如下关系:
如果i!=1,则其父节点的编号为i/2
如果2*i<=n,则其左子树根节点编号为2*i;若2*I > n,则无左子树
如果2*i + 1 <=n,则其右子树根节点编号为2*i + 1;若2*i +1 > n,则无右子树
三、二叉树的存储
二叉树通常可采用顺序存储结构和链式存储结构
1、顺序存储结构
与线性表的顺序存储类似,二叉树的顺序存储结构一般也由一个一维数组来构成,二叉树上的节点按某种规定的次序逐个保存到数组的各个单元中
二叉树顺序存储结构的数据定义如下:
#define MAXSIZE 100 //最大节点数
typedef int DATA ; //元素类型
typedef DATA seqBinTree[MAXSIZE ];
seqBinTree SBT; //定义保存二叉树组
对于一个完全二叉树,若用顺序存储,各节点之间具有对应关系。如下:
![2c8c21b02724d107ef834f973ee835ff.png](https://img-blog.csdnimg.cn/img_convert/2c8c21b02724d107ef834f973ee835ff.png)
![09627b7b2b3b8dbe25bfe2d1bbb75117.png](https://img-blog.csdnimg.cn/img_convert/09627b7b2b3b8dbe25bfe2d1bbb75117.png)
对于一个完全二叉树,若用顺序存储,各节点之间具有对应关系。例如,对于节点i,其父节点为i/2(若商不整数,则进行取整),而其左子节点的编号为2*i,右子节点的编号为2*i+1
对于节点E,由于存储在数组第5个位置,可推算出其父节点的序号为5/2 = 2,即其父节点为节点B。而如果节点E有子节点,则其左子节点的序号为2*5 =10,即其左子节点为J。同样,如果有右子节点,则其右子节点的序号为11,即节点K
如果顺序存储的二叉树不是完全二叉树,由于节点和数组中的序号没了对应关系,就比较麻烦了。为了能使节点与数组序号有对应关系,一般采用的方法是;将非完全二叉树转换为完全二叉树,将左侧缺少的节点虚设为无数据的节点(这里用“#”)
![8253bc55113c50a2c8ee5693334a682a.png](https://img-blog.csdnimg.cn/img_convert/8253bc55113c50a2c8ee5693334a682a.png)
将此图所示的完全二叉树按顺序保存到一维数组中,得到下图所示:
![7725659fdaab05a3f2f1fadba23a8702.png](https://img-blog.csdnimg.cn/img_convert/7725659fdaab05a3f2f1fadba23a8702.png)
但是,保存A---H共7个节点占用15个存储空间,造成存储空间的浪费。因此,对于二叉树的顺序存储,一般是适用于一些特殊情况(如保存完全二叉树)
2、链式存储结构
由于二叉树的顺序存储结构会造成空间浪费,因此,大多数情况下,二叉树都采用链式存储结构。使用链式存储结构非常符合二叉树的逻辑结构,一个节点可以由节点元素和两个分别指向左、右子树的指针组成
![650397a16fbccfeb852fb557fad24f77.png](https://img-blog.csdnimg.cn/img_convert/650397a16fbccfeb852fb557fad24f77.png)
四、操作二叉树
1、定义二叉树链式结构
#include
#include
#define QUEUE_MAXSIZE 50;//队列大小
typedef char DATA
typedef struct chainTree{
DATA data; //元素数据
struct chainTree *left;//左子树指针
struct chainTree *right;//右子树指针
} chainBinTree;
2、初始化二叉树
chainBinTree * binTreeInit(chainBinTree * node){//初始化二叉树根节点
if(node ! = NULL)
{
return node;
}else{
return NULL;
}
}
3、添加节点到二叉树
bt 为父节点,node为子节点,n=1表示添加左子树,n=2表示添加右子树
int binTreeAddNode(chainBinTree *bt,chainBinTre *node,int n)
{
if(bt == NULL){
printf("父节点不存在,请先设置父节点!");
return 0;
}
switch(n){
case 1:
if(bt->left)//左子树不为空
{
printf("左子树不能为空");
return 0;
}else{
bt->left = node;
}
break;
case 2:
if(bt->right)//右子树不为空
{
printf("右子树不能为空");
return 0;
}else{
bt->right = node;
}
break;
default:
printf("参数错误");
return 0;
}
return 1;
}
4、获取二叉树左右的子树
chainBinTree *binTreeLeft(chainBinTree *bt)
{
if(bt){
return bt->left;
}else{
return NULL;
}
}
chainBinTree *binTreeRight(chainBinTree *bt)
{
if(bt){
return bt->right;
}else{
return NULL;
}
}
5、获取二叉树状态
检查二叉树是否为空
int binTreeIsEmpty(chainBinTree *bt){
if(bt)
return 0;
else
return 1;
}
求二叉树的深度
int binTreeDeph(chainBinTree *bt){
int dep1,dep2;
if(bt==NULL){
return 0;//空树深度为0
}else{
//左子树深度(递归调用)
dep1 = binTreeDeph(bt->left)
//右子树深度(递归调用)
dep2 = binTreeDeph(bt->right)
if(dep1 > dep2){
return dep1 + 1;
}else{
return dep2 + 1;
}
}
}
6、在二叉树中查找
chainBinTree *binTreeFind(chainBinTree *bt,DATA data){
chainBinTree *p;
if(bt == NULL){
return NULL;
}else{
if(b->data == data){
return bt;
}else{
if(p = binTreeFind(bt->left,data))
return p;
else if(p = binTreeFind(bt->right,data))
return p;
else
return NULL;
}
}
}
该函数有2个参数,bt是需要查找的二叉树的根节点,data为需要查找的节点数据
7、清空二叉树
在向二叉树添加节点时,每个节点都由malloc函数申请分配内存,因此需要清空二叉树则必须使用free函数来释放节点所占的内存。清空二叉树的操作就是通过递归调用,对二叉树进行遍历,释放各个节点所占的内存
105 void binTreeClear(chainBinTree *bt)
106 {
107 if(bt)
108 {
109 binTreeClear(bt->left);//清空左子树
110 binTreeClear(bt->right);//清空了右子树
111 free(bt);//释放当前节点所占内存
112 bt = NULL;
113 }
114 return;
115 }
该函数有一个参数bt,就是需要清空二叉树的根节点。第107行进行判断,若当前节点bt不为空(表示还占用了系统内存),这时,不能直接使用free释放当前节点所占内存,因为该节点还可能有子树,如果直接将该节点所占内存释放,则无法再链接到其左右子树,造成内存被无效占用。因此,这里首先需要执行第109、110行递归调用当前函数,用来清空当前节点的左子树和右子树,最后在执行第111行释放当前节点所占内存
五、遍历二叉树
由于二叉树是非线性结构,那么将数据按二叉树结构保存时,怎样才能在所有节点中查找所需要的数据呢?这就需要定义一种算法,将二叉树的各个节点转换成一个线性序列来表示,以方便对各节点数据的检索,这就是二叉树的遍历
遍历是对树的一种最基本的运算,所谓遍历二叉树,就是按一定的规则和顺序走遍二叉树的所有节点,使每一个节点都被访问一次,而且只能被访问一次
如下图所示具有左右子树的二叉树,其中D表示根节点,L表示左子树,R表示右子树,对其进行遍历有3种方式:
![307bf2551bf5a017ea003b6ebba283a0.png](https://img-blog.csdnimg.cn/img_convert/307bf2551bf5a017ea003b6ebba283a0.png)
1、先序遍历(DLR):称为先根次序遍历,即先访问根节点,再按先序遍历左子树,最后按先序遍历右子树
2、中序遍历(LDR):称为中根次序遍历,即先按中序遍历左子树,再访问根节点,最后按中序遍历右子树
3、后序遍历(LRD):称为后根次序遍历,即先按后序遍历左子树,再按后序遍历右子树,最后访问根节点
先序遍历
1、算法的伪代码描述
void binTree_DLR(binTree)
{
if(二叉树不为空)
{
访问根节点;
binTree_DLR(left); //先序遍历左子树
binTree_DLR(right);//先序遍历右子树
}
}
2、代码实现
void preOrder(binTree root){
if(root != NULL ){
printf("%d",root->data);//访问根结点
preOrder(root->lchild); //先序遍历根节点的左子树
preOrder(root->rchild); //先序遍历根节点的右子树
}
}
中序遍历
void inOrder(binTree root){
if(root !=NULL ){
inOrder(root->lchild);//中序遍历根结点的左子树
printf("%d",root->data);//访问根节点
inOrder(root->rchild);//中序遍历根结点的右子树
}
}
后序遍历
void postOrder(binTree root){
if(root !=NULL ){
postOrder (root->lchild);//中序遍历根结点的左子树
postOrder (root->rchild);//中序遍历根结点的右子树
printf("%d",root->data);//访问根节点
}
}
遍历二叉树的基本操作就是访问节点,不论按照哪种次序遍历,对于含有n个节点的二叉树,遍历算法的时间复杂度都为O(n)。因为在遍历的过程中,每进行一次递归调用,都将函数的“活动记录”压入栈中,因此,栈的最大长度恰为树的高度。所以,在最坏情况下,二叉树有n个节点且高度为n的单枝树,遍历算法的空间复杂度也为O(n)
借助一个栈,可将二叉树的递归算法转换为非递归算法。下面给出中序遍历的非递归算法
int inOrderTraverse(binTree root){
initStack(st);//创建个空栈
p = root;//p指向树根节点
while(p!=NULL || !initStack(st)){
if(p!=NULL){//不是空树
push(st,p);//根节点指针进栈
p = p->lchild;//进入根的左子树
}else{
q = top(st); pop(st);//栈顶元素出栈
printf("%d",q->data);//访问根节点
p = q->rchild;//进入根的右子树
}
}
}
对于二叉树,还有层次遍历。设二叉树的根节点所在层树为1,层序遍历就是从树的根节点出发,首先访问第1层的树根节点,然后从左到右依次访问第2层上的节点,其次是第3层上的节点,以此类推,自上而下,自左至右逐层访问树中各节点的过程就是层序遍历
![46c27f7e51406a7acce5c9fff1b47aae.png](https://img-blog.csdnimg.cn/img_convert/46c27f7e51406a7acce5c9fff1b47aae.png)
上图中按照层次遍历的结果为:ABCDEFGHIJKL
//按照层次遍历
public static void binTree_level(TreeNode root){
int QUEUE_MAXSIZE = 6;
TreeNode p;
TreeNode q[]=new TreeNode[QUEUE_MAXSIZE];//定义个顺序队列
int head = 0;//定义首序号
int tail = 0;//定义尾序号
if(root!=null){
//计算循环队列队尾序号
tail = (tail + 1)%QUEUE_MAXSIZE;
q[tail] = root;
}
//队列不为空,进行循环
while(head!=tail){
//计算循环队列队首序号
head = (head + 1)%QUEUE_MAXSIZE;
//获取队首元素
p = q[head];
//若节点有左子树,则左子树进队
if(p.left!=null){
tail = (tail+1)%QUEUE_MAXSIZE;
q[tail] = p.left;
}
//若节点有右子树,则右子树进队
if(p.right!=null){
tail = (tail+1)%QUEUE_MAXSIZE;
q[tail] = p.right;
}
}
for(TreeNode obj:q){
if(obj!=null){
System.out.println(obj.value);
}
}
}
六、线索二叉树
对于二叉树进行遍历得到的节点序列,可以将遍历的结果看成是一个线性表。在该线性表中,除了第1个节点外,每个节点都有(且仅有)一个前驱;除了最后一个节点外,每个节点都有(且仅有)一个后继。例如:下列二叉树经过中序遍历后结果为
B F D A C G E H
![25ea191e789c56bb36cb9d8445aa46ca.png](https://img-blog.csdnimg.cn/img_convert/25ea191e789c56bb36cb9d8445aa46ca.png)
线索二叉树的表示
![e86f3fc08e66cfab197c70b1b3e8fdfb.png](https://img-blog.csdnimg.cn/img_convert/e86f3fc08e66cfab197c70b1b3e8fdfb.png)
当以二叉树链表形式来保存二叉树时,只能找到节点的左右子树信息,而不能直接得到节点的前驱和后继(只有通过遍历,在动态过程中才能查到前驱和后继的信息)。如果增加前驱和后继的指针那么增加了存储的开销。其实由二叉树性质可知,对于一颗具有n个节点的二叉树,对应的二叉树链表中共有2n个指针域,其中n-1个用于指向除根节点外的n-1个节点,另外n+1个指针域为空。可以利用二叉树中的这些空指针域来存放节点的前驱和后继。将每个节点中为空的左指针或右指针分别用于指向节点的前驱和后继,即可得到线索二叉树
在一个线索二叉树中,为了区别每个节点的左、右指针域所存放的是子树指针,还是线索,必须在节点结构中增加两个标志域:一个是左线索标志域lflag,另一个是右线索标志域eflag。若某个标志为1,则表示对应的指针域为线索,否则,对应的指针域为子树指针
七、哈夫曼树
1. 哈夫曼树的构造
给定N个权值分别为w1, w2, ..., Wn的节点。构造哈夫曼树的算法描述如下:
1)将这N个结点分别作为N棵树仅含一个结点的二叉树,构成森林F
2)构造一个新节点,并从F中选取两棵根结点权值最小的树作为新节点的左、右子树, 并且将新节点的权值置为左、右子树上根结点的权
值之和
3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中
4)重复步骤2和3,直至F中只剩下一棵树为止
2. 哈夫曼编码
哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码,它是可变长度编码。可变长编码即可以对待处理字符串中不同字符使用不等长的二进制位表示,可变长编码比固定长度编码好很多,可以对频率高的字符赋予短编码,而对频率较低的字符则赋予较长一些的编码,从而可以使平均编码长度减短,起到压缩数据的效果。
哈夫曼编码是前缀编码。如果没有一个编码是另一个编码的前缀,则称这样的编码为前缀编码。对前缀编码的解码也是相对简单的,因为没有一个码是另一个编码的前缀,所以可以识别出第一个编码,将它翻译为原码,再对余下的编码文件重复同样操作。
哈夫曼编码首先要构造一棵哈夫曼树,首先,将每个出现的字符当做一个独立的结点,其权值作为它出现的频度(或次数),然后构造哈夫曼树。显然所有字符节点均出现在叶子结点中。我们可以将字符的编码解释为从根至该字符路径上标记的序列,其中标记为0表示"转向左孩子",标记为1表示为"转向右孩子"。 如下图,矩形方块表示字符及出现的次数
![59d5c6c74bdba40d03d3f76e4321cfa1.png](https://img-blog.csdnimg.cn/img_convert/59d5c6c74bdba40d03d3f76e4321cfa1.png)
因此,这棵哈夫曼树的WPL为:
WPL = 1*45 + 3 * (13+12+16) + 4 * (5+9) = 224
哈夫曼树练习
1、设哈夫曼树中的节点总数为49,若用二叉链表作为存储结构,则该哈夫曼树中总共有个空指针域?
2、对于n(n大于等于2)个权值均不相同的字符构成哈夫曼树,以下叙述正确的是()
A、树中一定没有度为1的节点
B、该树一定是一颗完全二叉树
C、树中任一非叶节点的权值一定不小于子孙中任一节点的权值
D、树中两个权值最小的节点一定是兄弟节点(假设这两个节点恰好有两个)
3、若以(4、5、6、7、8)作为叶节点的权值构造哈夫曼树,则其带权路径长度是多少?
4、某段文本中各个字母出现的频率是a为4、b为3、o为12、h为7,i为10,使用哈夫曼编码,则可能的编码是
A、 a(001) 、b(000)、h(01)、i(10)、o(11)
B、 a(000) 、b(0001)、h(001)、o(01)、i(1)
C、 a(000) 、b(001)、h(01)、i(10)、o(00)
D、 a(0000) 、b(0001)、h(001)、o(000)、i(1)
5、用二进制来编码字符串“xyzwxyxx”,需要能够根据编码解码回原来的字符串,则最少需要()长度的二进制字符串
A、12 B、14 C、15 D、18 E、24