关于哈夫曼算法

预备知识

树形结构:是一种非线性结构,用于表示具有层次结构的数据。应用有操作系统的目录结构,编译系统源程序的语法结构,数据存储压缩等。

     树形结构应具体讨论树,二叉树,堆,哈夫曼树这几种有鲜明特征的树形结构

树:实质上是n个节点的有限集合。

1 空树:表示构成树的集合为空集

2 根节点:在非空树中,至少有一个根节点,其余节点都具有唯一前驱

3 子树:在非空树中,根节点将树分为多个子集,这些也具有层次结构的子集就称为非空树

4 递归:由根与子树的关系可以看出,树是一种具有递归特性的数据结构

5 森林:多棵树可以构成森林

6 了解以下概念——节点,路径,双亲,孩子,兄弟,后裔,祖先,节点的度,树的度,叶子节点和分支节点,高度

7 有序树和无序树,二叉树就是有序树

二叉树:二叉树是特殊的树,由根节点和与其同为二叉树的左右子树构成。二叉树有五种基本形态,二叉树的性质无非就是关于高度,节点数和度的简单计算。

1 满二叉树:满二叉树的每行节点都达到了该行最大节点数,即2^(i-1)个

2 完全二叉树:对于一棵满二叉树,按从上至下,从左至右的层次规律进行编号。完全二叉树就是对它同样进行层次编号所得的序号与满二叉树序号一一对应的二叉树。

        完全二叉树具有很多特殊性质:分支节点的序号满足2i+1<n,左孩子节点下标为2i+1,右孩子的下标为2i+2,双亲节点的下标为i-1/2

3 2-树,扩充二叉树:不存在度为一的节点,除了叶子节点之外,分支节点均有两个孩子

堆和优先权队列:加入元素的次序无关紧要,取出元素一定是最高优先级的元素,逻辑上表现为完全二叉树,存储上是顺序表

1 堆:逻辑本质上是完全二叉树,根节点为堆顶。存储结构上完全二叉树可以用顺序表表示,堆分为最大堆和最小堆

2 优先权队列:若以权值大小为优先级,则最小堆/最大堆即可表示优先权队列,每次取出优先级高的元素就是取出堆顶元素

哈夫曼算法与哈夫曼树:利用权优先队列和二叉树的基本操作实现哈夫曼算法,哈夫曼树就是一棵追求每个节点权值最小的扩充二叉树

1 内路径长度和外路径长度:除叶节点外的路径长度之和称为内路径长度,叶节点路径长度之和称为外路径长度

2 加权路径长度WPL:所有叶节点的加权路径长度之和

3 哈夫曼树:为了暴力的追求最小加权路径长度,通过哈夫曼算法构建的扩充二叉树

4 哈夫曼编码:不同字符出现的频率不同,哈夫曼编码就是应用于使用频率差异较大的场合的不等长编码。倘如,有字符集和权值集(与出现频率呈现一一对应的关系),构造生成哈夫曼树,左孩子标零右孩子标一,到达对应字符的路径所经过的01序列则是该字符的哈夫曼编码,编码的文本长度就是哈夫曼树的加权路径长度

ADT与存储表示

二叉树ADT

ADT BinaryTree{

数据:节点的有限集合,可空或者由一个根节点和两颗子树构成,两颗子树也为二叉树

运算:

  Ceate(bt):构造空二叉树bt

  New Node(x,ln,rn):创建一个新节点,值为x,左右孩子为ln,rn

  IsEmpty(bt):判断二叉树是否为空,返回true

  TreeClear(bt):清空二叉树中的所有节点

  Root(bt,x):若二叉树非空,用x返回其根节点的值

  MakeTree(bt,x,left,right):构造一棵二叉树,根节点值为x,左右子树分别为left,right

  PreOrder(bt):先序遍历

  InOrder(bt):中序遍历

  PostOrder(bt):后序遍历

  LevelOrder(bt):层次遍历

...

}

二叉树基本操作

