第四十讲 树
之前我们一直在讨论的是一对一的线性结构,无论是线性表也好,栈和队列也罢,都是2P模式。
可现实生活中,3P、4P等现象比比皆是!
例如:
一个年轻的妈妈生了4个孩子,而每个孩子都不像他们的爸爸,那么这类情况我们用线性结构的形式就不足以描述了!
所以我们需要研究这种一对多的数据结构:
树
1.树的定义
树(Tree)是n(n>=0)个结点的有限集。
推荐阅读:程序员眼中的“树”
当n=0时成为空树,在任意一棵非空树中:
有且仅有一个特定的称为根(Root)的结点;
当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、...、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。
虽然从概念上很容易理解树,但是有两点还是需要大家注意下:
① n>0时,根结点是唯一的,坚决不可能存在多个根结点。
②m>0时,子树的个数是没有限制的,但它们互相是一定不会相交的。
2.结点分类
刚才所有图片中,每一个圈圈我们就称为树的一个结点。
结点拥有的子树数称为结点的度-(Degree),树的度取树内各结点的度的最大值。
度为0的结点称为叶结点(Leaf)或终端结点;
度不为0的结点称为分支结点或非终端结点,除根结点外,分支结点也称为内部结点。
3.结点间的关系
结点的子树的根称为结点的孩子(Child)。
相应的,该结点称为孩子的双亲(Parent),同一双亲的孩子之间互称为兄弟(Sibling)。
结点的祖先是从根到该结点所经分支上的所有结点。
4.结点的层次
结点的层次(Level)从根开始定起,根为第一层,根的孩子为第二层。
其双亲在同一层的结点互为堂兄弟。
树中结点的最大层次称为树的深度(Depth)或高度。
第四十一讲 树的存储结构 |【双亲表示法】
1.树的存储结构
本节课我们来关心一下:
如何在内存中安排树这种结构的存放。
说到存储结构,就会想到我们前面章节讲过的顺序存储和链式存储两种基本结构。
对于线性表来说,很直观就可以理解。
但对于树这种一对多的结构,我们应该怎么办呢?
要存储树,简单的顺序存储结构和链式存储结构是不能滴!
不过如果充分利用它们各自的特点,完全可以间接地来实现。
大家先思考下:
如果你是总工程师,让你来设计和规划,你有多少种方法可以实现对树结构的存放?
当然你还要考虑到:
双亲、孩子、兄弟之间的关系。
这里要介绍三种不同的表示法:
双亲表示法、孩子表示法、孩子兄弟表示法。
2.双亲表示法
双亲表示法,言外之意就是:
以双亲作为索引的关键词的一种存储方式。
我们假设以一组连续空间存储树的结点,同时在每个结点中,附设一个指示其双亲结点在数组中位置的元素。
也就是说:
每个结点除了知道自己是谁之外,还知道它的粑粑妈妈在哪里。
那么我们可以做如下定义:
// 树的双亲表示法结点结构定义
#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; // 根的位置
int n; // 结点数目
}PTree;
这样的存储结构,我们可以根据某结点的parent指针找到它的双亲结点。
所用的时间复杂度是O(1),索引到parent的值为-1时,表示找到了树结点的根。
可是,如果我们要知道某结点的孩子是什么?
那么不好意思:
请遍历整个树结构!
这真是麻烦,能不能改进一下呢?
鱼油们怎么看?
存储结构的设计是一个非常灵活的过程,只要你愿意,你可以设计出任何你想要的奇葩!
一个存储结构设计得是否合理,取决于:
基于该存储结构的运算是否适合、是否方便,时间复杂度好不好等等。
第四十二讲 树的存储结构 | 【孩子表示法】孩子表示法
我们这次换个角度来考虑:
由于树中每个结点可能有多棵子树,可以考虑用多重链表来实现。
就像我们虽然有计划生育,但我们还是无法确保每个家庭只养育一个孩子的冲动。
那么对于子树的不确定性也是如此。
下图中树的度(结点拥有的子树数)是多少呢?
答案是:
3
如果我们用“孩子表示法”,聪明的鱼油可以想出多少种可行方案?
这里我们不限制大家的答案,小甲鱼给出三个参考的方案。
先来看下方案一:
根据树的度,声明足够空间存放子树指针的结点。
缺点十分明显,就是造成了浪费!
针对方案一的缺点,我们有了方案二:
这样我们就克服了浪费这个概念,我们从此走上了节俭的社会主义道路!
但每个结点的度的值不同,初始化和维护起来难度巨大吧?
难倒没有更好的了?
请看下边架构:
那只找到孩子找不到双亲貌似还不够完善,那么我们合并上一讲的双亲孩子表示法:
说了这么多,我们一起来把代码落实起来吧!
代码:
#define MAX_TREE_SIZE 100
typedef char ElemType;
// 孩子结点
typedef struct CTNode
{
int child; // 孩子结点的下标
struct CTNode *next; // 指向下一个孩子结点的指针
} *ChildPtr;
// 表头结构
typedef struct
{
ElemType data; // 存放在树中的结点的数据
int parent; // 存放双亲的下标
ChildPtr firstchild; // 指向第一个孩子的指针
} CTBox;
// 树结构
typedef struct
{
CTBox nodes[MAX_TREE_SIZE]; // 结点数组
int r, n;
}
第四十三讲 二叉树 | 【数据世界的2X青年】
1.二叉树的定义
世上树有万千种,唯有二叉课上讲。
这里的“二叉”是二叉树,因为二叉树使用的范围最广,最具有代表意义,因此我们重点讨论二叉树。
定义:
二叉树(Binary Tree)是n(n>=0)个结点的有限集合,该集合或者为空集(空二叉树),或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
这个定义显然是递归形式的,所以咱看上去有点晕。
因为自古有云:
“神使用递归,人使用迭代!
2.二叉树的特点
每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。
注意:
不是都需要两棵子树,而是最多可以是两棵,没有子树或者有一棵子树也都是可以的。
左子树和右子树是有顺序的,次序不能颠倒。
即使树中某结点只有一棵子树,也要区分它是左子树还是右子树,下面是完全不同的二叉树:
3.五种基本形态
4.很二的二叉树
为什么这么说呢?
因为若只从形态上来考虑,拥有三个结点的普通树只有两种情况:
两层或者三层。
但对于很二的二叉树来说,由于要区分左右,所以就演变成五种形态:
5.特殊二叉树-斜树
因为他很二,所以他也很特殊。
接下来再介绍一下一些特殊的二叉树。
虽然暂时你可能不能理解它们的用处,但我们有必要先了解一下。
斜树:
斜树是一定要斜的,但斜也要斜得有范儿。
例如:
6.特殊二叉树-满二叉树
坡坡有云:
“人有悲欢离合,月有阴晴圆缺,此事古难全。但愿人长久,千里共长娟。”
意思就是说:
完美的那是理想,不完美的才是人生。
但是对于二叉树来说,是否存在完美呢?
有滴,那就是满二叉树啦。
7.满二叉树 VS 完全二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树。
并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
例如:
①满二叉树的特点有:
叶子只能出现在最下一层。
非叶子结点的度一定是2。
在同样深度的二叉树中,满二叉树的结点个数一定最多,同时叶子也是最多。
满二叉树和完全二叉树历年都是一个重大考点,因为考生很容易混淆两者。
②完全二叉树定义:
对一棵具有n个结点的二叉树按层序编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点位置完全相同
例如:
③完全二叉树的特点有:
叶子结点只能出现在最下两层。
最下层的叶子一定集中在左部连续位置。
倒数第二层,若有叶子结点,一定都在右部连续位置。
如果结点度为1,则该结点只有左孩子。
同样结点树的二叉树,完全二叉树的深度最小。
二者考试重点:
满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树。
以下这些都不是完全二叉树:
课后作业
二叉树性质:
1.在二叉树的第i层上最多有2^(i-1) 个节点 。(i>=1)
2.二叉树中如果深度为k,那么最多有(2^k)-1个节点。(k>=1)
3.n0=n2+1 n0表示度数为0的节点 n2表示度数为2的节点
4.在完全二叉树中,具有n个节点的完全二叉树的深度为[log2n]+1,其中[log2n]+1是向下取整。
.若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:
(1) 若 i=1,则该结点是二叉树的根,无双亲, 否则,编号为 [i/2] 的结点为其双亲结点;
(2) 若 2i>n,则该结点无左孩子, 否则,编号为 2i 的结点为其左孩子结点;
(3) 若 2i+1>n,则该结点无右孩子结点, 否则,编号为2i+1 的结点为其右孩子结点。
第四十四讲 二叉树性质 | 【枯燥矫情】
1.二叉树的性质
性质一:
在二叉树的第i层上至多有2^(i-1)个结点(i>=1)
这个性质其实很好记忆,考试的时候懂得画出二叉树的图便可以推出
性质二
深度为k的二叉树至多有(2^k)-1个结点(k>=1)
这里一定要看清楚哦,是2^k再-1,老方法理解:
性质三:
对任何一棵二叉树T,如果其终端结点数为n0,度为2的结点数为n2,则n0=n2+1
这个就比较困难了,需要推导获得:
首先我们再假设度为1的结点数为n1,则二叉树T的结点总数n=n0+n1+n2
其次我们发现连接数总是等于总结点数n-1,并且等于n1+2*n2
所以n-1=n1+2*n2
所以n0+n1+n2-1=n1+n2+n2
最后n0=n2+1
性质四:
由于完全二叉树前边我们已经提到,它的叶子结点只会出现在最下面的两层。
我们可以同样如下推导:
性质五:
文字证明难以描述,请看本讲官方视频。。。
第四十五讲 二叉树的存储结构
1.存储结构
树结构在计算机中的存储形式很多,可谓:
天马行空任你创造,只要能够按照要求完成任务即可。
在前边的演示中,我们发觉很难单单只用顺序存储结构或者链式存储结构来存放。
但是二叉树是一种特殊的树,由于它的特殊性,使得用顺序存储结构或链式存储结构都能够简单实现。
二叉树的顺序存储结构就是:
用一维数组存储二叉树中的各个结点,并且结点的存储位置能体现结点之间的逻辑关系。
2.顺序存储结构
这下看出完全二叉树的优越性来了吧?
由于他的严格定义,在数组直接能表现出逻辑结构。
当然对于一般的二叉树,尽管层序编号不能反映逻辑关系。
但是也可以按照完全二叉树编号方式修改一下,把不存在的结点用“^”代替即可。
但是考虑到一种极端的情况,回顾一下斜树。
如果是一个又斜树,那么会变成这样。。。。。。
3.二叉链表
既然顺序存储方式的适用性不强,那么我们就要考虑链式存储结构啦。
二叉树的存储按照国际惯例来说一般也是采用链式存储结构的。
二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法。
我们称这样的链表叫做二叉链表。
结构定义代码:
typedef struct BiTNode
{
ElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
第四十六讲 二叉树的遍历 | 【考点】
1.二叉树的遍历
二叉树的遍历(traversing binary tree)是指:
从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。
这里有两个关键词:
次序和访问
二叉树的遍历次序不同于线性结构,线性结构最多也就是分为顺序、循环、双向等简单的遍历方式。
树的结点之间不存在唯一的前驱和后继这样的关系,在访问一个结点后,下一个被访问的结点面临着不同的选择。
这就像我们的人生,漫漫长途上一步踏错,满盘皆输!
遍历方法
二叉树的遍历方式可以很多,如果我们限制了从左到右的习惯方式。
那么主要就分为一下四种:
前序遍历
中序遍历
后序遍历
层序遍历
①前序遍历
前序遍历:
若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。
遍历的顺序为:
A->B->D->H->I->E->J->C->F->K->G
②中序遍历
中序遍历:
若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。
遍历的顺序为:
H->D->I->B->E->J->A->F->K->C->G
③后序遍历
后序遍历:
若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根结点。
遍历的顺序为:
H->I->D->J->E->B->K->F->G->C->A
④层序遍历
层序遍历:
若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。
遍历的顺序为:
A->B->C->D->E->F->G->H->I->J->K
第四十七讲 二叉树的建立和遍历算法 |【代码实操】
1.二叉树的建立和遍历算法
有童鞋会说,我们上节课研究这么多遍历的方法干啥呢?
聪明的鱼油们怎么看?!
对于二叉树,思路方面我们已经谈得够多了,是时候由小甲鱼带大家来上机操作。
题目要求:
建议二叉树并输出每个字符所在的层数。如右图要求输出
A在第一层
B、C在第二层
D、E在第三层
代码实现:
#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
typedef struct BiTNode
{
char data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
// 创建一棵二叉树,约定用户遵照前序遍历的方式输入数据
CreateBiTree(BiTree *T)
{
char c;
scanf("%c", &c);
//空格代表没有
if( ' ' == c )
{
*T = NULL;
}
else
{
*T = (BiTNode *)malloc(sizeof(BiTNode));
(*T)->data = c;
CreateBiTree(&(*T)->lchild);
CreateBiTree(&(*T)->rchild);
}
}
// 访问二叉树结点的具体操作,你想干嘛?!
visit(char c, int level)
{
printf("%c 位于第 %d 层\n", c, level);
}
// 前序遍历二叉树
PreOrderTraverse(BiTree T, int level)
{
if( T )
{
visit(T->data, level);
PreOrderTraverse(T->lchild, level+1);
PreOrderTraverse(T->rchild, level+1);
}
}
int main()
{
int level = 1;
BiTree T = NULL;
CreateBiTree(&T);
PreOrderTraverse(T, level);
return 0;
}
最终结果:
高进阶(提示):
//前序遍历
void preorderTraverse(BiTree T) {
if(T==NULL)
return;
printf("%c",T->data);
preorderTraverse(T->lchild);
preorderTraverse(T->rchild);
}
//中序遍历
void InorderTraverse(BiTree T) {
if(T){
InorderTraverse(T->lchild);
printf("%c",T->data);
InorderTraverse(T->rchild);
}
}
//后序遍历
void PostorderTraverse(BiTree T) {
if(T) {
PostorderTraverse(T->lchild);
PostorderTraverse(T->rchild);
printf("%c",T->data);
}
}
第四十八讲 线索二叉树 |【强逻辑玩家必备】
1.线索二叉树
为什么需要线索二叉树呢?
正如程序猿发觉:
单链表并不总能满足他们设计的程序某些要求的时候,发明了双向链表来弥补一样。
线索二叉树也是在需求中被创造的!
那普通的二叉树到底有什么缺陷让我们发指呢?
三个要点:
一,浪费空间
二,浪费时间
三,浪费青春
来,我们具体分析下如何个浪费法,一道数序题:
请问以下有多少个“^”?总共浪费了多少字节的空间?(32bit的机器)
答案:
10*4 = 40字节。
40字节虽然没有多少,但是相对于整体来讲整整1/3都被浪费了,可见利用率之低。
脑筋急转弯:
我们知道通过对二叉树的约定遍历方式,可以得到一个固定的遍历顺序,那么请问哪种遍历方式可以节省“^”所浪费的空间?(利用“^”记录该结点的前驱后继)
前序遍历:
A->B->D->H->I->E->C->F->G
画圈点代表不能记录节点的前驱后继。
中序遍历:
H->D->I->B->E->A->F->C->G
没错,中序遍历可以拯救地球!
我们发现红色的结点都是刚才“^”造成浪费的结点。
利用中序遍历刚好它们均处于字符中间,可以很好地利用“^”来存放前驱和后继的指针。
不过好事总是多磨滴~
我们是有主观意识所以可以很容易出来哪些结点可以利用存放前驱后继。
有鱼油不同意了:
这所谓的主观意识还不简单,不就是从第一个开始每隔一个结点都可以?
上图经过中序遍历后结果是:
F->D->G->B->A->C->E
黄色说明只有一个空闲的指针位置。
如果是这样的话我们就面临一个问题:
机器怎么识别到底是存放指针还是线索?
没错,她需要一丁点儿提示,为此我们将已经定义好的结构进行“扩容”:
ltag为0时指向该结点的左孩子,为1时指向该结点的前驱。
rtag为0时指向该结点的右孩子,为1时指向该结点的后继。
第四十九讲 线索二叉树代码实现 | 【实操课】存储结构定义
1.二叉线索树存储结构定义如下:
/* 二叉树的二叉线索存储结构定义*/
typedef enum{Link, Thread}PointerTag; //Link = 0表示指向左右孩子指针;Thread = 1表示指向前驱或后继的线索
typedef struct BitNode
{
char data; //结点数据
struct BitNode *lchild, *rchild; //左右孩子指针
PointerTag Ltag; //左右标志
PointerTag rtal;
}BitNode, *BiTree;
线索化的实质就是将二叉链表中的空指针改为指向前驱或后继的线索。
由于前驱和后继信息只有在遍历该二叉树时才能得到。
所以,线索化的过程就是在遍历的过程中修改空指针的过程。
2.中序遍历线索化的递归函数
中序遍历线索化的递归函数代码如下:
BiTree pre; //全局变量,始终指向刚刚访问过的结点
//中序遍历进行中序线索化
void InThreading(BiTree p)
{
if(p)
{
InThreading(p->lchild); //递归左子树线索化
//===
if(!p->lchild) //没有左孩子
{
p->ltag = Thread; //前驱线索
p->lchild = pre; //左孩子指针指向前驱
}
if(!pre->rchild) //没有右孩子
{
pre->rtag = Thread; //后继线索
pre->rchild = p; //前驱右孩子指针指向后继(当前结点p)
}
pre = p;
//===
InThreading(p->rchild); //递归右子树线索化
}
}
上述代码除了//===之间的代码以外,和二叉树中序遍历的递归代码机会完全一样。
只不过将打印结点的功能改成了线索化的功能。
中间部分代码做了这样的事情:
因为此时p结点的后继还没有访问到,因此只能对它的前驱结点pre的右指针rchild做判断,if(!pre->rchild)表示如果为空,则p就是pre的后继,于是pre->rchild = p,并且设置pre->rtag = Thread,完成后继结点的线索化。
完成前驱和后继的判断后,不要忘记当前结点p赋值给pre,以便于下一次使用。
有了线索二叉树后,对它进行遍历时,其实就等于操作一个双向链表结构。
最终代码实现:
#include <stdio.h>
#include <stdlib.h>
typedef char ElemType;
// 线索存储标志位
// Link(0):表示指向左右孩子的指针
// Thread(1):表示指向前驱后继的线索
typedef enum {Link, Thread} PointerTag;
typedef struct BiThrNode
{
char data;
struct BiThrNode *lchild, *rchild;
PointerTag ltag;
PointerTag rtag;
} BiThrNode, *BiThrTree;
// 全局变量,始终指向刚刚访问过的结点
BiThrTree pre;
// 创建一棵二叉树,约定用户遵照前序遍历的方式输入数据
void CreateBiThrTree( BiThrTree *T )
{
char c;
scanf("%c", &c);
if( ' ' == c )
{
*T = NULL;
}
else
{
*T = (BiThrNode *)malloc(sizeof(BiThrNode));
(*T)->data = c;
(*T)->ltag = Link;
(*T)->rtag = Link;
CreateBiThrTree(&(*T)->lchild);
CreateBiThrTree(&(*T)->rchild);
}
}
// 中序遍历线索化
void InThreading(BiThrTree T)
{
if( T )
{
InThreading( T->lchild ); // 递归左孩子线索化
if( !T->lchild ) // 如果该结点没有左孩子,设置ltag为Thread,并把lchild指向刚刚访问的结点。
{
T->ltag = Thread;
T->lchild = pre;
}
if( !pre->rchild )
{
pre->rtag = Thread;
pre->rchild = T;
}
pre = T;
InThreading( T->rchild ); // 递归右孩子线索化
}
}
void InOrderThreading( BiThrTree *p, BiThrTree T )
{
*p = (BiThrTree)malloc(sizeof(BiThrNode));
(*p)->ltag = Link;
(*p)->rtag = Thread;
(*p)->rchild = *p;
if( !T )
{
(*p)->lchild = *p;
}
else
{
(*p)->lchild = T;
pre = *p;
InThreading(T);
pre->rchild = *p;
pre->rtag = Thread;
(*p)->rchild = pre;
}
}
void visit( char c )
{
printf("%c", c);
}
// 中序遍历二叉树,非递归
void InOrderTraverse( BiThrTree T )
{
BiThrTree p;
p = T->lchild;
while( p != T )
{
while( p->ltag == Link )
{
p = p->lchild;
}
visit(p->data);
while( p->rtag == Thread && p->rchild != T )
{
p = p->rchild;
visit(p->data);
}
p = p->rchild;
}
}
int main()
{
BiThrTree P, T = NULL;
CreateBiThrTree( &T );
InOrderThreading( &P, T );
printf("中序遍历输出结果为: ");
InOrderTraverse( P );
printf("\n");
return 0;
}
第五十讲 树、森林及二叉树的相互转换 |【三位一体】
1.树、森林及二叉树的相互转换
在这一章节开始的时候我们是从一棵普通的树开始介绍。
在满足树的条件下可以是任意形状,一个结点可以有任意多个孩子,这样对树的处理显然要复杂很多。
所以我们研究出了一些条条框框的限定,如:
二叉树,完全二叉树,满二叉树等。
那么这时候你就会想:
如果所有的树都像二叉树一样方便处理就好了。
祝贺你,想对了!
2.普通树转换为二叉树
步骤如下:
加线,在所有兄弟结点之间加一条连线。
去线,对树中每个结点,只保留它与第一孩子结点的连线,删除它与其他孩子结点之间的连线。
层次调整,以树的根结点为轴心,将整棵树顺时针旋转一定的角度,使之结构层次分明。
3.森林转换为二叉树
步骤如下:
把每棵树转换为二叉树。
第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子,用线连接起来。
4.二叉树转换为树、森林
二叉树转换为普通树是刚才的逆过程,步骤也就是反过来做而已。
判断一棵二叉树能够转换成一棵树还是森林。
标准很简单,那就是只要看这棵二叉树的根结点有没有右孩子,有的话就是森林,没有的话就是一棵树。
5.树与森林的遍历
树的遍历分为两种方式:
一种是先根遍历,另一种是后根遍历。
先根遍历:先访问树的根结点,然后再依次先根遍历根的每棵子树。
后根遍历:先依次遍历每棵子树,然后再访问根结点。
先根遍历结果:
A->B->E->F->C->G->D->H->I->J
后根遍历结果:
E->F->B->G->C->H->I->J->D->A
森林的遍历也分为前序遍历和后序遍历,其实就是:
按照树的先根遍历和后根遍历依次访问森林的每一棵树。
我们的惊人发现:
树、森林的前根(序)遍历和二叉树的前序遍历结果相同,树、森林的后根(序)遍历和二叉树的中序遍历结果相同!
这其实也就证实我们视频开头讲解的那个例子。
我们找到了对树和森林遍历这种复杂问题的简单解决方案!
第五十一讲 赫夫曼树 |【Huffman知名压缩算法】
1.赫夫曼树
在数据膨胀、信息爆炸的今天,数据压缩的意义不言而喻。
谈到数据压缩,就不能不提赫夫曼(Huffman)编码,赫夫曼编码是首个实用的压缩编码方案。
即使在今天的许多知名压缩算法里,依然可以见到赫夫曼编码的影子。
另外,在数据通信中,用二进制给每个字符进行编码时不得不面对的一个问题:
如何使电文总长最短且不产生二义性
根据字符出现频率,利用赫夫曼编码可以构造出一种不等长的二进制。
使编码后的电文长度最短,且保证不产生二义性。
介绍赫夫曼编码前,我们必须得介绍赫夫曼树。
什么叫做赫夫曼树呢?
我们先来看一个例子。
以下程序在效率上有什么问题呢?
if( a < 60 )
printf(“不及格”);
else if( a < 70 )
printf(“及格”);
else if( a < 90 )
printf(“良好”);
else
printf(“优秀”);
如果我们把判断流程改为以下,效果可能有明显的改善:
定义与原理
我们先把这两棵二叉树简化成叶子结点带权的二叉树
(注:树结点间的连线相关的数叫做权(Weight))。
结点的路径长度:
从根结点到该结点的路径上的连接数。
树的路径长度:
树中每个叶子结点的路径长度之和。
结点带权路径长度:
结点的路径长度与结点权值的乘积。
树的带权路径长度:
WPL(Weighted Path Length)是树中所有叶子结点的带权路径长度之和。
WPL的值越小,说明构造出来的二叉树性能越优。
那么现在的问题是:
如何构造出最优的赫夫曼树呢?
别急,赫夫曼大叔给了我们解决的方案。
步骤演示:
第五十二讲 赫夫曼编码 |【掌握原理-实操】
上一节课我们已经谈了赫夫曼树的基本原理和构造方式。
而赫夫曼编码可以很有效地压缩数据(通常可以节省20%~90%的空间,具体压缩率依赖于数据的特性)。
1.名词解释
定长编码:
像ASCII编码
变长编码:
单个编码的长度不一致,可以根据整体出现频率来调节
前缀码:
所谓的前缀码,就是没有任何码字是其他码字的前缀
赫夫曼思路
五个代码基础模块:
build a priority queue
build a huffmanTree
build a huffmanTable
encode
decode
2.赫夫曼编码步骤:
第五十三讲 赫夫曼编码C语言实现 |【读代码】
main文件
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include "huffman.h"
int main(void)
{
//根据字符串建立树
htTree *codeTree = buildTree("beep boop beer!");
//根据霍夫曼树建立表格
hlTable *codeTable = buildTable(codeTree);
//使用霍夫曼表进行编码
encode(codeTable,"beep boop beer!");
//使用霍夫曼树
//对初始字符串的符号进行解码
decode(codeTree,"0011111000111");
//输出 : 0011 1110 1011 0001 0010 1010 1100 1111 1000 1001
return 0;
}
huffman.h
代码实现:
#pragma once
#ifndef _HUFFMAN_H
#define _HUFFMAN_H
//霍夫曼树初始化定义
typedef struct _htNode {
char symbol;
struct _htNode *left, *right;
}htNode;
/*
我们将整个树“封装”在一个结构中,因为将来我们可能会添加像“大小”这样的属性,保证可拓展性。
这样我们不必修改整个源代码。
*/
typedef struct _htTree {
htNode *root;
}htTree;
//霍夫曼表节点(链表实现)
typedef struct _hlNode {
char symbol;
char *code;
struct _hlNode *next;
}hlNode;
//定义霍夫曼表
typedef struct _hlTable {
hlNode *first;
hlNode *last;
}hlTable;
htTree * buildTree(char *inputString);
hlTable * buildTable(htTree *huffmanTree);
void encode(hlTable *table, char *stringToEncode);
void decode(htTree *tree, char *stringToDecode);
#endif
huffman.cpp
代码实现:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "huffman.h"
#include "pQueue.h"
void traverseTree(htNode *treeNode, hlTable **table, int k, char code[256])
{
//遍历到树结尾,往表中添加数据
if(treeNode->left == NULL && treeNode->right == NULL)
{
code[k] = '\0';
hlNode *aux = (hlNode *)malloc(sizeof(hlNode));
aux->code = (char *)malloc(sizeof(char)*(strlen(code)+1));
strcpy(aux->code,code);
aux->symbol = treeNode->symbol;
aux->next = NULL;
if((*table)->first == NULL)
{
(*table)->first = aux;
(*table)->last = aux;
}
else
{
(*table)->last->next = aux;
(*table)->last = aux;
}
}
//左节点不为空,设置为0
if(treeNode->left!=NULL)
{
code[k]='0';
traverseTree(treeNode->left,table,k+1,code);
}
//右节点不为空,设置为1
if(treeNode->right!=NULL)
{
code[k]='1';
traverseTree(treeNode->right,table,k+1,code);
}
}
hlTable * buildTable(htTree * huffmanTree)
{
//初始化霍夫曼表
hlTable *table = (hlTable *)malloc(sizeof(hlTable));
table->first = NULL;
table->last = NULL;
//辅助变量
char code[256];
//k保存遍历的圈数
int k=0;
//遍历树计算节点
traverseTree(huffmanTree->root,&table,k,code);
return table;
}
htTree * buildTree(char *inputString)
{
//计算标志出现频率
//计算可能大小
//(256 ASCII characters)
int * probability = (int *)malloc(sizeof(int)*256);
//初始化数组
for(int i=0; i<256; i++)
probability[i]=0;
//将该标志视为数组索引,并计算每个标志出现的次数
for(int i=0; inputString[i]!='\0'; i++)
probability[(unsigned char) inputString[i]]++;
//保存树节点的队列
pQueue * huffmanQueue;
initPQueue(&huffmanQueue);
//为字符串中的每个标志创造节点
for(int i=0; i<256; i++)
if(probability[i]!=0)
{
htNode *aux = (htNode *)malloc(sizeof(htNode));
aux->left = NULL;
aux->right = NULL;
aux->symbol = (char) i;
addPQueue(&huffmanQueue,aux,probability[i]);
}
//释放不需要的数组
free(probability);
//按照指定步骤创建树
while(huffmanQueue->size!=1)
{
int priority = huffmanQueue->first->priority;
priority+=huffmanQueue->first->next->priority;
htNode *left = getPQueue(&huffmanQueue);
htNode *right = getPQueue(&huffmanQueue);
htNode *newNode = (htNode *)malloc(sizeof(htNode));
newNode->left = left;
newNode->right = right;
addPQueue(&huffmanQueue,newNode,priority);
}
//创建树
htTree *tree = (htTree *) malloc(sizeof(htTree));
tree->root = getPQueue(&huffmanQueue);
return tree;
}
void encode(hlTable *table, char *stringToEncode)
{
hlNode *traversal;
printf("\nEncoding\nInput string : %s\nEncoded string : \n",stringToEncode);
//遍历表中每个元素
//输出找到的指定标志
for(int i=0; stringToEncode[i]!='\0'; i++)
{
traversal = table->first;
while(traversal->symbol != stringToEncode[i])
traversal = traversal->next;
printf("%s",traversal->code);
}
printf("\n");
}
void decode(htTree *tree, char *stringToDecode)
{
htNode *traversal = tree->root;
printf("\nDecoding\nInput string : %s\nDecoded string : \n",stringToDecode);
//对每个字节进行解码
//左节点0,右节点1
for(int i=0; stringToDecode[i]!='\0'; i++)
{
if(traversal->left == NULL && traversal->right == NULL)
{
printf("%c",traversal->symbol);
traversal = tree->root;
}
if(stringToDecode[i] == '0')
traversal = traversal->left;
if(stringToDecode[i] == '1')
traversal = traversal->right;
if(stringToDecode[i]!='0'&&stringToDecode[i]!='1')
{
printf("The input string is not coded correctly!\n");
return;
}
}
if(traversal->left == NULL && traversal->right == NULL)
{
printf("%c",traversal->symbol);
traversal = tree->root;
}
printf("\n");
}
pQueue.h
代码实现:
#pragma once
#ifndef _PQUEUE_H
#define _PQUEUE_H
#include "huffman.h"
//修改数据类型来保存指向霍夫曼树节点的指针
#define TYPE htNode *
#define MAX_SZ 256
typedef struct _pQueueNode {
TYPE val;
unsigned int priority;
struct _pQueueNode *next;
}pQueueNode;
typedef struct _pQueue {
unsigned int size;
pQueueNode *first;
}pQueue;
void initPQueue(pQueue **queue);
void addPQueue(pQueue **queue, TYPE val, unsigned int priority);
TYPE getPQueue(pQueue **queue);
#endif
pQueue.cpp
代码实现:
#include "pQueue.h"
#include <stdlib.h>
#include <stdio.h>
void initPQueue(pQueue **queue)
{
//为优先级队列类型分配内存
//初始化这些字段的值
(*queue) = (pQueue *) malloc(sizeof(pQueue));
(*queue)->first = NULL;
(*queue)->size = 0;
return;
}
void addPQueue(pQueue **queue, TYPE val, unsigned int priority)
{
//如果队列已满,我们不必添加指定的值。
//返回一个错误信息到控制台
if((*queue)->size == MAX_SZ)
{
printf("\nQueue is full.\n");
return;
}
pQueueNode *aux = (pQueueNode *)malloc(sizeof(pQueueNode));
aux->priority = priority;
aux->val = val;
// 如果队列为空,增加初始值
//如果结构在运行时发生异常修改(很少发生),会强制验证两次。
if((*queue)->size == 0 || (*queue)->first == NULL)
{
aux->next = NULL;
(*queue)->first = aux;
(*queue)->size = 1;
return;
}
else
{
//如果已经有队列中的元素和元素的优先级,我们要添加大于第一个元素的优先级,将它添加到第一个元素的前面。
//注意,需要优先级从高到低
if(priority<=(*queue)->first->priority)
{
aux->next = (*queue)->first;
(*queue)->first = aux;
(*queue)->size++;
return;
}
else
{
//根据优先级寻找节点的位置
pQueueNode * iterator = (*queue)->first;
while(iterator->next!=NULL)
{
//降序,将元素放在头部
if(priority<=iterator->next->priority)
{
aux->next = iterator->next;
iterator->next = aux;
(*queue)->size++;
return;
}
iterator = iterator->next;
}
//如果我们到了最后,我们还没有添加元素,将它放到末尾
if(iterator->next == NULL)
{
aux->next = NULL;
iterator->next = aux;
(*queue)->size++;
return;
}
}
}
}
TYPE getPQueue(pQueue **queue)
{
TYPE returnValue;
//只要不为空,从队列中获取元素
if((*queue)->size>0)
{
returnValue = (*queue)->first->val;
(*queue)->first = (*queue)->first->next;
(*queue)->size--;
}
else
{
//如果队列为空,打印“队列为空”
//返回当前内存中的值到returnValue
printf("\nQueue is empty.\n");
}
return returnValue;
}