紫薇星上的数据结构(8)

今天我们继续来整理树的知识点,这一部分主要整理树的几种表示方法以及哈夫曼树的知识点,在看这篇文章之前请先看一下紫薇星上的数据结构(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构造哈夫曼树

哈夫曼树构造算法:

  1. 由给定的n个权值构造n棵只有一个叶结点的二叉树,从而得到一个二叉树的集合F = {T1,T2, … ,TN};
  2. 在F中选取根结点的权值最小和次小的两棵二叉树作为左,右子树构造一棵新的二叉树,这棵二叉树根结点的权值为其左右子树权值之和;
  3. 在集合F中删除作为左右子树的两棵二叉树,并将建立的二叉树加入到集合F 中;
  4. 重复2、3直到F中只剩下最后一棵所需的二叉树,就是哈夫曼树;

代码就不在这里实现了,大家要了解哈夫曼树的思想,如果需要代码可以私信我。


今天整理了树的表示方法、如何将一个树转换为二叉树、遍历文件目录以及哈夫曼树的思想,下次我们将整理图的知识点,等图整理完后,这个系列也快结束了,我们下次见👋

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值