typedef struct btnode {                //定义二叉树节点,数据域,左右孩子指针
    char element;
    struct btnode* lChild;
    struct btnode* rChild;
}BTNode;

typedef struct binarytree {            //定义二叉树结构,包含一个根节点
    BTNode* root;
}BinaryTree;

/*
有关二叉树的相关定义操作
创建空树,创造树节点,返回根节点,构造新树,释放树空间
*/
void Create(BinaryTree* bt) {        //创建一棵空树
    bt->root = NULL;
}

BTNode* NewNode(char x, BTNode* ln, BTNode* rn) {        //创建一个新节点,值为x,ln,rn为左右孩子节点
    BTNode* p = (BTNode*)malloc(sizeof(BTNode));
    p->element = x;
    p->lChild = ln;
    p->rChild = rn;
    return p;
}

bool Root(BinaryTree* bt, char* x) {                    //返回树的根节点
    if (bt->root) {
        x = &bt->root->element;
        return true;
    }
    else {
        return false;
    }
}
void MakeTree(BinaryTree* bt, char e, BinaryTree* left, BinaryTree* right) {        //创建树,左右子树分别为left,right
    if (bt->root || left == right) {
        return;
    }
    bt->root = NewNode(e, left->root, right->root);
    left->root = NULL;                //左右置空防止共享节点,可能导致混乱
    right->root = NULL;
}

void Clear(BTNode* t) {
    if (!t)                                        //当前节点不为空才释放
        return;
    Clear(t->lChild);                            //递归过程从叶子节点开始,向上释放空间
    Clear(t->rChild);
    free(t);                                    //不可先释放子树的根节点,丢失了其后裔
}
void TreeClear(BinaryTree* bt) {                //释放二叉树的空间
    Clear(bt->root);                            //本质是释放节点的空间,也是一个递归过程
}


/*
先序遍历算法,递归
*/
void PreOrder(BTNode* t) {
    if (!t)                                        //判断节点是否为空
        return;
    printf("%c", t->element);                    //输出当前根节点的值,根
    PreOrder(t->lChild);                        //递归遍历左子树的根节点,左
    PreOrder(t->rChild);                        //递归遍历右子树的根节点,右
}
void PreOrderTree(BinaryTree* bt) {                //先序遍历二叉树
    PreOrder(bt->root);                            //本质是遍历节点,从根节点开始
}

/* 层次遍历算法,借助队列实现 */
/*
有关存储二叉树节点的队列Q的定义和操作
创建空队列,入队,出队,获取头节点,判空,释放队空间
*/
void create(Queue* Q, int mSize) {    //初始化空队列,front=rear =0
    Q->maxSize = mSize;
    Q->tnode = (Elemtype*)malloc(sizeof(Elemtype) * mSize);        //(Elemtype*)表示初始化数组的首地址
    Q->front = Q->rear = 0;
}

bool IsEmpty(Queue* Q) {
    return Q->front == Q->rear;        //注意循环队列对空的条件是front=rear
                                    //队满的条件是front =(rear+1)%maxsize,队首空间不放元素
}
    
void Destroy(Queue* Q) {            //销毁队列,四个结构置零
    Q->maxSize = 0;
    free(Q->tnode);
    Q->front = Q->rear = -1;
}

bool EnQueue(Queue *Q, Elemtype *t) {    //入队rear=(rear+1)%maxsize
    Q->rear = (Q->rear + 1) % Q->maxSize;
    Q->tnode[Q->rear]= *t;
    return true;
}
bool DeQueue(Queue* Q) {            //出队front=(front+1)%maxsize        
    if (IsEmpty(Q)) {
        return false;
    }
    Q->front = (Q->front + 1) % Q->maxSize;
    return true;
}

