必须用 std::priority_queue 配合自定义小顶堆比较器,否则合并顺序错乱、WPL 不再最优;因默认是大顶堆,需显式传入返回 a->weight > b->weight 的仿函数,且须手动管理内存、过滤零频字符、检查边界并安全释放节点。

必须用 std::priority_queue 配合自定义小顶堆比较器,否则合并顺序错乱、WPL 不再最优。
为什么 std::priority_queue 默认不能直接用
它默认是大顶堆,pq.top() 返回的是最大权值节点,但哈夫曼算法要求每次取最小两个权值节点合并。不干预的话,贪心选择直接失效,生成的树既不是最优,也不满足前缀码性质。
- 错误写法:
std::priority_queue<node></node>不加任何比较器 → 实际按地址或未定义行为排序 - 正确写法:显式传入仿函数,且
operator()必须返回a->weight > b->weight(注意是大于号) - 别用
std::greater<node></node>补救:它要求Node重载operator>,反而多一层出错可能 - 调试技巧:在循环里加一句
std::cout weight ,确认每次弹出的确实是当前最小值
Node 结构体必须含指针字段,不能只存权值
只塞 int 进队列(如 priority_queue<int></int>)会丢失所有结构信息——后续根本没法回溯生成编码,因为没有 left/right 指针来还原路径。
- 结构体至少要有:
char ch(叶子节点有效)、int weight、Node* left、Node* right - 内部节点的
ch应设为'\0',避免和叶子混淆;解码时只认ch != '\0'的节点 - 所有节点必须用
new Node(...)动态分配,不能复用旧节点内存——否则delete时 double free 或悬垂指针 - 合并逻辑严格四步:弹出两个、
new父节点、赋左右子、pq.push();少一步就断链
生成编码必须用 DFS 引用回溯,不能拼接字符串
用 std::string code 传值递归,每次调用都拷贝一份,在深度 >15 的树上极易触发频繁分配/销毁,性能差且栈易溢出;更严重的是回溯失效,右子树加的 '1' 不会自动消失。
- 正确方式:传
std::string& path,进左子树前path += '0',返回前path.pop_back() - 初始化时建议
path.reserve(256),避免多次扩容 - 遇到叶子节点(
node->ch != '\0')立刻写入std::unordered_map<char std::string>& codes</char>并return,别继续向下递归 - 单字符输入要单独处理:此时树只有根节点,
codes[ch] = "0"(或 "1"),不能跳过 DFS 直接返回空串
内存释放和边界检查容易被忽略
构建过程产生大量 new Node,但 std::priority_queue 不管理裸指针生命周期;不手动释放必然泄漏。而漏掉边界检查(如空输入、零频字符)会导致 top() 崩溃或生成无效分支。
- 建树前必须检查:
if (pq.size() == 0) return nullptr;、if (pq.size() == 1) { auto n = pq.top(); pq.pop(); return n; } - 预处理频率表时过滤
freq == 0的项,否则会引入无意义叶子节点 - 安全释放整棵树:用
std::vector<node> allNodes</node>在构建过程中记录所有new出来的节点指针(包括中间节点),最后统一for (auto n : allNodes) delete n; - 别在合并循环里
delete left或delete right——它们已被新节点引用,删了就成悬垂指针
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











