数据结构 哈夫曼树(HuffmanTree) 优先队列实现

哈夫曼树(HuffmanTree)

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近

简介

在计算机数据处理中,哈夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。
例如,在英文中,e的出现机率最高,而z的出现概率则最低。当利用哈夫曼编码对一篇英文进行压缩时,e极有可能用一个比特来表示,而z则可能花去25个比特(不是26)。用普通的表示方法时,每个英文字母均占用一个字节,即8个比特。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。
哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。所谓树的带权路径长度,就是树中所有的叶结点的权值乘上其到根结点的路径长度(若根结点为0层,叶结点到根结点的路径长度为叶结点的层数)。树的路径长度是从树根到每一结点的路径长度之和,记为WPL=(W1L1+W2L2+W3L3+…+WnLn),N个权值Wi(i=1,2,…n)构成一棵有N个叶结点的二叉树,相应的叶结点的路径长度为Li(i=1,2,…n)。可以证明哈夫曼树的WPL是最小的。

术语

哈夫曼树又称为最优树.

  1. 路径和路径长度
    在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1。
  2. 结点的权及带权路径长度
    若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积。
  3. 树的带权路径长度
    树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL。

构建哈夫曼树

  1. 创建一个包含所有字符的节点的优先队列(或最小堆),其中每个节点的权重是对应字符的频率。
  2. 从队列中选择两个具有最小权重的节点,并创建一个新节点作为它们的父节点。新节点的权重是这两个节点的权重之和。
  3. 将新节点插入队列。
  4. 重复步骤2和步骤3,直到队列中只剩下一个节点,这个节点就是哈夫曼树的根节点。
  5. 通过遍历哈夫曼树,给每个字符分配一个唯一的编码。通常,向左走表示添加一个"0",向右走表示添加一个"1"。

多叉哈夫曼树

哈夫曼树也可以是k叉的,只是在构造k叉哈夫曼树时需要先进行一些调整。构造哈夫曼树的思想是每次选k个权重最小的元素来合成一个新的元素,该元素权重为k个元素权重之和。但是当k大于2时,按照这个步骤做下去可能到最后剩下的元素少于k个。解决这个问题的办法是假设已经有了一棵哈夫曼树(且为一棵满k叉树),则可以计算出其叶节点数目为(k-1)nk+1,式子中的nk表示子节点数目为k的节点数目。于是对给定的n个权值构造k叉哈夫曼树时,可以先考虑增加一些权值为0的叶子节点,使得叶子节点总数为(k-1)nk+1这种形式,然后再按照哈夫曼树的方法进行构造即可。

实现

实现哈夫曼树的方式有很多种,可以使用优先队列(Priority Queue)简单达成这个过程,给与权重较低的符号较高的优先级(Priority),算法如下:

  1. 把n个终端节点加入优先队列,则n个节点都有一个优先权Pi,1 ≤ i ≤ n
  2. 如果队列内的节点数>1,则:
    ⑴从队列中移除两个最小的Pi节点,即连续做两次remove(min(Pi), Priority_Queue)
    ⑵产生一个新节点,此节点为(1)之移除节点之父节点,而此节点的权重值为(1)两节点之权重和
    ⑶把(2)产生之节点加入优先队列中
  3. 最后在优先队列里的点为树的根节点(root)
    而此算法的时间复杂度(Time Complexity)为O(n log n);因为有n个终端节点,所以树总共有2n-1个节点,使用优先队列每个循环须O(log n)。
    实现代码:(以cpp为例)
#include 
#include 
#include 

using namespace std;

// 哈夫曼树节点的定义
struct Node {
    char data;
    int frequency;
    Node* left;
    Node* right;

    Node(char data, int frequency) : data(data), frequency(frequency), left(nullptr), right(nullptr) {}

    // 用于 priority_queue 中比较节点的大小
    bool operator>(const Node& other) const {
        return frequency > other.frequency;
    }
};