bool Front(Queue* Q, BTNode* *tnode) {            //获取队头节点,在tnode中位于front+1的位置
    if (IsEmpty(Q))
        return false;
    *tnode= &Q->tnode[(Q->front + 1) % Q->maxSize];
    return true;
}
void LevelOrderTree(BinaryTree* tree) {
    if (!tree->root)
        return;
    Queue Q;
    create(&Q, 100);
    BTNode *p = tree->root;
    EnQueue(&Q, p);
    while (!IsEmpty(&Q))
    {
        Front(&Q,&p);                //存放二叉树节点的队列结构体中tnode存在问题,应声明为BTnode* *tnode型
        DeQueue(&Q);
        printf("%c", p->element);
        if (p->lChild)
            EnQueue(&Q, p->lChild);
        if (p->rChild)
            EnQueue(&Q, p->rChild);
    }
    Destroy(&Q);
}

优先权队列的向上调整和向下调整

优先权队列ADT

ADT PriorityQueue{

  数据:最小堆,带权值的完全二叉树

  运算:

    Creat(PQ,mSize):创建一个空优先权队列

    Destroy(PQ):销毁一个优先权队列

    IsEmpty(PQ):对空则返回true

    IsFull(PQ):队满则返回true

    Size(PQ):获取当前队列的元素数量

    Append(PQ,x):将x加入优先权队列

    Serve(PQ,x):取出堆顶元素,并用x返回

...

}

//优先权队列的结构体
typedef struct priorityQueue {
    ElemType *elements;
    int n;
    int maxSize;
}PriorityQueue;

1 向上调整:对于一个优先权队列,在加入新元素时,其最小堆的性质被破坏,因此插入操作Append的关键操作是对新元素进行向上调整。

      以最小堆为例,首先找到当前节点的双亲节点,与自身比较,较小则交换二者,并设置p指针指向原来其双亲节点所在位置(保证p指针始终指向新添加元素),直到根节点

//将元素x加入优先权队列    heap[]数组为待调整的权优先队列,current为插入新节点的下标
void AdjustUp(ElemType heap[], int current) {
    int p = current;
    ElemType temp;
    while (p > 0) {                                                                
        if (heap[p].root->element < heap[(p - 1) / 2].root->element) {            //如果当前元素小于其双亲节点,交换两者
            temp = heap[p];                                                        //直到p指向根节点
            heap[p] = heap[(p - 1) / 2];
            heap[(p - 1) / 2] = temp;
            p = (p - 1) / 2;                        //p移到其双亲结点的位置,向上考察
        }
        else
            break;
    }
}

void Append(PriorityQueue* PQ, ElemType *x) {        //加入新元素的关键操作在于调整新加入的元素在权优先中的位置,即对数组下标n-1进行调整
    if (IsFull(PQ))                                    //两个参数,PQ为所操作的全优先队列,*x为要插入的树
        return;
    PQ->elements[PQ->n] = *x;                        //将树x插入队列最后
    PQ->n++;                                        //长度加一
    AdjustUp(PQ->elements, PQ->n - 1);                //前面的元素已经按照权优先规则,新插入向上调整形成最小堆
}

2 向下调整:对于一个优先权队列,在取出堆顶元素时,将堆底元素代替堆顶元素后,最小堆的性质被破坏,因此取元素Serve的关键操作是对堆顶元素进行下移操作。

      以最小堆为例,首先只有分支节点才需要向下调整,通过下标找到当前节点的左右孩子并比较,获取较小的一个minChild与当前元素值比较,将较大者下移,并设置p指针指向所移的左/右孩子的位置,直到叶子节点

//取出优先权队列中优先权最高的元素,通过x返回
void AdjustDown(ElemType heap[], int current, int border) {
    int p = current;
    int minChild;
    ElemType1 temp;
    while (2 * p + 1 <= border) {                                            //当当前节点为分支节点
        if ((2 * p + 2 <= border) && (heap[2 * p + 2].root->element <= heap[2 * p + 1].root->element))    //有右孩子,且右孩子较小
            minChild = 2 * p + 2;
        else                                                                //没有右节点,或者较大,就指向左孩子
            minChild = 2 * p + 1;
        if (heap[p].root->element <= heap[minChild].root->element)
            break;
        else {                                                                //其孩子节点比当前节点小就开始交换
            temp = heap[p];
            heap[p] = heap[minChild];
            heap[minChild] = temp;
            p = minChild;                                                    //p移至其孩子节点的位置,向下考察,直到p为叶子节点
        }
    }
}

