往期内容回顾
从零实现 priority_queue(二叉堆)并理解仿函数的使用
前言
priority_queue(优先队列)是算法与工程中非常常见的数据结构:它按“优先级”而不是插入顺序返回元素。C++ 标准库中 std::priority_queue 基于堆(通常是二叉堆)实现,默认用 std::vector 作为底层容器,比较器默认为 std::less<T>(即最大堆)。
在实现优先队列时,我们同时会遇到“比较器”这一核心点:比较器既可以是标准的 std::less/std::greater,也可以是我们自定义的仿函数(函数对象)、函数指针或 lambda。理解仿函数的工作方式与设计对实现灵活且高效的优先队列非常关键。
本文目标:
-
从零实现一个模板化、可定制比较器的二叉堆 priority_queue<T, Compare>,并且支持常见操作(push/pop/top/)。
-
深入讲解比较器(仿函数)如何工作,如何使用函数对象、std::greater/std::less,以及性能注意点。
主要内容概览
-
1、二叉堆的数据结构与基本算法(Adjust-up、Adjust-down)
-
2、面向模板的实现:支持泛型元素与比较器类型
-
3、仿函数(functor)详解:定义、用途、状态ful 比较器与 lambda 的区别
-
4、用法示例:最大堆、最小堆、按 pair 的第二元比较、流数据 top-k 等
-
总结与工程建议
分节逐步实现与讲解
1) 二叉堆的基本思想(直观)
-
用连续数组(std::vector<T>)表示完全二叉树。
-
下标关系(0-based):父节点 p = (i-1)/2,左子 2*i+1,右子 2*i+2。
-
push:把新元素加到数组末尾,然后向上调整(Adjust-up),直到满足堆序(父优先级 >= 子)。
-
pop:将堆顶元素与末尾交换并删除末尾,然后对新堆顶进行向下调整(Adjust-down)。
注意:堆的“谁在堆顶”由比较器决定。我们使用模板参数 Compare,并约定:Compare(a, b) 返回 true 当且仅当 a < b(按某种意义),换言之,如果 Compare 是 std::less<T>,我们会得到 最大堆(值大的在顶)。本文在实现中会严格说明比较器语义,避免混淆。
2) 代码:完整的 priority_queue 实现
#include<iostream> #include<assert.h> using namespace std; // 比较器仿函数 template<typename T> struct Less { bool operator()(const T& t1, const T& t2){ return t1<t2; // 小于 => 大顶堆 }; }; template<typename T> struct Greater { bool operator()(const T& t1, const T& t2){ return t1 > t2;// 大于 => 小顶堆 }; }; template<typename T,class Container = vector<T>, typename Compare =Less<T>> class my_priority_queue{ private: Container _con; public: // 向上调整(插入时) void Adjust_up(int child){ Compare com; int parent = (child-1)/2; while (child>0) { // if(_con[child] > _con[parent]){ if(com(_con[parent],_con[child])){// 父比子小 => 交换 ::swap(_con[child],_con[parent]); child = parent; parent = (child-1)/2; } else{ break; } } } // 向下调整(删除堆顶时) void Adjust_down(int root){ Compare com; int parent =root; int child = 2*parent+1; while (child<_con.size()) { // if(child+1 < _con.size() && _con[child+1] > _con[child]){ if(child+1 < _con.size() && com(_con[child],_con[child+1])){ ++child;// 选更优的孩子 } if(com(_con[parent],_con[child])){ ::swap(_con[child],_con[parent]); parent = child; child = 2*parent+1; } else{ break; } } } void push(const T&x){ _con.push_back(x); Adjust_up(_con.size()-1); } void pop(){ swap(_con[0],_con[_con.size()-1]); _con.pop_back(); Adjust_down(0); } T& top(){ return _con.front(); } size_t size(){ return _con.size(); } bool empty(){ return _con.empty(); } };
实现要点说明:
-
Compare 的语义:我们约定 comp_(a, b) 表明 a “比” b(按某种规则)——返回 true 当且仅当 a < b(例:std::less)。在 Adjust_up / Adjust__down 中我们把 less_than(parent, child) 为真作为交换条件,这样当 Compare 是 std::less(a < b),子节点更大则上浮,得到一个 最大堆。
-
push / pop 的复杂度为 O(log n)。
-
我在 top() / pop() 上抛 std::out_of_range,以便更安全;标准 std::priority_queue 在空容器上行为未定义,但博客示例里出错更显眼。你可以改成断言或与 std::priority_queue 行为一致。
3) 如何用不同比较器得到最大堆 / 最小堆 / 自定义优先级
默认(最大堆)
void test1(){ my_priority_queue<int,vector<int>,less<int>> pq; pq.push(6); pq.push(3); pq.push(9); pq.push(2); pq.push(12); while(!pq.empty()){ cout << pq.top()<<" "; pq.pop(); } cout<< endl; }
最小堆(把小元素当成优先级高)
void test1(){ my_priority_queue<int,vector<int>,Greater<int>> pq; pq.push(6); pq.push(3); pq.push(9); pq.push(2); pq.push(12); while(!pq.empty()){ cout << pq.top()<<" "; pq.pop(); } cout<< endl; }
使用仿函数对 pair 按第二元素比较
先定义比较器(仿函数):
template<typename T> struct Less { bool operator()(const T& t1, const T& t2){ return t1<t2; // 小于 => 大顶堆 }; }; template<typename T> struct Greater { bool operator()(const T& t1, const T& t2){ return t1 > t2;// 大于 => 小顶堆 }; };
注意:上面比较器返回 a.second < b.second,这使得当 b.second 比 a.second 大时 comp_(a,b) 为 true,因此 b 会在堆中上浮成为更高优先级(符合我们在实现中 less_than 的使用逻辑)。
4) 仿函数(Functor)的全面讲解
什么是仿函数?
仿函数是定义了 operator() 的类/结构体实例。它像函数一样可调用,但又能携带状态(成员变量),并且类型是可编译期确定的(比 std::function 更高效)。
形式举例:
-
无状态仿函数(最常见):
struct Cmp {
bool operator()(int a, int b) const { return a < b; }
};
-
有状态(stateful)仿函数:
struct ModCmp {
int mod;
ModCmp(int m): mod(m) {}
bool operator()(int a, int b) const {
return (a % mod) < (b % mod);
}
};
// 使用:
BinaryHeap<int, ModCmp> h(ModCmp(10)); // comparator 带有 state=10
为什么常用仿函数而不是函数指针?
-
仿函数可内联,调用开销小;可携带状态(函数指针不能)。
-
仿函数类型参与模板实例化,能启用编译期优化(如内联)。
注意比较器的方向很容易混淆:
-
std::less<T> -> 把较大的元素放在堆顶(max-heap)。
-
std::greater<T> -> 把较小的元素放在堆顶(min-heap)。
-
你定义自己的 operator() 时,建议用“返回 a < b” 的直觉来实现,然后按上文实现中该比较器的语义来理解结果。
5) 用例:Top-k 流式处理(min-heap 技巧)
经典场景:求数据流中前 k 大元素,做法是维护一个大小为 k 的最小堆,堆顶始终是当前第 k 大元素。
class Solution { public: int findKthLargest(vector<int>& nums, int k) { for(int i = 0;i<k;++i){ pq.push(nums[i]); } for(int j = k;j<nums.size();++j){ if(nums[j] >= pq.top()){ pq.pop(); pq.push(nums[j]); } } return pq.top(); } priority_queue<int,vector<int>, greater<int>> pq; };
复杂度:每次操作 O(log k),总体 O(n log k)。
总结(结语 & 推荐)
-
我们从头实现了一个模板化的 priority_queue<T, Compare>:支持 push/pop/top可配置比较器。代码清晰且能直接替代 std::priority_queue 做教学或定制用途。
-
仿函数(functor)是实现可定制比较器的首选:它可以被编译器内联、携带状态、并能作为模板参数消除运行时开销。
-
在实现与使用中要注意:比较器语义(less vs greater) 问题。