// 构建哈夫曼树的函数
Node* buildHuffmanTree(const unordered_map& frequencies) {
    // 优先队列,用于存储节点,并按照频率从小到大排列
    priority_queue, greater> pq;

    // 将字符频率转换为节点,并加入优先队列
    for (const auto& entry : frequencies) {
        pq.push(Node(entry.first, entry.second));
    }

    // 构建哈夫曼树
    while (pq.size() > 1) {
        // 取出两个最小频率的节点
        Node* left = new Node(pq.top().data, pq.top().frequency);
        pq.pop();
        Node* right = new Node(pq.top().data, pq.top().frequency);
        pq.pop();

        // 创建一个新节点作为它们的父节点,并将新节点加入优先队列
        Node* internalNode = new Node('\0', left->frequency + right->frequency);
        internalNode->left = left;
        internalNode->right = right;
        pq.push(*internalNode);
    }

    // 返回哈夫曼树的根节点
    return new Node('\0', pq.top().frequency);
}

// 生成哈夫曼编码的递归辅助函数
void generateHuffmanCodes(Node* root, string currentCode, unordered_map& codes) {
    if (root) {
        if (root->data != '\0') {
            codes[root->data] = currentCode;
        }
        generateHuffmanCodes(root->left, currentCode + "0", codes);
        generateHuffmanCodes(root->right, currentCode + "1", codes);
    }
}

// 生成哈夫曼编码的函数
unordered_map getHuffmanCodes(Node* root) {
    unordered_map codes;
    generateHuffmanCodes(root, "", codes);
    return codes;
}

int main() {
    // 示例字符频率字典
    unordered_map frequencies = {{'a', 5}, {'b', 9}, {'c', 12}, {'d', 13}, {'e', 16}, {'f', 45}};

    // 构建哈夫曼树
    Node* root = buildHuffmanTree(frequencies);

    // 生成哈夫曼编码
    unordered_map codes = getHuffmanCodes(root);

    // 打印字符和对应的哈夫曼编码
    for (const auto& entry : codes) {
        cout << entry.first << ": " << entry.second << endl;
    }

    return 0;
}

此外,有一个更快的方式使时间复杂度降至线性时间(Linear Time)O(n),就是使用两个队列(Queue)创建哈夫曼树。第一个队列用来存储n个符号(即n个终端节点)的权重,第二个队列用来存储两两权重的合(即非终端节点)。此法可保证第二个队列的前端(Front)权重永远都是最小值,且方法如下:
4. 把n个终端节点加入第一个队列(依照权重大小排列,最小在前端)
5. 如果队列内的节点数>1,则:
⑴从队列前端移除两个最低权重的节点
⑵将(1)中移除的两个节点权重相加合成一个新节点
⑶加入第二个队列
6. 最后在第一个队列的节点为根节点

虽然使用此方法比使用优先队列的时间复杂度还低,但是注意此法的第1项,节点必须依照权重大小加入队列中,如果节点加入顺序不按大小,则需要经过排序,则至少花了O(n log n)的时间复杂度计算。
但是在不同的状况考量下,时间复杂度并非是最重要的,如果我们考虑英文字母的出现频率,变量n就是英文字母的26个字母,则使用哪一种算法时间复杂度都不会影响很大,因为n不是一笔庞大的数字。

实现代码:(以cpp为例)

#include 
#include 
#include 

using namespace std;

// 哈夫曼树节点的定义
struct Node {
    char data;
    int frequency;
    Node* left;
    Node* right;

    Node(char data, int frequency) : data(data), frequency(frequency), left(nullptr), right(nullptr) {}

    // 用于 priority_queue 中比较节点的大小
    bool operator>(const Node& other) const {
        return frequency > other.frequency;
    }
};

// 构建哈夫曼树的函数
Node* buildHuffmanTree(const vector& weights) {
    priority_queue, greater> minHeap; // 存储最小权重的节点
    for (int i = 0; i < weights.size(); ++i) {
        minHeap.push(new Node(char('a' + i), weights[i]));
    }

    while (minHeap.size() > 1) {
        // 取出两个最小权重的节点
        Node* left = minHeap.top();
        minHeap.pop();
        Node* right = minHeap.top();
        minHeap.pop();

        // 创建一个新节点作为它们的父节点,并将新节点加入最小堆
        Node* internalNode = new Node('\0', left->frequency + right->frequency);
        internalNode->left = left;
        internalNode->right = right;
        minHeap.push(internalNode);
    }

    // 返回哈夫曼树的根节点
    return minHeap.top();
}

