C++ STL 進階:手寫 priority_ueue 與仿函數(shù)機制詳解
1. 引言:什么是priority_queue
1.1 基本概念
priority_queue 是 C++ 標準庫中的一個容器適配器。它提供的是一種優(yōu)先級隊列的數(shù)據(jù)結構語義:
- 插入元素的時間復雜度為 O(log N)
- 刪除元素的時間復雜度為 O(log N)
- 獲取最高優(yōu)先級元素的時間復雜度為 O(1)
- 核心特性:每次
pop出來的,都是當前隊列中優(yōu)先級最高的元素。

1.2 底層結構
priority_queue底層默認使用std::vector作為容器,并在其之上維護一個堆結構:
- 大堆:父節(jié)點 ≥ 子節(jié)點 →
top()返回最大值 - 小堆:父節(jié)點 ≤ 子節(jié)點 →
top()返回最小值
堆的性質通過兩個核心操作來維護:向上調整(插入時) 和 向下調整(刪除時)。
但是priority_queue是反過來的:
默認傳小于less仿函數(shù),大的優(yōu)先級高;傳大于greater仿函數(shù),小的優(yōu)先級高。因此我們在實現(xiàn)的過程中也要依照次風格。
1.3 比較器的默認行為
priority_queue 有一個可選的模板參數(shù) Compare,用于定義元素之間的優(yōu)先級比較規(guī)則:
- 如果不顯式提供,默認使用
std::less<T> std::less<T>是一個仿函數(shù),內部調用operator<- 在默認
std::less下,priority_queue表現(xiàn)為大堆(最大值在堆頂)
如果你需要小堆,可以顯式傳入 std::greater<T> 或自定義仿函數(shù)。
注意:比較器不是“必須寫的”,它有默認行為。 是否顯式提供,取決于你是否需要改變默認的排序規(guī)則。我們下面會對比較器進行顯示構造來演示仿函數(shù)和比較器的行為。
2. priority_queue的核心實現(xiàn)
2.1 成員變量與默認構造
2.1.1 結構框架與默認構造
template<class T, class Container = vector<T>, class Compare = Less<T>>
//Container,Compare 是我們對庫里面的結構進行了顯示構造便于我們理解底層結構
class priority_queue {
public:
priority_queue() = default; // 編譯器生成默認構造
private:
Container _con; // 底層容器
};這里引出Container,Compare我們下面的章節(jié)進行詳細的解釋
2.1.2 迭代器區(qū)間構造
構造結構:
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
:_con(first, last)
{
//給一個迭代器區(qū)間構造優(yōu)先隊列的底層是堆結構
//向下調整建隊構造
for (int i = (_con.size() - 1 - 1) / 2; i >= 0; i--) {
adjust_down(i);
}
}
因為存儲底層結構是vector,邏輯結構是堆,所以說我們在構造的過程中就涉及到了建堆:(我們選擇向下調整建堆,時間效率比向上建隊高,具體原因這里不做闡述)
建堆時間復雜度O(N)。
向下調整算法:
void adjust_down(size_t parent) {
Compare com;
size_t child = parent * 2 + 1;
while (child < _con.size()) {
if (child + 1 < _con.size() && com(_con[child], _con[child + 1])) {
child++;
}
// 如果 父節(jié)點優(yōu)先級 < 孩子節(jié)點優(yōu)先級,則交換
// 在大堆中:parent < child -> com(parent, child) 為 true -> 交換
// 在小堆中:parent > child -> com(parent, child) 為 true -> 交換
if (com(_con[parent], _con[child])) {
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else {
break;
}
}
}
2.2 push插入數(shù)據(jù)
push邏輯:
void push(const T& x) {
_con.push_back(x);
adjust_up(_con.size() - 1);
}
插入數(shù)據(jù)并且需要維持堆結構,我們需要將插入的數(shù)據(jù)進行向上調整:
void adjust_up(size_t child) {
Compare com;
size_t parent = (child - 1) / 2;
while (child > 0) {
//if (_con[parent] < _con[child])
if(com(_con[parent],_con[child])){
swap(_con[parent], _con[child]);//調用的庫里面的函數(shù)
child = parent;
parent = (child - 1) / 2;
}
else {
break;
}
}
}2.3 pop刪除數(shù)據(jù)
void pop() {
assert(!empty());
std::swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
adjust_down(0);
}
2.4 top / empty / size
const T& top() const { return _con[0]; }
bool empty() const { return _con.empty(); }
size_t size() const { return _con.size(); }
對于上述的底層構造我們引出適配器模式和仿函數(shù)的使用,下面我們來詳細解釋一下??????
3. 適配器模式與priori_queue的設計
3.1 什么是適配器模式
適配器模式:把一個已有的東西包裝一下,換個接口或者行為再使用。
(實際上就像手機充電線一樣,有不同的接口來適配不同的手機。)
容器適配器:把底層容器包裝成另一種數(shù)據(jù)結構。在這里說人話也就是將容器類型引入到模板里面,讓你可以任意調用已有的底層容器的接口。
3.2 priority_queue的適配器設計
也就是:
template<class T, class Container = vector<T>, class Compare = Less<T>>
//Container讓我們可以很輕松的更改priority_queue的底層存儲邏輯
//這里只是給一個缺省值我們可以顯示給的
class priority_queue {
Container _con;
};
Container:底層容器類型(默認vector<T>)Compare:比較仿函數(shù)類型(默認Less<T>,對應大堆)
意義:模板適配器參數(shù)的缺省值讓用戶在使用時更簡潔,同時保留了高度可定制性
4. 仿函數(shù)
4.1 什么是仿函數(shù)
仿函數(shù)實際上就是將operator()進行了重載,讓類和結構體可以像函數(shù)一樣被調用。
仿函數(shù)的定義:
template<class T>
class Less {
public:
bool operator()(const T& x, const T& y) {
return x < y;
}
};
template<class T>
class Greater {
public:
bool operator()(const T& x, const T& y) {
return x > y;
}
};4.2 仿函數(shù)的用法
4.2.1 作為比較器,控制容器或者算法的行為
實際上就是將容器的規(guī)則或者算法的規(guī)則作為參數(shù)傳入,讓容器或者算法的行為可以定制。
代碼示例1:
int main() {
//默認使用 Less → 大堆
ZL::priority_queue<int> pq1;
//顯示指定Greater->建大堆
ZL::priority_queue<int, vector<int>, Greater<int>> pq2;
pq1.push(100);
pq1.push(5);
pq1.push(200);
pq1.push(10);
while (!pq1.empty()) {
cout << pq1.top() << " ";
pq1.pop();
}
return 0;
}示例代碼2:
template<class T, class Compare>
void BubbleSort(T* a, int n, Compare com) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n - i - 1; ++j) {
if (com(a[j + 1], a[j])) // 比較規(guī)則由仿函數(shù)決定
swap(a[j], a[j + 1]);
}
}
}
int main() {
int a[] = { 1,3,4,5,6,3,2 };
//讓類可以像函數(shù)一樣調用
BubbleSort(a, 7, Less<int>());//排升序
BubbleSort(a, 7, Greater<int>());//排降序
for (auto ch : a) {
cout << ch;//自動迭代不用++
}
cout << endl;
}解釋一下該過程:

