树(Tree)
一、树的基本概念
1、树的概念
一类重要的非线性数据结构,是以分支关系定义的层次结构
2、树的定义
由n(n>=0)个结点组成的有限集合T。当n=0时成为空树,非空树满足:
1)有一个称之为根(root)的结点.
2)除根以外的其余结点被分为m(0<=m<n)个互不相同的集合T1,T2…Tm, 其中每一个集合本身又是一棵树,且称为根的子树。
3、树的特点
除根结点外,每个结点仅有一个前驱(父)结点
树中的结点数等于所有结点的度数加1.
4、基本术语
1)结点的度:结点拥有的子树数目
2)叶子(终端)结点:度为0的结点
3)分支(非终端)结点:度不为0的结点
4)树的度:树的各结点度的最大值
5)内部结点:除根结点之外的分支结点
6)双亲与孩子结点:结点的子树的根称为该结点的孩子;该结点称为孩子的双亲
7)兄弟:属于同一双亲的孩子
8)结点的祖先:从根到该结点所经分支上的所有结点
9)结点的子孙:该结点为根的子树中的任一结点
10)结点的层次:表示该结点在树中的相对位置。根为第一层,其他的结点依次下推;若
结点在第L层上,则其孩子在第L+1层上
11)堂兄弟:双亲在同一层的结点互为堂兄弟
12)树的深(高)度:树中结点的最大层次
13)有序树:树中各结点的子树从左至右是有次序的,不能互换。否则,称为无序树
14)路径长度:从树中某结点Ni出发,能够“自上而下”通过树中结点到达结点Nj,则称Ni到Nj存在
一条路径,路径长度等于这两个结点之间的分支数
15)树的路径长度:从根到每个结点的路径长度之和。
16)森林:是m(m≥0)棵互不相交的树的集合
5、基本操作
二、树的存储结构
1、双亲表示法
我们假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示器指示其双亲结点到链表中的位置。也就是说,每个结点除了知道自已是谁以外,还知道它的双亲在哪里。
其中data是数据域,存储结点的数据信息。而parent是指针域,存储该结点的双亲在数组中的下标。
以下是双亲表示法的结点结构定义代码。
/*树的双亲表示法结点结构定义*/
#define MAX_TREE_SIZE 100
typedef int ElemType; //树结点的数据类型,目前暂定为整型
/*结点结构*/
typedef struct PTNode{
ElemType data; //结点数据
int parent; //双亲位置
}PTNode;
/*树结构*/
typedef struct{
PTNode nodes[MAX_TREE_SIZE]; //结点数组
int r, n; //根的位置和结点数
}PTree;
2、孩子表示法
把每个结点的孩子结点排列起来,以单链表作存储结构,则n个结点有n个孩子链表,如果是叶子结点则此单链表为空。然后n个头指针又组成-一个线性表,采用顺序存储结构,存放进一个一维数组中。
为此,设计两种结点结构,一个是孩子链表的孩子结点。
其中child是数据域,用来存储某个结点在表头数组中的下标。next 是指针域,用来存储指向某结点的下一个孩子结点的指针。
另一个是表头数组的表头结点。
其中data是数据域,存储某结点的数据信息。firstchild 是头指针域,存储该结点的孩子链表的头指针。
以下是我们的孩子表示法的结构定义代码。
/*树的孩子表示法结构定义*/
#define MAX_TREE_SIZE 100
/*孩子结点*/
typedef struct CTNode{
int child;
struct CTNode *next;
}*ChildPtr;
/*表头结点*/
typedef struct{
ElemType data;
ChildPtr firstchild;
}CTBox;
/*树结构*/
typedef struct{
CTBox nodes[MAX_TREE_SIZE]; //结点数组
int r, n; //根的位置和结点数
}
3、孩子兄弟表示法
任意一棵树, 它的结点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。 因此,我们设置两个指针,分别指向该结点的第一个孩子和此结点的右兄弟。
其中data是数据域,firstchild 为指针域,存储该结点的第一个孩子结点的存储地址,rightsib 是指针域,存储该结点的右兄弟结点的存储地址。
/*树的孩子兄弟表示法结构定义*/
typedef struct CSNode{
Elemtype data;
struct CSNode *firstchild, *rightsib;
} CSNode, *CSTree;
二叉树
一、二叉树的概念
1、二叉树的概念
(1)定义
二叉树是n(n>=0)个结点的有限集合,它或为空树(n=0),或由一个根结点和两棵互不相交的左子树和右子树的二叉树组成。
(2)特点
定义是递归的;0<=结点的度<=2;是有序树
(3)二叉树的五种基本形态
(4)两种特殊的二叉树
满二叉树:每一层上的结点数都是最大结点数。
完全二叉树:只有最下面两层结点的度可小于2,而最下一层的叶结点集中在左边若干位置上。
2、二叉树的性质
(1)性质1 二叉树的第i层上至多有2^(i-1) (i≥1)个结点
(2)性质2 深度为k的二叉树至多有2^k-1个结点(k≥1)
(3)性质3 对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1
(4)性质4 具有n个结点的完全二叉树的深度为[log2(n)]+1
(5)性质5 一棵具有n个结点的完全二叉树(又称顺序二叉树)对其结点按层从上至下(每层从左至右)进行1-n的编号,则对任一结点i(1≤i≤n)有:
若i>1,则i的双亲是[i/2];若i=1,则i是根,无双亲。
若2i≤n,则i的左孩子是2i;否则,i无左孩子
若2i+1≤n,则i的右孩子是2i+1;否则,i无右孩子
二、二叉树的存储结构
1、顺序存储结构
(1)完全二叉树
按完全二叉树编号存放(由1-n)
(2)三元组
存储节点数据和左右孩子在向量中的序号
双亲:存储节点数据和其父结点的序号
2、二叉链表
(1)图表说明
(2)类型定义
typedef struct btnode{
btnode *lchild,rchild;
ElemType data;
}BiTNode,*BiTree;//定义指针变量,用来存放根结点地址,通常用该指针表示一个二叉树
结论:若一个二叉树含有n个结点,则它的二叉链表中必含有2n个指针域,其中必含有n+1个空的链表
3、三叉链表/带双亲的二叉链表
(1)图表说明
(左孩子父母为正值,右孩子父母为负值)
(2)类型定义
typedef struct btnode{
btnode *lchild,rchild;
btnode *parent;
ElemType data;
}*BiTree;
三、遍历二叉树
概念:二叉树的遍历( traversing binary tree )是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
目的:非线性结构->线性结构
注:三种遍历算法中,递归遍历左、右子树的顺序都是固定的,只是访问根结点的顺序不同。不管采用哪种遍历算法,每个结点都访问一次且仅访问一次,故时间复杂度都是O(n)。在递归遍历中,递归工作栈的栈深恰好为树的深度,所以在最坏情况下,二叉树是有n个结点且深度为n的单支树,遍历算法的空间复杂度为O(n)
1、先序遍历
(1)操作过程(PreOrder)
若二叉树为空,则什么也不做,否则(DLR)
1)访问根结点;
2)先序遍历左子树;
3)先序遍历右子树
(2)递归算法
void PreOrder1(BTree T){
if(T){
visit(T);
PreOrder(T->lchild);
PreOrder(T->rchild);
}
}
(3)另一种描述
void PreOrder2(BTree T){
visit(T);
if(T->lchild) PreOrder(T->lchild);
if(T->rchild) PreOrder(T->rchild);
}
(4)消除尾递归的递归算法
void PreOrder3(BiTree T){
while(T){
visit(T);
PreOrder3(T->lchild);
T=T->rchild;
}
(5)非递归算法
先将根结点压入栈,然后根结点出栈并访问根结点,而后依次将根结点的右孩子、左孩子入栈,直到栈为空为止
void PreOrder4(BiTree T){
Inistack(s);
p=bt;
Push(s,NULL);
while(p){
visit(p);
if(p->rchild) Push(s,p->rchild);
if(p->lchild) p=p->lchild;
else p=Pop(s);
}
}
2、中序遍历
(1)操作过程(InOrder)
若二叉树为空,则什么也不做,否则(LDR)
1)中序遍历左子树;
2)访问根结点;
3)中序遍历右子树。
(2)递归算法
void InOrder1(BiTree T){
if(T){
Inorder(T->lchild);
visit(T);
Inorder(T->rchild);
}
}
(3)非递归算法
1.沿着根的左孩子,依次入栈,直到左孩子为空,说明已找到可以输出的结点。
2.栈顶元素出栈并访问:若其右孩子为空,继续执行步骤2;若其右孩子不空,将右子树转执行步骤1。
typedef struct node{
char data;
struct node*lchild,*rchild;
}*Bitree;
void Inorder2(Bitree bt){
Bitree p;
p=bt; //p是遍历指针
Initstack(s); //初始化栈S
while(p||!IsEmpty(s)){ //栈不空或p不空时循环
if(p){
Push(s,p); //当前节点入栈
p=p->lchild; //左孩子不空,一直向左走
}else{
Pop(s,p); //栈顶元素出栈
visit(p); //访问出栈结点
printf("%c",p->data);
p=p->rchild; //向右子树走,p赋值为当前结点的右孩子
}
}
}
3、后序遍历
(1)操作过程(PostOrder)
若二叉树为空,则什么也不做,否则(LRD)
1)后序遍历左子树;
2)后序遍历右子树;
3)访问根结点。
(2)递归算法
void PostrOrder(BiTree T,void(*visit)(BiTree)){
if(T){
PostrOrder(T->lchild);
PostrOrder(T->rchild);
visit(T);
}
}
(3)非递归算法
1.沿着根的左孩子,依次入栈,直到左孩子为空
2.读栈顶元素:若其右孩子不空且未被访问过,将右子树转执行1;否则,栈顶元素出栈并访问。
void PostOrder2(BiTree T){
InitStack(S);
p = T;
r = NULL;
while(p || !IsEmpty(S)){
if(p){ //走到最左边
push(S, p);
p = p->lchild;
}else{ //向右
GetTop(S, p); //读栈顶元素(非出栈)
//若右子树存在,且未被访问过
if(p->rchild && p->rchild != r){
p = p->rchild; //转向右
push(S, p); //压入栈
p = p->lchild; //再走到最左
}else{ //否则,弹出结点并访问
pop(S, p); //将结点弹出
visit(p->data); //访问该结点
r = p; //记录最近访问过的结点
p = NULL;
}
}
}
}
4、层次遍历
(1)操作过程
要进行层次遍历,需要借助一个队列。先将二叉树根结点入队,然后出队,访问出队结点,若它有左子树,则将左子树根结点入队;若它有右子树,则将右子树根结点入队。然后出队,访问出队结…如此反复,直至队列为空。
(2)遍历算法
void LevelOrder(BiTree T){
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q, T); //将根节点入队
while(!IsEmpty(Q)){ //队列不空则循环
DeQueue(Q, p); //队头结点出队
visit(p); //访问出队结点
if(p->lchild != NULL){
EnQueue(Q, p->lchild); //左子树不空,则左子树根节点入队
}
if(p->rchild != NULL){
EnQueue(Q, p->rchild); //右子树不空,则右子树根节点入队
}
}
}
5、由遍历序列构造二叉树
(1)先序序列和中序序列
在先序遍历序列中,第一个结点一定是二叉树的根结点;
在中序遍历中,根结点必然将中序序列分割成两个子序列,前一个子序列是根结点的左子树的中序序列,后一个子序列是根结点的右子树的中序序列。根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。
在先序序列中,左子序列的第一个结点是左子树的根结点,右子序列的第一个结点是右子树的根结点。
如此递归地进行下去,便能唯一地确定这棵二叉树
(2)后序序列和中序序列
后序序列的最后一个结点就如同先序序列的第一个结点,可以将中序序列分割成两个子序列,然后采用类似的方法递归地进行划分,进而得到一棵二叉树。
(3)层序序列+前序/中序/后序序列
要注意的是,若只知道二叉树的先序序列和后序序列,则无法唯一确定一棵二叉树。
四、树和森林
1、树转换为二叉树
(1)规则
每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟,这个规则又称“左孩子右兄弟”。由于根结点没有兄弟,所以对应的二叉树没有右子树。
(2)画法
1)在兄弟结点之间加一连线;
2)对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉;
3)以树根为轴心,顺时针旋转45°
2、森林转化为二叉树
画法
1)先将森林里的每一棵树转换成一棵二叉树;
2)从最后一棵树开始,把后一棵树的作为前一棵树的根的右子
3、树的遍历
先序遍历:若树非空,先访问根结点,再依次遍历根结点的每棵子树
后序遍历:若树非空,先依次遍历根结点的每棵子树,再访问根结点
4、森林的遍历
(1)先序遍历森林
若森林为非空,则按如下规则进行遍历:
●访问森林中第一棵树的根结点。
●先序遍历第一棵树中根结点的子树森林。
●先序遍历除去第一棵树之后剩余的树构成的森林。
(2)后序遍历森林
森林为非空时,按如下规则进行遍历:
●后序遍历森林中第一棵树的根结点的子树森林。
●访问第一棵树的根结点。
●后序遍历除去第一棵树之后剩余的树构成的森林。
(3)中序遍历森林
森林为非空时,按如下规则进行遍历:
●中序遍历森林中第一棵树的根结点的子树森林。
●访问第一棵树的根结点。
●中序遍历除去第一棵树之后剩余的树构成的森林。
树与二叉树的应用
一、二叉树的基本操作
1、初始建立一棵空的不带头结点的二叉树
BiTree Initiate(){
BiTNode *bt;
bt=NULL;
return bt;
}
2、生成一棵以x为根结点的数据域值以lbt和rbt为左右子树的二叉树
BiTree Create(ElemType x,BiTree lbt,BiTree rbt){
BiTree p;
if(p(BiTNode*)malloc(sizeof(BiTNode))==NULL) return NULL;
p->data=x;
p->lchild=lbt;
p->rchild=rbt;
return p;
}
3、建立二叉树的二叉链表
void CreateBiTree(BtTree *T)
{
char ch;
scanf("%c",&ch);
if(c==' ')
*T=NULL;
else{
*T=(BtNode*)malloc(sizeof(BtNode));
*T->data=ch;
CreateBiTree(&(*T->lchild));
CreateBiTree(&(*T->rchild));
}
}
4、在二叉树bt中的parent所指结点和其左/右子树之间插入数据元素为x的结点
BiTree InsertL(BiTree bt,ElemType x,BiTree parent){
BiTree p;
if(parent==NULL){
printf("/n插入出错");
return NULL;
}
if((p=(BiTNode*)malloc(sizeof(BiTNode)))==NULL)
return NULL;
p->data=x;
p->lchild=NULL;
p->rchild=NULL;
if(parent->lchild==NULL) parent->lchild=p;
else{
p->lchild=parent->lchild;
parent->lchild=p;
}
return bt;
}
BiTree InsertR(BiTree bt,ElemType x,BiTree parent){
BiTree p;
if(parent==NULL){
printf("/n插入出错");
return NULL;
}
if((p=(BiTNode*)malloc(sizeof(BiTNode)))==NULL)
return NULL;
p->data=x;
p->lchild=NULL;
p->rchild=NULL;
if(parent->rchild==NULL) parent->lchild=p;
else{
p->rchild=parent->rchild;
parent->rchild=p;
}
return bt;
}
5、在二叉树bt中删除parent的左/右子树
BiTree Delete(BiTree bt,BiTree parent){
BiTree p;
if(parent==NULL||parent->lchild==NULL){
printf("\n删除出错");
return NULL;
}
p=parent->lchild;
parent->lchild==NULL;
free(p);
return bt;
}
BiTree Delete(BiTree bt,BiTree parent){
BiTree p;
if(parent==NULL||parent->rchild==NULL){
printf("\n删除出错");
return NULL;
}
p=parent->rchild;
parent->rchild==NULL;
free(p);
return bt;
}
二、二叉树的遍历应用
1、求后序遍历的第一个结点//叶结点
思路:当指针为空时,返回NULL;当指针不为0时,进入循环,判断是否左右孩子存在,利用前序遍历法,若左孩子存在,指针指向左孩子,若左孩子不存在,指针指向右孩子,如此往复,直至左右孩子均不存在,返回最终结点值。
typedef struct btnode{
btnode *lchild,*rchild;
ElemType data;
}BiTNode,*BiTree;
BiTree firstnode(BiTree root)
{
if(!root) return NULL;//当指针为0时,返回NULL
while(root->lchild||root->rchild){
if(root->lchild) root=root->lchild;//若左孩子存在,指针指向左孩子
else root=root->rchild;//若左孩子不存在,指针指向右孩子
}
return root;//返回最终结点值
}
2、在二叉树中查找结点值为x的结点
思路:当指针不为0且判断值不为0时,如果指针指向的值等于x,则将q赋值为该指针,判断值赋为1;如果指针指向的值不等于x,则利用前序遍历法,依次遍历左右子树,查找x值。
/*
{ ...
q=NULL;
bool F=FALSE;
pre_find(bt,x,q);
...}
*/
void pre_find(BiTree bt,ElemType x,BiTree &q){
if(bt&&F==FALSE){
if(bt->data==x){
q=bt;
F=TRUE;
}
else{
pre_find(bt->lchild,x,q);
pre_find(bt->rchild,x,q);
}
}
}
3、求二叉树中每个结点所处的层次
思路:首先判断指针为空,然后打印该指针指向的结点值,以及所对应的层次;利用前序遍历法,遍历左右子树,依次打印每个结点所处的层次,注意每进行一次左右子树的遍历,其level值+1。
/*{ ...
pre_level(bt,1);
...}
*/
void pre_level(BiTree p,int level){
if(p){
write(p->data,level);//实现时可以用printf代替
pre_level(p->lchild,level+1);
pre_level(p->rchild,level+1);
}
}
``
### 4、求二叉树的高度
思路1:首先判断指针是否为空,然后判断如果h(初始化为0)小于该结点值的level(深度),则令h=level;利用前序遍历法,依次进行h与该结点所处的level值的比较,注意每进行一次左右子树的遍历,其level值+1,最终得到二叉树的高度。
```cpp
/*{ ...
h=0;
pre_height(bt,1);
...}
*/
void pre_height(BiTree p,int level){
if(p){
if(h<level) h=level;
pre_height(p->lchild,level+1);
pre_height(p->rchild,level+1);
}
}
思路2:
/*
{ ...
post_height(bt,h);
...}
*/
void post_height(BiTree p,int &h){
if(bt==NULL) h=0;
else{
post_height(bt->lchild,h1);
post_height(bt->rchild,h2);
h=1+max(h1,h2);
}
}
5、复制一棵二叉树
思路:当指针为空时,创建一个新的二叉树,其指针为q;将bt指针指向的结点值赋给q指针指向的结点值;利用前序遍历法,进行重复操作。
/*{ ...
pre_copy(bt,q);
...}
*/
void pre_copy(BiTree bt,BiTree &q){
if(bt){
new(q);
q->data=bt->data;
pre_copy(bt->lchild,q->lchild);
pre_copy(bt->rchild,q->rchild);
}else q=NULL;
}
6、统计叶子结点的个数
思路:利用递归,若指针为空,则返回0;若左孩子和右孩子均不存在,则返回1;其他情况,返回 CountLeaf(bt->lchild)+CountLeaf(bt->rchild);
int CountLeaf(BiTree bt)
{
if(bt==NULL) return 0;
else if(!bt->lchild&&!bt->rchild) return 1;
else return (CountLeaf(bt->lchild)+CountLeaf(bt->rchild));
}
7、判断两棵二叉树的相似性
思路:判断两棵树的指针是否均为空,若均为空,则返回true;若p、q的非空性不一致,则返回false;若一致,再对左右子树进行下一步的比对。
bool Similar(BiTree p,BiTree q)
{
if(p==NULL&&q==NULL) return true;
else if(!p&&q||p&&!q) return false;
else return (Similar(p->lchild,q->lchild)&&Similar(p->rchild,p->rchild));
}
8、已知一棵二叉树按顺序方式存储在数组A[0…n]中(A[0]不存放结点信息),设计算法求下标分别为i和j的两个结点最近公共祖先结点
思路:利用while循环判读i与j是否相等,若不相等且i>j,则令i=i/2,即下标为i的结点的双亲结点的下标;若不相等且i<j,则令j=j/2,即下标为j的结点的双亲结点的下标;循环结束,则i=j,找到最近的共同祖先结点,返回i值。
int forefather(ElemType A[],int i,int j,int n){
while(i!=j)
if(i>j) i=i/2;//下标为i的结点的双亲结点的下标
else j=j/2;//下标为j的结点的双亲结点的下标
return i;//返回最近祖先结点的下标
}
9、求二叉树所有结点的左右子树相互交换的算法
思路:判断指针是否为空,如果指针为空,则返回;交换左右孩子;利用先序遍历法,进行重复操作。
void exchange(BiTree T)
{
BiTree p;
if(T==NULL) return ;
p=T->lchild;
T->lchild=T->rchild;
T->rchild=p;
exchange(T->lchild);
exchange(T->rchild);
}
10、中序遍历输出
void InOrderOut(BiTree T)
{
BiTree p,q;
if(T)
{
printf("\n%3c",T->data);
p=T->lchild;
q=T->rchild;
if(p) printf("l:%3c",p->data);
if(q) printf("r:%3c",q->data);
InOrderOut(T->lchild);
InOrderOut(T->rchild);
}
}
三、哈夫曼树和哈夫曼编码
1、哈夫曼树的定义和原理
(1)结点的权
树中结点常常被赋予一个表示某种意义的数值
(2)树的路径长度
从树根到每个结点的路径上的分支数。
(3)结点的带权路径长度
从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积。
(4)树的带权路径长度
树中所有叶结点的带权路径长度之和称为,记为
(5)最优二叉树(哈夫曼树)
带权路径WPL最小的二叉树
2、哈夫曼树的构造步骤:
(1)基本思想
使权大的结点靠近根
(2)步骤
1)先把有权值的叶子结点按照从大到小(从小到大也可以)的顺序排列成一个有序序列。
2)取最后两个最小权值的结点作为一个新节点的两个子结点,注意相对较小的是左孩子。
3)用第2步构造的新结点替掉它的两个子节点,插入有序序列中,保持从大到小排列。
4)重复步骤2到步骤3,直到根节点出现。
(3)代码实现
代码实现中,单个结点的类型定义如下:
//单个结点的信息
typedef struct {
char c;
unsigned int w;//结点权值
int parent; //父节点
int lchild, rchild; //左右孩子
} HTNode,*HuffmanTree;
代码实现时,我们用一个数组存储构建出来的哈夫曼树中各个结点的基本信息(权值、父结点、左孩子以及右孩子)。该数组的基本布局如下:
用数字7、5、2、4构建一棵哈夫曼树为例:
第一阶段
所构建的哈夫曼树的总结点个数为2 × 4 − 1 = 7 ,但是这里我们开辟的数组可以存储8个结点的信息,因为数组中下标为0的位置我们不存储结点信息;
先将用于构建哈夫曼树的数字7、5、4、2依次赋值给数组中下标为1-4的权值位置,其余信息均初始化为0
第二阶段
·从数组中下标为1-4的元素中,选取权值最小,并且父结点为0(代表其还没有父结点)的两个结点,生成它们的父结点:
1、下标为5的结点的权值等于被选取的两个结点的权值之和。
2、两个被选取的结点的父结点就是下标为5的结点。
3、下标为5的结点左孩子是被选取的两个结点中权值较小的结点,另外一个是其右孩子。
·再从数组中下标为1-5的元素中,选取权值最小,并且父结点为0的两个结点,生成它们的父结点。
·继续从数组中下标为1-6的元素中,选取权值最小,并且父结点为0的两个结点,生成它们的父结点。
·此时,除了下标为0的元素以外,数组中所有元素均已有了自己的结点信息,哈夫曼树已经构建完毕。
·观察该数组中的数据,我们可以发现,权值为7、5、4、2的结点的左孩子和右孩子均为0,也就是它们没有左右孩子,因为它们是叶子结点。此外,数组中父结点为0的结点其实就是所构建的哈夫曼树的根结点。
代码如下
//在下标为1到i-1的范围找到权值最小的两个值的下标,其中s1的权值小于s2的权值
void Select(HuffmanTree HT, int n, int* s1, int* s2){
int min1 = 0, min2 = 0;
unsigned int value = 65535;//内存地址最大值
for (int i = 1; i <= n; i++)//选出权值最小,并且父结点为0的两个结点
if (HT[i].w < value && HT[i].parent == 0){
min = i;
value = HT[s].w;
}
value = 65535;
for (int j = 1; j <= n; j++)
if (HT[j].w < value && j != min1 && HT[j].parent == 0){
min2 = j;
value = HT[t].w;
}
*s1 = min1;
*s2 = min2;
}
//建立哈夫曼树
int BuildTree(HuffmanTree* ptrHT, Data* ptr, int n) {
int m = 0, i = 0, s1 = 0, s2 = 0;
m = 2 * n - 1;//哈夫曼树总结点数
if (n <= 1)//若数的度小于等于1,则哈夫曼树不存在,返回FALSE
return FALSE;
HuffmanTree HT; //初始化哈夫曼树
HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode));//申请空间
if (!HT) //若哈夫曼树为空,返回FALSE
return FALSE;
for (i = 1; i <= n; i++){
HT[i].c = ptr[i - 1].inputstring;//将相应的字符赋值给n个叶子结点
HT[i].w = ptr[i - 1].weight;//将算得的权值赋权值给n个叶子结点
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
//初始化
for (; i <= m; i++){
HT[i].c = 0;
HT[i].w = 0;
HT[i].parent = 0;
HT[i].lchild = 0;
HT[i].rchild = 0;
}
//根据结点权值构建哈夫曼树
for (i = n + 1; i <= m; i++){
Select(HT, i - 1, &s1, &s2);
HT[s1].parent = i;
HT[s2].parent = i;
HT[i].lchild = s1;
HT[i].rchild = s2;
HT[i].w = HT[s1].w + HT[s2].w;
}
*ptrHT = HT;
return TRUE;
//打印哈夫曼树中各结点之间的关系
printf("哈夫曼树为:>\n");
printf("下标 权值 父结点 左孩子 右孩子\n");
printf("0 \n");
for (int i = 1; i <= m; i++){
printf("%-4d %-6.2lf %-6d %-6d %-6d\n", i, HT[i].w, HT[i].parent, HT[i].lchild, HT[i].rchild);
}
printf("\n");
}
3、哈夫曼编码
(1)概念
1)用于通信和数据传送中字符的二进制编码,可以使电文编码总长度最短
2)哈夫曼编码是不等长编码
哈夫曼编码是前缀编码,即任一字符的编码都不3)是另一字符编码的前缀
4)哈夫曼编码树中没有度为1的结点。若叶子结点的个数为n,则哈夫曼编码树的结点总数为2n-1
(2)发送过程
根据哈夫曼树得到的编码表送出字符数据
(3)接收过程
按左0右1规定,从根结点走到一个叶结点,完成一个字符的译码。反复此过程,直到接收数据结束
(4)代码实现
第一阶段
因为数据个数为4,所以我们开辟一个大小为4的辅助空间,并将最后一个位置赋值为’\0’,用于暂时存放正在生成的哈夫曼编码。
为了存放这4个数据哈夫曼编码,我们开辟一个字符指针数组,该数组中有5个元素,每个元素的类型为char**,该字符指针数组的基本布局如下:
注意:这里为了与“构建哈夫曼树时所生成的数组”中的下标相对应,所以该字符指针数组中下标为0的元素也不存储有效数据。
第二阶段
利用已经构建好的哈夫曼树,生成这4个数据的哈夫曼编码。单个数据生成哈夫曼编码的过程如下:
1.判断该数据结点与其父结点之间的关系,若该数据结点是其父结点的左孩子,则将start指针前移,并将0填入start指向的位置,若是右孩子,则在该位置填1。
2.接着用同样的方法判断其父结点与其父结点的父结点之间的关系,直到待判断的结点为哈夫曼树的根结点为止,该结点的哈夫曼编码生成完毕。
3.将字符串中从start的位置开始的数据拷贝到字符指针数组中的相应位置。
注意:在每次生成数据的哈夫曼编码之前,先将start指针指向’\0’。
哈夫曼编码的生成函数
//生成哈夫曼编码
int GetCode(HuffmanTree* p_HT, Data** pptr, int n) {
char* code;
int start = 0, c = 0, f = 0, i = 0;
HuffmanTree HT;
Data* ptr = *pptr;
BuildTree(&HT, ptr, n);//建立哈夫曼树
code = (char*)malloc(n * sizeof(char));//开n+1个空间,因为下标为0的空间不用
if (!code) return FALSE;
code[n - 1] = '\0';//辅助空间最后一个位置为'\0'
for (i = 1; i <= n; i++){
start = n - 1;//每次生成数据的哈夫曼编码之前,先将start指针指向'\0'
for (c = i, f = HT[i].parent; f != 0; c = f, f = HT[f].parent)
//正在进行的第i个数据的编码,找到该数据的父结点
if (HT[f].lchild == c)//如果该结点是其父结点的左孩子,则编码为0,否则为1
code[--start] = '0';
else
code[--start] = '1';
ptr[i - 1].encode = (char*)malloc((n - start) * sizeof(char));//开辟用于存储编码的内存空间
if (!ptr[i - 1].encode) return FALSE;
strcpy(ptr[i - 1].encode, &cd[start]);//将编码拷贝到字符指针数组中的相应位置
}
free(code);//释放辅助空间
*pptr = ptr;
*p_HT = HT;
return TRUE;
printf("hello");
}
//打印字符对应的编码
int PrintCode(Data* ptr, int n){
if (!ptr) return FALSE;
for (int i = 0; i < n; i++)
printf("%c %-s\n", ptr[i].inputstring, ptr[i].encode);
return TRUE;
}
主函数以及输入辅助函数
//主函数
int main(){
int number = 0;
Data* ptr = NULL;
HuffmanTree HT;//初始化哈夫曼树
printf("请输入要编码的字符串:");
GetData(&ptr, &number);
printf("\n对应的编码表为:\n");
GetCode(&HT, &ptr, number);
PrintCode(ptr, number);
}
//辅助函数,获取字符
int GetData(Data** pptr, int* n){
int i = 0;//所测得的第i个字符类型
int m = 0;//现有的字符类型数
char c = 0;
Data* ptr = NULL;//初始化指针变量
//输入字符
L: while ((c = getchar()) != '\n'){
for (i = 0; i < m; i++){//将输入的字符与现有的字符进行比对
if (c == ptr[i].inputstring){//从0开始与现有的进行比对,如果比对成功
ptr[i].weight++;//权值加1
goto L;//跳出本次循环进行下一字符的输入判断
}
}
if (i == m){//i加至与m相同说明无此字符,需要新存进去
ptr = (Data*)realloc(ptr, (m + 1) * sizeof(Data));//重新调整之前调用所分配的pd所指向的内存块的大小
if (!ptr) //如果pd为空,返回false
return FALSE;
ptr[m].inputstring = c;//将c赋给pd[m].inputstring
ptr[m].weight = 1;//权值为1
ptr[m].encode = NULL;//编码为0
m++;//字符类型数加1
}
}
*n = m;//m为有几种字符
*pptr = ptr;//返回DATA结构体数组
return TRUE;
}