// 生成哈夫曼编码的递归辅助函数
void generateHuffmanCodes(Node* root, string currentCode, unordered_map& codes) {
    if (root) {
        if (root->data != '\0') {
            codes[root->data] = currentCode;
        }
        generateHuffmanCodes(root->left, currentCode + "0", codes);
        generateHuffmanCodes(root->right, currentCode + "1", codes);
    }
}

// 生成哈夫曼编码的函数
unordered_map getHuffmanCodes(Node* root) {
    unordered_map codes;
    generateHuffmanCodes(root, "", codes);
    return codes;
}

int main() {
    // 示例权重数组
    vector weights = {5, 9, 12, 13, 16, 45};

    // 构建哈夫曼树
    Node* root = buildHuffmanTree(weights);

    // 生成哈夫曼编码
    unordered_map codes = getHuffmanCodes(root);

    // 打印字符和对应的哈夫曼编码
    for (const auto& entry : codes) {
        cout << entry.first << ": " << entry.second << endl;
    }

    return 0;
}

综合题

【问题描述】在数据压缩问题中,需要将数据文件转换成由二进制字符0、1组成的二进制串,称之为编码,已知待压缩的数据中包含若干字母(A-Z),为获得更好的空间效率,请设计有效的用于数据压缩的二进制编码,使数据文件压缩后编码总长度最小,并输出这个最小长度值。

【输入形式】待压缩的数据(长度不大于100的大写字母)

【输出形式】编码的最小总长度值

【样例输入】ABACCDA

【样例输出】13

【样例说明】A编码0,B编码110,C编码10,D编码111,ABACCDA的编码为0110010101110

代码实现

#include 
#include 
#include 

using namespace std;

const int MAX_CHAR = 26;  // 大写字母的个数

// 定义节点结构
struct Node {
    char data;
    unsigned freq;
    Node* left, *right;

    Node(char data, unsigned freq) : data(data), freq(freq), left(nullptr), right(nullptr) {}
};

// 比较节点的频率
struct compare {
    bool operator()(Node* left, Node* right) {
        return (left->freq > right->freq);
    }
};
//构建最小堆时用作比较函数

// 生成哈夫曼树
Node* buildHuffmanTree(const string& data) {
    priority_queue, compare> minHeap;

    // 统计字符频率
    int freq[MAX_CHAR] = {0};
    for (char c : data) {
        freq[c - 'A']++;
    }

    // 创建节点并加入最小堆
    for (int i = 0; i < MAX_CHAR; ++i) {
        if (freq[i] > 0) {
            minHeap.push(new Node('A' + i, freq[i]));
        }
    }

    // 构建哈夫曼树
    while (minHeap.size() > 1) {
        Node* left = minHeap.top();
        minHeap.pop();

        Node* right = minHeap.top();
        minHeap.pop();

        Node* newNode = new Node('$', left->freq + right->freq);//'$'用作内部节点数据,没有实际意义
        newNode->left = left;
        newNode->right = right;

        minHeap.push(newNode);
    }

    return minHeap.top();
}

// 计算哈夫曼编码长度
unsigned calculateHuffmanCodeLength(Node* root, unsigned depth = 0) {
    if (!root)
        return 0;

    if (!root->left && !root->right)
        return root->freq * depth;
//递归计算哈夫曼树中每个叶子节点的编码长度,即路径长度乘以叶子节点的频率。
    return calculateHuffmanCodeLength(root->left, depth + 1) + calculateHuffmanCodeLength(root->right, depth + 1);
}

// 主函数
int main() {
    string input;
    cin >> input;

    if (input.size() > 100) {

        return 1;
    }

    // 生成哈夫曼树
    Node* root = buildHuffmanTree(input);

    // 计算最小编码长度
    unsigned minLength = calculateHuffmanCodeLength(root);

    cout << minLength << endl;

    return 0;
}

上面代码使用了优先队列实现,使用数组统计频率,实现基于哈夫曼编码的数据压缩。

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