数据结构-树(Tree)

树(Tree)

一、树的基本概念

1、树的概念

一类重要的非线性数据结构,是以分支关系定义的层次结构

2、树的定义

由n(n>=0)个结点组成的有限集合T。当n=0时成为空树,非空树满足:
1)有一个称之为根(root)的结点.
2)除根以外的其余结点被分为m(0<=m

3、树的特点

除根结点外,每个结点仅有一个前驱(父)结点
树中的结点数等于所有结点的度数加1.数据结构-树(Tree)_第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、基本操作

数据结构-树(Tree)_第2张图片

二、树的存储结构

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)二叉树的五种基本形态

数据结构-树(Tree)_第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)数据结构-树(Tree)_第4张图片

(2)三元组

存储节点数据和左右孩子在向量中的序号
数据结构-树(Tree)_第5张图片
双亲:存储节点数据和其父结点的序号
数据结构-树(Tree)_第6张图片数据结构-树(Tree)_第7张图片

2、二叉链表

(1)图表说明

数据结构-树(Tree)_第8张图片

(2)类型定义
typedef struct btnode{
    btnode *lchild,rchild;
    ElemType data;
}BiTNode,*BiTree;//定义指针变量,用来存放根结点地址,通常用该指针表示一个二叉树

结论:若一个二叉树含有n个结点,则它的二叉链表中必含有2n个指针域,其中必含有n+1个空的链表

3、三叉链表/带双亲的二叉链表

(1)图表说明

(左孩子父母为正值,右孩子父母为负值)
数据结构-树(Tree)_第9张图片

(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)操作过程

数据结构-树(Tree)_第10张图片
要进行层次遍历,需要借助一个队列。先将二叉树根结点入队,然后出队,访问出队结点,若它有左子树,则将左子树根结点入队;若它有右子树,则将右子树根结点入队。然后出队,访问出队结…如此反复,直至队列为空。

(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°
数据结构-树(Tree)_第11张图片

2、森林转化为二叉树

画法

1)先将森林里的每一棵树转换成一棵二叉树;
2)从最后一棵树开始,把后一棵树的作为前一棵树的根的右子
数据结构-树(Tree)_第12张图片

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

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)树的带权路径长度

树中所有叶结点的带权路径长度之和称为,记为
数据结构-树(Tree)_第13张图片

(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;

代码实现时,我们用一个数组存储构建出来的哈夫曼树中各个结点的基本信息(权值、父结点、左孩子以及右孩子)。该数组的基本布局如下:
数据结构-树(Tree)_第14张图片
用数字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**,该字符指针数组的基本布局如下:
数据结构-树(Tree)_第15张图片
注意:这里为了与“构建哈夫曼树时所生成的数组”中的下标相对应,所以该字符指针数组中下标为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;
}

你可能感兴趣的:(数据结构,数据结构,算法)