目录
哈夫曼树(Huffman Tree):定义、操作及时空复杂度分析
(二)C++ 代码实现(简化示意,使用 std::priority_queue)
(二)C++ 代码实现(示意修改权值后简单调整情况,不完整严格调整)
一、哈夫曼树的定义
哈夫曼树又称最优二叉树,是一种带权路径长度最短的二叉树。对于一棵二叉树,其带权路径长度(WPL,Weighted Path Length)是树中所有叶节点的权值乘以其到根节点的路径长度(路径上的边数)之和。例如,假设有叶节点权重分别为 w1
、w2
、w3
,对应的路径长度为 l1
、l2
、l3
,那么这棵树的 WPL = w1 * l1 + w2 * l2 + w3 * l3
。哈夫曼树就是在给定一组带权值的节点时,通过特定的构造方法得到的使得 WPL
最小的二叉树结构。
哈夫曼树通常用于数据压缩领域,权重可以理解为字符出现的频率,频率越高的字符在哈夫曼树中的路径长度往往越短,编码时使用较短的编码表示,从而实现数据的有效压缩。
二、哈夫曼树的节点结构(C++ 示例)
struct HuffmanTreeNode {
int weight; // 节点的权值(比如字符的频率)
HuffmanTreeNode* left;
HuffmanTreeNode* right;
HuffmanTreeNode(int w) : weight(w), left(NULL), right(NULL) {}
};
三、哈夫曼树的构建操作(核心操作,相当于创建)
(一)操作原理
- 首先,将给定的所有带权值的节点看作是一个个只有根节点的独立二叉树,放入一个集合(通常用优先队列来实现,按照权值从小到大排序)。
- 然后,每次从集合中取出权值最小的两棵二叉树,将它们合并为一棵新的二叉树,新树的根节点权值为这两棵子树的根节点权值之和,左右子树分别为取出的那两棵二叉树。
- 重复步骤 2,直到集合中只剩下一棵二叉树,这棵二叉树就是最终的哈夫曼树。
例如,有节点权重分别为 2
、3
、5
、7
,先把它们各自作为单独的树放入优先队列。第一次取出权重为 2
和 3
的树,合并成一棵新树,根节点权重为 5
,左右子树分别为原权重 2
和 3
的树;接着再从队列中取树继续合并,直到最后得到完整的哈夫曼树。
(二)C++ 代码实现(简化示意,使用 std::priority_queue
)
#include <queue>
using namespace std;
HuffmanTreeNode* buildHuffmanTree(vector<int> weights) {
priority_queue<HuffmanTreeNode*, vector<HuffmanTreeNode*>,
function<bool(HuffmanTreeNode*, HuffmanTreeNode*)>> pq(
[](HuffmanTreeNode* a, HuffmanTreeNode* b) {
return a->weight > b->weight;
});
// 将每个权重节点作为单独的树放入优先队列
for (int w : weights) {
pq.push(new HuffmanTreeNode(w));
}
while (pq.size() > 1) {
HuffmanTreeNode* left = pq.top();
pq.pop();
HuffmanTreeNode* right = pq.top();
pq.pop();
HuffmanTreeNode* parent = new HuffmanTreeNode(left->weight + right->weight);
parent->left = left;
parent->right = right;
pq.push(parent);
}
return pq.top();
}
(三)时间复杂度分析
构建哈夫曼树的过程中,每次从优先队列中取出两个最小权值的节点并插入新节点,总共需要进行 n - 1
次合并操作(n
是带权节点的数量)。每次操作涉及到优先队列的取出和插入,优先队列(一般用堆实现)的插入和删除操作时间复杂度为 O(log n)
,所以构建哈夫曼树总的时间复杂度为 O(n log n)
。
(四)空间复杂度分析
在构建过程中,需要额外的空间来存储节点以及维护优先队列。对于 n
个带权节点,最终构建的哈夫曼树有 2n - 1
个节点(因为每次合并增加一个新节点),所以节点存储空间为 O(n)
。优先队列最多存储 n
个节点(在合并过程中),其空间复杂度相对节点存储空间来说也是 O(n)
,所以总体空间复杂度为 O(n)
。
四、哈夫曼树的查找操作
(一)操作原理
哈夫曼树的查找操作通常是查找某个特定权值的节点或者判断某个节点是否在树中,一般基于递归遍历的方式进行。从根节点开始,比较要查找的值与当前节点的权值,如果相等则找到;如果要查找的值小于当前节点权值,则在左子树中继续查找;如果大于,则在右子树中查找,直到找到目标或者到达空节点(表示未找到)。
(二)C++ 代码实现
bool search(HuffmanTreeNode* root, int target) {
if (root == NULL) return false;
if (root->weight == target) return true;
return search(root->left, target) || search(root->right, target);
}
(三)时间复杂度分析
由于查找操作需要遍历哈夫曼树的节点,最坏情况下需要遍历所有节点,哈夫曼树的节点数量为 2n - 1
(n
为原始带权节点数量),所以时间复杂度为 O(n)
。在平均情况下,如果树相对平衡,时间复杂度也接近 O(log n)
,不过通常按照构建规则,其形态不一定是严格平衡二叉树,所以一般按最坏情况考虑。
(四)空间复杂度分析
查找操作主要通过递归实现,递归调用栈的空间取决于树的高度,哈夫曼树的高度最大可以达到 n
(极端不平衡情况),所以空间复杂度最坏为 O(n)
,在平均情况下,如果相对平衡,空间复杂度约为 O(log n)
,但通常考虑最坏情况。
五、哈夫曼树的删除操作
(一)操作原理
删除哈夫曼树中的节点相对复杂些,一般如果要删除叶节点,需要先找到其双亲节点,将双亲节点指向该叶节点的指针置为 NULL
,然后释放叶节点的内存空间。如果删除的是非叶节点,需要处理好其左右子树的连接关系,比如可以将其左子树或者右子树的节点重新调整构建新的哈夫曼树(具体取决于应用场景要求),然后释放被删除节点的内存空间。
(二)C++ 代码实现(简化示意删除叶节点情况)
void deleteLeaf(HuffmanTreeNode* &root, HuffmanTreeNode* leaf) {
if (root == leaf) {
root = NULL;
return;
}
queue<HuffmanTreeNode*> q;
q.push(root);
while (!q.empty()) {
HuffmanTreeNode* current = q.front();
q.pop();
if (current->left == leaf) {
current->left = NULL;
delete leaf;
return;
} else if (current->right == leaf) {
current->right = NULL;
delete leaf;
return;
}
if (current->left!= NULL) q.push(current->left);
if (current->right!= NULL) q.push(current->right);
}
}
(三)时间复杂度分析
删除操作需要先查找要删除的节点及其双亲节点(如果删除叶节点),查找过程时间复杂度最坏为 O(n)
(遍历整棵树)。然后进行节点指针调整和内存释放等操作,这些操作时间复杂度相对查找来说较小,所以总体时间复杂度为 O(n)
。
(四)空间复杂度分析
如果采用队列辅助遍历树来查找节点(如上述代码示例),队列中最多存储一层的节点,在最坏情况下(树是一条链形态),需要存储 n
个节点(n
为哈夫曼树节点数量),再加上递归调用栈空间(最坏也是 O(n)
),所以总体空间复杂度为 O(n)
。不过在实际应用中,删除操作可能并不频繁,而且可以根据具体场景优化空间使用情况。
六、哈夫曼树的修改操作
(一)操作原理
修改哈夫曼树中的节点信息(比如修改节点的权值),首先要通过查找操作找到目标节点,然后更新其权值。但修改权值后可能会破坏哈夫曼树原有的最优性质(带权路径长度最短),所以可能需要重新调整树的结构,一般是重新构建部分树或者整个树,使其再次满足哈夫曼树的性质,这个过程和构建哈夫曼树的部分操作类似。
(二)C++ 代码实现(示意修改权值后简单调整情况,不完整严格调整)
void modifyWeight(HuffmanTreeNode* root, int target, int newWeight) {
if (root == NULL) return;
if (root->weight == target) {
root->weight = newWeight;
// 简单调整示例(可能不严格保证哈夫曼树性质)
if (root->left!= NULL && root->right!= NULL) {
if (root->left->weight > root->right->weight) {
swap(root->left, root->right);
}
}
return;
}
modifyWeight(root->left, target, newWeight);
modifyWeight(root->right, target, newWeight);
}
(三)时间复杂度分析
修改操作依赖查找操作先定位节点,查找时间复杂度最坏为 O(n)
。修改后调整树结构的时间复杂度取决于调整的复杂程度,简单调整如上述代码示例时间复杂度相对较低,但如果严格按照哈夫曼树性质重新构建部分或整个树,时间复杂度会接近构建哈夫曼树的 O(n log n)
,所以总体时间复杂度在 O(n)
到 O(n log n)
之间,具体取决于调整策略和实际情况。
(四)空间复杂度分析
和查找操作类似,递归调用栈空间取决于树的高度,最坏为 O(n)
,再加上如果重新构建树等操作可能需要额外的空间来存储临时节点等,不过主要空间还是取决于树本身节点数量,总体空间复杂度一般也在 O(n)
左右(考虑到实际调整情况不同可能有一定变化)。
哈夫曼树在数据压缩等领域有着极为重要的应用,通过合理构建和利用其结构特性,能有效地减少数据存储空间,提高数据传输和存储的效率,尽管其操作在某些方面有一定复杂度,但在特定应用场景下所带来的优势使其成为一种非常有价值的数据结构。