void Serve(PriorityQueue* PQ, ElemType* x) {
    if (IsEmpty(PQ))
        return;
    *x = PQ->elements[0];
    PQ->n--;
    PQ->elements[0] = PQ->elements[PQ->n];
    AdjustDown(PQ->elements, 0, PQ->n - 1);
}

3 创建,销毁,判空,判满,获取元素

//创建一个空优先权队列
void CreatePQ(PriorityQueue* PQ, int mSize) {
    PQ->maxSize = mSize;
    PQ->n = 0;
    PQ->elements = (ElemType1*)malloc(mSize * sizeof(ElemType1));
}

//销毁一个优先权队列
void Destroy(PriorityQueue* PQ) {
    free(PQ->elements);
    PQ->n = 0;
    PQ->maxSize = 0;
}
//判断队空,空返回true
bool IsEmpty(PriorityQueue* PQ) {
    if (PQ->n == 0)
        return true;
    else
        return false;
}
//判断队满,满返回true
bool IsFull(PriorityQueue* PQ) {
    if (PQ->n == PQ->maxSize)
        return true;
    else
        return false;
}
//返回优先权队列元素的数量
int Size(PriorityQueue* PQ) {
    return PQ->n;
}

 

算法核心

哈夫曼算法是实现哈夫曼编码的核心,本质上是暴力追求每个节点的带权路径最小以实现总带权路径最小

算法步骤

1 对与权值集合w1~wn,生成n棵只含根节点且值为wi的森林F

2 获得该森林中权值最小的两棵树,合并两棵树,新树的权值是两子树根节点的权值之和,然后将新树加入森林F 

3 重复执行 2 操作,直到森林只剩下一棵树,该树就是哈夫曼树(2执行了n-1次)

具体实现

创建森林的操作需要构造n棵只含根节点的二叉树,需要使用到maketree()函数

获取森林 n棵树中的根节点权值最小的两棵树,可以使用权优先队列即最小堆PriorityQueue,堆顶即是权值最小的二叉树的根节点,使用maketree函数合并两棵树并插入优先权队列

BinaryTree CreateHFMTree(ElemType wealth[], int m) {
    PriorityQueue *PQ = (PriorityQueue*)malloc(sizeof(PriorityQueue));        //声明存储二叉树的全优先队列,分配空间
    BinaryTree* x = (BinaryTree*)malloc(sizeof(BinaryTree));                //声明树xyz并为其分配内存
    BinaryTree* y = (BinaryTree*)malloc(sizeof(BinaryTree));
    BinaryTree* z = (BinaryTree*)malloc(sizeof(BinaryTree));
    Create(x);
    Create(y);
    Create(z);
    CreatePQ(PQ, m);                                                        //调用函数传参时,确保pq,xzy,已分配内存
    for (int i = 0; i < m; i++) {                                            //循环创建只有一个根节点的树x,并入全优先队
        MakeTree(x, wealth[i], y, z);                                                        
        Append(PQ, x);
    }
    while (PQ->n > 1) {
        Serve(PQ, x);
        Serve(PQ, y);
        if (x->root->element < y->root->element)
            MakeTree(z, x->root->element + y->root->element, x, y);
        else
            MakeTree(z, x->root->element + y->root->element, y, x);
        Append(PQ, z);
    }
    Serve(PQ, x);
    return *x;
}

总结

对树这章节的掌握,主要就在于二叉树的存储结构和前中后序遍历和层次遍历的递归,非递归方式,掌握堆和权优先队列的存储结构,哈夫曼算法的基本思想,哈夫曼树的构造,了解哈夫曼编码

posted @ 2022-09-01 16:36  yahuhu  阅读(164)  评论(0)    收藏  举报