今天我们继续来整理树的知识点,这一部分主要整理树的几种表示方法以及哈夫曼树的知识点,在看这篇文章之前请先看一下紫薇星上的数据结构(7)哦~
8.1树的表示法
在这一篇文章中我们接着昨天树的知识点继续整理,树有好几种表示形式,我们列举一下:
- 图像表示:
双亲表示法:【数据】【双亲】;
孩子表示法:【数据】【孩子】;
孩子兄弟表示法:【长子】【数据】【兄弟】;
- 括号表示:
图中这棵树可以表示为:(A,(B,(D,E),C));
- 遍历表示:
遍历表示上一篇文章已经讲了四种遍历方法了,这里就不再赘述。
看完上面的表示方法我们来一一说明一下:
双亲表示法
用一组连续的存储单元存储树的结点,每个结点除了数据域data之外,还设有一个parent域用以指示其双亲结点的位置,方式如下:【data】【parent】;我们用一个例子来说明一下:
简单说就是存储数据时在数据域后加一个指针指向双亲,这样就能够直接找到某个结点的双亲结点,方便查找。
孩子表示法
把每个结点的孩子结点排列起来,看成一个线性表,且以单链表做存储结构,则 n 个结点有 n 个孩子链表(叶子结点的好质量表为空表),而 n 个头指针又组成一个线性表,我们仍然看一下这个例子:
使用单链表存储一个结点和他的孩子结点,同时每个结点都使用链表来存储他们的孩子结点,这样就形成了多重链表结构。
孩子兄弟表示法
因为树在表示的时候要使用双亲表示法或者孩子表示法,但是这两种方法都很难实现,大家如果需要代码可以私信我。回归正题,在这样一棵树中使用前两种表示法显然实现是很困难的,但我们可以将其转换为二叉树,使用孩子兄弟表示法就可以将树转换为二叉树。
我们刚才说过孩子兄弟表示法的形式为:【长子】【数据】【兄弟】;这里把几个概念解释一下:
长子:父结点下的第一个子结点就是长子结点,对A来说B就是它的长子结点,对B来说E就是它的长子结点;
兄弟:父结点下的与长子结点同一层的其余结点都为兄弟结点,C和D就是A的兄弟结点,F是E的兄弟结点,但H不是E的兄弟结点,H是G的兄弟结点;
刚才说孩子兄弟表示法可以将树转化为二叉树,怎么转换呢?因为孩子兄弟表示法的形式为:【长子】【数据】【兄弟】;所以在一个结点有一个数据域和两个指针域,一个用来存放长子结点,另一个用来存放兄弟结点,就像我们的二叉树的链式存储结构一样,这里左子树对应的是长子结点,右子树对应的是兄弟结点,我们举一个例子来说明:
首先我们看一下左边的这个树,明显的不能按照二叉树的行为来进行处理,我们将所有的兄弟结点与父结点的连接全部删除,然后将所有的兄弟结点连接在长子结点处,这样就可以将其转化为二叉树了,具体操作是:从根结点开始,每个结点都向下一层,长子结点存放在左侧,兄弟节点存放在右侧,以此类推,这样就得到了右图中的二叉树。
这里我们使用代码来实现一下孩子兄弟表示法,首先建立Brother.h和Brother.c文件,在Brother.h中添加操作:
#ifndef BROTHER_H_INCLUDED
#define BROTHER_H_INCLUDED
#include "ElementType.h"
#include <string.h>
typedef struct cbNode{
ElementType data;
struct cbNode *firstChild; //长子结点
struct cbNode *nextSibling; //兄弟结点
}CBNode, *CBTree;
//初始化
void InitCBTree(CBTree *tree);
//构造树
void CreateCBTree(CBNode **node);
//前序遍历
void PreOrderCBTree(CBNode *node);
//测试函数
void TestCBTree();
#endif // BROTHER_H_INCLUDED
然后在Brother.c中添加操作:
#include "Brother.h"
static int id = 0;
//初始化
void InitCBTree(CBTree *tree){
*tree = (CBTree*)malloc(sizeof(CBNode));
(*tree)->firstChild = NULL;
(*tree)->nextSibling = NULL;
}
//构造树
void CreateCBTree(CBNode **node){
char inputName[NAME_SIZE];
gets(inputName);
//判断是否输入回车
if(strcmp(inputName, "\0") == 0){
return;
}
if(*node == NULL){
*node = (CBNode*)malloc(sizeof(CBNode));
(*node)->firstChild = NULL;
(*node)->nextSibling = NULL;
}
//为结点赋值
(*node)->data.id = ++id;
strcpy((*node)->data.name, inputName);
//分别便利输入长子结点和兄弟结点
printf("请输入长子结点:");
CreateCBTree(&(*node)->firstChild);
printf("请输入兄弟结点:");
CreateCBTree(&(*node)->nextSibling);
}
//前序遍历
void PreOrderCBTree(CBNode *node){
//先序遍历在遍历时先遍历长子结点,然后遍历兄弟结点
if(node != NULL){
printf("[%d, %s]-", node->data.id, node->data.name);
CBNode *p = node->firstChild;
PreOrderCBTree(p);//递归遍历长子
//然后遍历兄弟
while(p){
p = p->nextSibling;
PreOrderCBTree(p);
}
}
}
//测试函数
void TestCBTree(){
CBTree tree;
InitCBTree(&tree);
printf("根结点:");
CreateCBTree(&tree);
printf("先序遍历:\n");
PreOrderCBTree(tree);
}
我们直接在main.c中测试一下看能不能出来:
#include <stdio.h>
#include <stdlib.h>
#include "Brother.h"
int main(){
TestCBTree();
return 0;
}
在这里我们就直接将上面的满二叉树例子输进去,编译通过,运行结果如下:
根结点:A
请输入长子结点:B
请输入长子结点:E
请输入长子结点:
请输入兄弟结点:F
请输入长子结点:
请输入兄弟结点:
请输入兄弟结点:C
请输入长子结点:G
请输入长子结点:
请输入兄弟结点:H
请输入长子结点:
请输入兄弟结点:I
请输入长子结点:
请输入兄弟结点:
请输入兄弟结点:D
请输入长子结点:J
请输入长子结点:
请输入兄弟结点:K
请输入长子结点:
请输入兄弟结点:
请输入兄弟结点:
请输入兄弟结点:
先序遍历:
[1, A]-[2, B]-[3, E]-[4, F]-[5, C]-[6, G]-[7, H]-[8, I]-[9, D]-[10, J]-[11, K]-
Process returned 0 (0x0) execution time : 38.072 s
Press any key to continue.
我们还要知道转换过来的树的深度,以便进行层序遍历和其它操作,在昨天的文章中我们已经编写了LinkedQueue的代码,我们可以直接拿过来用,不过有一点需要注意,昨天的是针对于二叉树使用队列进行层序遍历的,所以代码中的所有LinkedNode全部要换为我们今天的新的类型CBNode。为了看得清楚,我重新编写一次,大家要是不想再敲一次的话就直接将昨天的LinkedQueue.h和LinkedQueue.c全部复制一遍改下文件名字就好了,不要忘记改变其中的变量类型。
我们新建一个CBQueue.h和CBQueue.c,在CBQueue.h中添加操作:
#ifndef CBQUEUE_H_INCLUDED
#define CBQUEUE_H_INCLUDED
#include <stdio.h>
#include <stdlib.h>
#include "ElementType.h"
//这段代码从Brother.h中剪切出来的,Brother.h中要加上#inlude "CBQueue.h"
typedef struct cbNode{
ElementType data;
struct cbNode *firstChild; //长子结点
struct cbNode *nextSibling; //兄弟结点
}CBNode, *CBTree;
//链队结点
typedef struct qNode{
CBNode *data;
struct qNode *next;
}QueueNode;
//链队列
typedef struct{
QueueNode *qFront;
QueueNode *qRear;
}LinkedQueue;
void InitCBQueue(LinkedQueue *linkQueue);
void enCBQueue(LinkedQueue *linkedQueue, CBNode *data);
CBNode *deCBQueue(LinkedQueue *linkedQueue);
int IsCBQueueEmpty(LinkedQueue *linkedQqueue);
#endif // CBQUEUE_H_INCLUDED
这里要注意,我们将CBNode的代码从Brother.h中剪切出来了,Brother.h中没有这段代码了,我们要在Brother.h中要加上#inlude "CBQueue.h",在Brother.h中添加操作:
#ifndef BROTHER_H_INCLUDED
#define BROTHER_H_INCLUDED
#include "ElementType.h"
#include <string.h>
#include "CBQueue.h"
//初始化
void InitCBTree(CBTree *tree);
//构造树
void CreateCBTree(CBNode **node);
//前序遍历
void PreOrderCBTree(CBNode *node);
//测试函数
void TestCBTree();
//获得树的深度-利用了层序遍历的代码
int GetCBTreeDepth(CBTree tree);
#endif // BROTHER_H_INCLUDED
然后在CBQueue.c中编写操作:
#include "CBQueue.h"
void InitCBQueue(LinkedQueue *linkQueue){
linkQueue->qFront = (QueueNode*)malloc(sizeof(QueueNode));
linkQueue->qFront->next = NULL;
linkQueue->qRear = linkQueue->qFront;
}
void enCBQueue(LinkedQueue *linkedQueue, CBNode *data){
QueueNode *node = (QueueNode*)malloc(sizeof(QueueNode));
node->data = data;
node->next = NULL;
linkedQueue->qRear->next = node;
linkedQueue->qRear = node;
}
CBNode *deCBQueue(LinkedQueue *linkedQueue){
CBNode *data = NULL;
if(linkedQueue->qFront == linkedQueue->qRear){
return data;
}
QueueNode *node = linkedQueue->qFront->next;
data = node->data;
linkedQueue->qFront->next = node->next;
if(linkedQueue->qRear == node){
linkedQueue->qRear = linkedQueue->qFront;
}
free(node);
return data;
}
int IsCBQueueEmpty(LinkedQueue *linkedQqueue){
if(linkedQqueue->qFront == linkedQqueue->qRear){
return 1;
}
return 0;
}
然后我们在Brother.c中添加操作:
//测试函数
void TestCBTree(){
CBTree tree;
InitCBTree(&tree);
printf("根结点:");
CreateCBTree(&tree);
printf("先序遍历:\n");
PreOrderCBTree(tree);
printf("\n层序遍历:\n");
int depth = GetCBTreeDepth(tree);
printf("\n树的深度为:%d\n", depth);
}
//获得树的深度-利用了层序遍历的代码
int GetCBTreeDepth(CBTree tree){
//使用层序遍历计算树的深度
LinkedQueue queue; //用来记录结点的队列
InitCBQueue(&queue);
enCBQueue(&queue, tree); //根结点入队
int zOrder = 1; //层号
printf("\n第%d层:", zOrder); //每层打印一次
CBNode *last = tree; //每轮遍历的最后一个结点
CBNode *lastChild = tree; //每次遍历的最后一个子结点
while(!IsCBQueueEmpty(&queue)){
CBNode *node = deCBQueue(&queue); //出队
printf("[%d, %s] ", node->data.id, node->data.name);
//将当前出队的所有子结点入队,等待出队打印
CBNode *tempNode = node->firstChild; //拿到长子结点
while(tempNode){
enCBQueue(&queue, tempNode);
lastChild = tempNode;
tempNode = tempNode->nextSibling;
}
//所有孩子结点都出队,但下一层还有孩子结点,进入下一层
if(last == node && !IsCBQueueEmpty(&queue)){
zOrder ++;
printf("\n第%d层:", zOrder);
last = lastChild;
}
}
return zOrder;
}
在main.c中实现一下,编译通过,运行结果如下:
根结点:A
请输入长子结点:B
请输入长子结点:E
请输入长子结点:
请输入兄弟结点:F
请输入长子结点:
请输入兄弟结点:
请输入兄弟结点:C
请输入长子结点:G
请输入长子结点:
请输入兄弟结点:H
请输入长子结点:
请输入兄弟结点:I
请输入长子结点:
请输入兄弟结点:
请输入兄弟结点:D
请输入长子结点:J
请输入长子结点:
请输入兄弟结点:K
请输入长子结点:
请输入兄弟结点:
请输入兄弟结点:
请输入兄弟结点:
先序遍历:
[1, A]-[2, B]-[3, E]-[4, F]-[5, C]-[6, G]-[7, H]-[8, I]-[9, D]-[10, J]-[11, K]-
层序遍历:
第1层:[1, A]
第2层:[2, B] [5, C] [9, D]
第3层:[3, E] [4, F] [6, G] [7, H] [8, I] [10, J] [11, K]
树的深度为:3
Process returned 0 (0x0) execution time : 16.950 s
Press any key to continue.
8.2打印文件目录树
现在有需求:
- 读取新建项目的目录文件树;
- 遍历目录树访问文件目录;
- 记录访问的目录名或者文件名,并按照tree命令的打印格式在控制台打印结果。
是不是一头雾水?先告诉大家应该打印什么,我们在电脑中打开这个项目的所在文件夹:
可以看到有很多文件和子文件夹哈,我们点击左上角的“文件”,然后再点击下图中箭头所在的地方:
这就是电脑中的CMD了:
然后我们简单输入“tree”,就会出现目录树下的所有文件夹,或者输入“tree /f”,就会出现目录下的所有文件:
这就是我电脑中的项目目录了,我们要做的就是使用数据结构层次遍历将他打印出来。
首先我们建立FileTest.h和FileTest.c这两个文件,然后在FileTest.h中编写操作:
#ifndef FILETEST_H_INCLUDED
#define FILETEST_H_INCLUDED
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h> //文件的内容
#include <sys/stat.h> //文件的状态
typedef enum fileType{
TYPE_FILE, //文件类型
TYPE_FOLDER //文件夹类型
}FileType;
//文件结点
typedef struct fileNode{
char fileName[1024]; //文件名/目录名,含后缀
struct fileNode *firstChild;
struct fileNode *nextSibling;
struct fileNode *parent;
}FileNode, FileTree;
//打印给定的目录结构dir,目录深度depth
void PrintDir(char *dir, int depth);
#endif // FILETEST_H_INCLUDED
在FileTest.c中编写操作:
#include "FileTest.h"
//打印给定的目录结构dir,目录深度depth
void PrintDir(char *dir, int depth){
DIR *dirPtr; //目录指针
if((dirPtr = opendir(dir)) == NULL){
sprintf(stderr, "无法打开目录:<%s>\n", dir);
}
struct dirent *dirContent; //目录内容
while((dirContent = readdir(dirPtr)) != NULL){
printf("当前文件名:%s\n", dirContent->d_name);
}
}
其实编写到这一步的时候我们已经能够打印出目录了,我们在main.c中试一下:
#include <stdio.h>
#include <stdlib.h>
#include "FileTest.h"
int main(){
PrintDir("D:\\CDproject\\Tree", 0);
return 0;
}
编译通过,运行结果如下:
当前文件名:.
当前文件名:..
当前文件名:bin
当前文件名:BinaryTree.c
当前文件名:BinaryTree.h
当前文件名:Brother.c
当前文件名:Brother.h
当前文件名:CBQueue.c
当前文件名:CBQueue.h
当前文件名:ElementType.h
当前文件名:FileTest.c
当前文件名:FileTest.h
当前文件名:LinkedQueue.c
当前文件名:LinkedQueue.h
当前文件名:main.c
当前文件名:obj
当前文件名:SeqTree.c
当前文件名:SeqTree.h
当前文件名:Tree.cbp
当前文件名:Tree.depend
当前文件名:Tree.layout
当前文件名:TreeNode.h
Process returned 0 (0x0) execution time : 0.374 s
Press any key to continue.
这样就打印出了目录下所有文件和文件夹,但也只是在这一层中的文件以及文件夹名称,并不能进入到文件夹内部去,并且可以看到文件是不完整的,还有“.”和“..”的存在,我们继续在FileTest.c中编写操作:
#include "FileTest.h"
//打印给定的目录结构dir,目录深度depth
void PrintDir(char *dir, int depth){
DIR *dirPtr; //目录指针
if((dirPtr = opendir(dir)) == NULL){
sprintf(stderr, "无法打开目录:<%s>\n", dir);
}
struct dirent *dirContent; //目录内容
while((dirContent = readdir(dirPtr)) != NULL){
//跳过文件名为 . 和 .. 的情况
if(strcmp(dirContent->d_name, ".") == 0 || strcmp(dirContent->d_name, "..") == 0){
continue;
}
char fullName[1024];
sprintf(fullName, "%s\\%s", dir, dirContent->d_name);
printf("当前文件名:%s\n", fullName);
//如果是文件夹,需要读取目录(递归)
struct stat statInfo; //当前文件或目录的信息
if((stat(fullName, &statInfo)) == -1){
sprintf(stderr, "无法获取文件<%s>的详细信息!\n", fullName);
}
//固定用法
if((statInfo.st_mode & S_IFMT) == S_IFDIR){
printf("\t是目录!\n");
//是目录就递归调用
PrintDir(fullName, ++depth);
}else if((statInfo.st_mode & S_IFMT) == S_IFREG){
printf("\t是一般文件,所占空间:%ldK\n", statInfo.st_size / 1024);
}
printf("\n");
}
}
编译通过,运行结果如下:
当前文件名:D:\CDproject\Tree\bin
是目录!
当前文件名:D:\CDproject\Tree\bin\Debug
是目录!
当前文件名:D:\CDproject\Tree\bin\Debug\Tree.exe
是一般文件,所占空间:94K
当前文件名:D:\CDproject\Tree\BinaryTree.c
是一般文件,所占空间:3K
当前文件名:D:\CDproject\Tree\BinaryTree.h
是一般文件,所占空间:0K
当前文件名:D:\CDproject\Tree\Brother.c
是一般文件,所占空间:2K
当前文件名:D:\CDproject\Tree\Brother.h
是一般文件,所占空间:0K
当前文件名:D:\CDproject\Tree\CBQueue.c
是一般文件,所占空间:0K
当前文件名:D:\CDproject\Tree\CBQueue.h
是一般文件,所占空间:0K
当前文件名:D:\CDproject\Tree\ElementType.h
是一般文件,所占空间:0K
当前文件名:D:\CDproject\Tree\FileTest.c
是一般文件,所占空间:1K
当前文件名:D:\CDproject\Tree\FileTest.h
是一般文件,所占空间:0K
当前文件名:D:\CDproject\Tree\LinkedQueue.c
是一般文件,所占空间:1K
当前文件名:D:\CDproject\Tree\LinkedQueue.h
是一般文件,所占空间:0K
当前文件名:D:\CDproject\Tree\main.c
是一般文件,所占空间:1K
当前文件名:D:\CDproject\Tree\obj
是目录!
当前文件名:D:\CDproject\Tree\obj\Debug
是目录!
当前文件名:D:\CDproject\Tree\obj\Debug\BinaryTree.o
是一般文件,所占空间:6K
当前文件名:D:\CDproject\Tree\obj\Debug\Brother.o
是一般文件,所占空间:4K
当前文件名:D:\CDproject\Tree\obj\Debug\CBQueue.o
是一般文件,所占空间:3K
当前文件名:D:\CDproject\Tree\obj\Debug\FileTest.o
是一般文件,所占空间:4K
当前文件名:D:\CDproject\Tree\obj\Debug\LinkedQueue.o
是一般文件,所占空间:3K
当前文件名:D:\CDproject\Tree\obj\Debug\main.o
是一般文件,所占空间:4K
当前文件名:D:\CDproject\Tree\obj\Debug\SeqTree.o
是一般文件,所占空间:3K
当前文件名:D:\CDproject\Tree\SeqTree.c
是一般文件,所占空间:1K
当前文件名:D:\CDproject\Tree\SeqTree.h
是一般文件,所占空间:0K
当前文件名:D:\CDproject\Tree\Tree.cbp
是一般文件,所占空间:1K
当前文件名:D:\CDproject\Tree\Tree.depend
是一般文件,所占空间:1K
当前文件名:D:\CDproject\Tree\Tree.layout
是一般文件,所占空间:0K
当前文件名:D:\CDproject\Tree\TreeNode.h
是一般文件,所占空间:0K
Process returned 0 (0x0) execution time : 0.146 s
Press any key to continue.
可以看到就打印出来了,至于能不能达到我们电脑中tree的效果,大家自己研究一下加一点细节就可以了。
8.3哈夫曼树及应用
大家在日常使用电脑时应该都压缩过文件吧,压缩文件时使用哈夫曼编码可以对文件进行无损压缩。这里我们先引入一些概念:
- 路径:从树中一个结点到另一个结点之间的分支构成两个结点之间的路径;
- 路径长度:路径上的分支数量称为路径长度;
- 树的路径长度就是从树根到每一个结点的路径长度之和。
举个例子说明一下:
在这棵树中,根结点到A长度为3,到B长度为3,到C长度为2,到D长度为2,到E长度为2,所以这棵树的路径长度为12。
然后我们引入一个概念:权值。在计算机数据结构领域中,权值是树或者图中两个结点路径上的值,这个值表明一种代价,如从一个结点到达另外一个结点的路径的长度、所花费的时间、付出的费用等。
一般来说在一棵树中,结点可以随意安排,权值也会随着结点的位置不同而不同,路径也是,所以同样的数据元素在不同的结点位置所构成的树,路径长度不同,权值也不同,那么带权路径长度也不同,树的带权路径长度也称为树的代价。
- 结点的权:在一些应用中,赋予树中结点的一个有某种意义的实数;
- 结点的带权路径长度:结点到树根之间的路径长度与该结点上权的乘积;
- 树的带权路径长度(Weighted Path Length of Tree):定义为树中所有叶结点的带权路径长度之和。
哈夫曼树/最优二叉树的定义
那么什么是哈夫曼树呢?
- 假设有n个权值{W1, W2, W3, ... , Wn},构造一个有n个结点的二叉树;
- 每个叶节点带权Wk,每个也值得路径长度为 Ik;
- 我们通常记作:带权路径长度WPL最小的二叉树称为哈夫曼树。
在刚才的二叉树中,如果每个结点的权值为都0.1,那么这棵树的WPL就等于3*0.1+3*0.1+2*0.1+2*0.1+2*0.1 = 1.2。
构造哈夫曼树
如何构造哈夫曼树呢?
- 将有权值的叶结点按从大到小的顺序排列成一个有序序列;
- 取权值最小的两个结点作为一个新结点的两个子结点,相对较小的为左孩子;
- 然后将这个新结点代替构成他的两个结点与其他结点排序;
- 重复上述过程,直到所有结点都称为哈夫曼树的结点。
我们来举一个例子,现在有五个元素:A, B, C, D,E;他们的权值为:0.11,0.15,0.31,0.34,0.09;现在我们来构造一个哈夫曼树:
- 首先进行排序:D0.34,C0.31,B0.15,A0.11,E0.09;
- 这时取A和E作为两个最小结点组成新结点N1,N1的权值为0.2;
- 然后N1代替A和E,重新排序:D0.34,C0.31,N1 0.2,B0.15;
- 这时取N1和B作为两个最小结点组成新结点N2,N2的权值为0.35;
- 然后N2代替N1和B,重新排序:N2 0.35,D0.34,C0.31;
- 这时取C和D作为两个最小结点组成新结点N3,N3的权值为0.65;
- 然后N3代替C和D,重新排序:N3 0.65,N2 0.35;
- 这时取N2和N3作为两个最小结点组成新结点T,T的权值为1;
这里要注意:T为根结点;N2在N3的左侧,因为N2比N3小。
这就是一个完整的哈夫曼树的构成过程,这颗哈夫曼树的WPL是2.16。
哈夫曼树的应用
在电报的发送与接收中我们知道是由“·”和“—”构成的,其原理就是将26个字母在句子中使用中的频率按从大到小,频率高的就用短的电脉冲信号代替,频率低的就用长的电脉冲信号代替,这样的信号组合发送过去之后,接收方就可以编译出来。
所以在远程通讯中,要将待传字符转换为二进制的字符串,怎样编码才能使他们组成的报文在网络中传的最快?以26个字母为例,我们只需要知道在发送的报文中字母出现的频率,然后就像上面构造哈夫曼树一样构造一个字母哈夫曼树,不同的是,我们在构造完后,将每个左路径的值变为0,右路径的值变为1,然后我们要找哪个字母,只需要将从根结点到这个字母的路径上的个值组合就可以了。
我们举个例子:假如现在有一篇报文ABDCADEEFD,字母表前六个字母ABCDEF的出现频率分别为27,8,15,17,28,5;现在已知根据二进制编码后编码表如下,产生的编码为:000001011010000011100100101011,现在我们使用哈夫曼树来构造哈夫曼编码,编码表如下,这样产生的编码是:0111010011101001010110000,明显比上面的编码少了很多,这样子处理后在大篇文章的发送中产生的消耗就会大幅减少。
因为在设计编码时,不能将一个编码设计为另一个编码的前缀,比如一个编码为00,另一个编码为001,那么我们在看到001001这条编码的时候就不能区分00和001的位置了,所以设计编码的关键是长度要不等,不能因为前缀相同而不能区分。
8.4构造哈夫曼树
哈夫曼树构造算法:
- 由给定的n个权值构造n棵只有一个叶结点的二叉树,从而得到一个二叉树的集合F = {T1,T2, … ,TN};
- 在F中选取根结点的权值最小和次小的两棵二叉树作为左,右子树构造一棵新的二叉树,这棵二叉树根结点的权值为其左右子树权值之和;
- 在集合F中删除作为左右子树的两棵二叉树,并将建立的二叉树加入到集合F 中;
- 重复2、3直到F中只剩下最后一棵所需的二叉树,就是哈夫曼树;
代码就不在这里实现了,大家要了解哈夫曼树的思想,如果需要代码可以私信我。
今天整理了树的表示方法、如何将一个树转换为二叉树、遍历文件目录以及哈夫曼树的思想,下次我们将整理图的知识点,等图整理完后,这个系列也快结束了,我们下次见👋