4.2.2 作為判斷條件或者作為轉換規(guī)則
示例代碼1:查找第一個偶數(shù)(作為判斷條件)
struct OP1 {
bool operator()(int x) {
return x % 2 == 0;
}
};
int main() {
int a[] = { 1,3,2,9,1,3,4,5 };
//查找第一個偶數(shù)
auto it = find_if(a, a + 7, OP1());
cout << *it << endl
return 0;
}
示例代碼2:將偶數(shù)都乘二(作為轉換條件)
struct OP2 {
int operator()(int x) {
if (x % 2 == 0)
return x * 2;
else
return x;
}
};
int main() {
//transform 是 C++ 標準庫中的一個算法,用于對容器中的每個元素執(zhí)行某種操作,并將結果存儲到另一個位置。
//它屬于 <algorithm> 頭文件。
transform(v.begin(), v.end(), v.begin(), OP2());
for (auto& e : v) {
cout << e << " ";
}
cout << endl;
return 0;
}
4.2.3 處理指針時自定義比較邏輯
下面截取日期代碼的一部分做演示:
//struct PDateLess
//{
// bool operator()(const Date* p1, const Date* p2)//仿函數(shù)
// {
// return *p1 < *p2;//這里能直接對存儲地址解引用對比出誰的日期大是因為
//在日期結構體中對<運算符進行了重載
// }
int main() {
ZL::priority_queue<Date*, vector<Date*>, PDateLess> q1;
q1.push(new Date(2018, 10, 29));
q1.push(new Date(2018, 10, 28));
q1.push(new Date(2018, 10, 30));
while (!q1.empty()) {
cout << *q1.top() << " ";
q1.pop();
}
cout << endl;
return 0;
}這里自定義仿函數(shù)的原因是:指針的默認比較是按地址大小,而不是按 Date 對象的內容。通過仿函數(shù)可以“糾正”這個行為(也就是解引用獲得存儲在該地址里的內容)。
4.3 仿函數(shù)存在的意義
仿函數(shù)的存在實際上是C++想要摒棄函數(shù)指針的使用,仿函數(shù)提供了比函數(shù)指針更優(yōu)的替代方案,在現(xiàn)代 C++ 中優(yōu)先推薦使用仿函數(shù)或 lambda。
| 問題 | 沒有仿函數(shù)時的痛點 | 有仿函數(shù)時的解決方式 |
|---|---|---|
| 算法需要多種行為 | 寫多個函數(shù)或用函數(shù)指針,效率低且不能內聯(lián) | 把行為封裝成仿函數(shù),編譯期內聯(lián),無額外開銷 |
| 需要攜帶狀態(tài) | 函數(shù)無法記住之前的調用信息 | 仿函數(shù)可以有成員變量,在多次調用間保持狀態(tài) |
| 類型安全 | 函數(shù)指針類型檢查弱,容易傳錯 | 仿函數(shù)作為類類型,類型檢查更嚴格 |
| 與模板結合 | 函數(shù)指針作為模板參數(shù)不自然 | 仿函數(shù)是類型,作為模板參數(shù)使用方便 |
| 復雜規(guī)則封裝 | 難以復用 | 一個仿函數(shù)類可以在多個地方復用 |
到此這篇關于C++ STL 進階:手寫 priority_ueue 與仿函數(shù)機制詳解的文章就介紹到這了,更多相關C++ priority_ueue 與仿函數(shù)機制內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
VS2010/MFC編程(常用控件:樹形控件Tree Control控件創(chuàng)建h和實例)
本篇文章介紹了VS2010/MFC編程:常用控件:樹形控件Tree Control,包括樹形控件的創(chuàng)建、CTreeCtrl類的主要成員函數(shù)和應用實例有興趣的可以了解一下。2016-12-12
C語言實現(xiàn)學生信息管理系統(tǒng)開發(fā)
這篇文章主要為大家詳細介紹了C語言實現(xiàn)學生信息管理系統(tǒng)開發(fā),文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-08-08
C++實現(xiàn)LeetCode(65.驗證數(shù)字)
這篇文章主要介紹了C++實現(xiàn)LeetCode(65.驗證數(shù)字),本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內容,需要的朋友可以參考下2021-07-07

