1.priority_queue
1.1 priority_queue的介紹
priority_queue 官方文檔介紹
1. 優先隊列是一種容器擴充卡,根據嚴格的弱排序标準,它的第一個元素總是它所包含的元素中最大的。
2. 此上下文類似于堆,在堆中可以随時插入元素,并且隻能檢索最大堆元素(優先隊列中位于頂部的元素)。
3. 優先隊列被實作為容器擴充卡,容器擴充卡即将特定容器類封裝作為其底層容器類,queue提供一組特定的成員函數來通路其元素。元素從特定容器的“尾部”彈出,其稱為優先隊列的頂部。
4. 底層容器可以是任何标準容器類模闆,也可以是其他特定設計的容器類。容器應該可以通過随機通路疊代器通路,并支援以下操作:
empty():檢測容器是否為空
size():傳回容器中有效元素個數
front():傳回容器中第一個元素的引用
push_back():在容器尾部插入元素
pop_back():删除容器尾部元素
5. 标準容器類vector和deque滿足這些需求。預設情況下,如果沒有為特定的priority_queue類執行個體化指定容器類,則使用vector。
6. 需要支援随機通路疊代器,以便始終在内部保持堆結構。容器擴充卡通過在需要時自動調用算法函數
make_heap、push_heap和pop_heap來自動完成此操作。
1.2 priority_queue 的使用及模拟實作
優先級隊列預設使用vector作為其底層存儲資料的容器,在vector上又使用了堆算法将vector中元素構造成堆的結構,是以priority_queue就是堆,所有需要用到堆的位置,都可以考慮使用priority_queue。注意: 預設情況下priority_queue是大堆。
函數聲明 | 接口說明 |
priority_queue() / priority_queue(first,last) | 構造一個空的優先級隊列 |
empty() | 檢測優先級隊列是否為空,是傳回true,否則傳回 false |
top() | 傳回優先級隊列中最大(最小元素),即堆頂元素 |
push(x) | 在優先級隊列中插入元素x |
pop() | 删除優先級隊列中最大(最小)元素,即堆頂元素 |
注意:
1、預設情況下,priority_queue是大堆
int main(){
vector<int> v = { 3,2,5,7,1,10,9,8,6,4 };
priority_queue<int> q1;
for (auto& e : v)
q1.push(e);
while (!q1.empty())
{
cout << q1.top() << " ";
q1.pop();
}
cout << endl;
//如果要建立小堆,将第三個模闆參數換成greater比較方式
priority_queue<int, vector<int>, greater<int>> q2(v.begin(), v.end());
while (!q2.empty())
{
cout << q2.top() << " ";
q2.pop();
}
return 0;
}
2.、如果在priority_queue中放自定義類型的資料,使用者需要在自定義類型中提供> 或者< 的重載。
比如我們之前寫過的日期類,如果要比較兩個日期的大小,就要自己重載>或者<
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
bool operator<(const Date& d)const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
bool operator>(const Date& d)const
{
return (_year > d._year) ||
(_year == d._year && _month > d._month) ||
(_year == d._year && _month == d._month && _day > d._day);
}
friend ostream& operator<<(ostream& _cout, const Date& d)
{
_cout << d._year << "-" << d._month << "-" << d._day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
void TestPriorityQueue(){
// 大堆,需要使用者在自定義類型中提供<的重載
priority_queue<Date> q1;
q1.push(Date(2018, 10, 29));
q1.push(Date(2018, 10, 28));
q1.push(Date(2018, 10, 30));
cout << q1.top() << endl;
// 如果要建立小堆,需要使用者提供>的重載
priority_queue<Date, vector<Date>, greater<Date>> q2;
q2.push(Date(2018, 10, 29));
q2.push(Date(2018, 10, 28));
q2.push(Date(2018, 10, 30));
cout << q2.top() << endl;
}
int main(){
TestPriorityQueue();
return 0;
}
模拟實作:
我們剛才已經明确priority_queue就是堆,并且預設是大堆,并且priority_queue的底層存儲資料的容器是vector,是以priority_queue也是非常好實作的
//優先級隊列 -- 大堆 < 小堆 >
template<class T, class Container = vector<T>,class Compare = less<T>>
class priority_queue
{
public:
void AdjustUp(int child)
{
Compare comFunc;
int parent = (child - 1) / 2;
while (child > 0)
{
//if (_con[parent] < _con[child])
if (comFunc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
void AdjustDown(int{
Compare comFunc;
size_t child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && comFunc(_con[parent], _con[child]))
{
++child;
}
if (comFunc(_con[parent], _con[child]))
{
swap(_con[parent], _con[child]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void push(const{
_con.push_back(x);
AdjustUp(_con.size() - 1);
}
void pop(){
assert(!_con.empty());
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
AdjustDown(0);
}
const T& top(){
return _con[0];
}
size_t size(){
return _con.size();
}
bool empty(){
return _con.empty();
}
private:
Container _con;
};
堆中元素的删除詳細分析過程
2.容器擴充卡
2.1 什麼是擴充卡
擴充卡是一種設計模式(設計模式是一套被反複使用的、多數人知曉的、經過分類編目的、代碼設計經驗的總結),該種模式是将一個類的接口轉換成客戶希望的另外一個接口。
2.2 STL标準庫中stack和queue的底層結構
雖然stack和queue中也可以存放元素,但在STL中并沒有将其劃分在容器的行列,而是将其稱為容器擴充卡,這是因為stack和隊列隻是對其他容器的接口進行了包裝,STL中stack和queue預設使用deque,比如:
3.deque
3.1deque的介紹
vector是單向開口的連續線型空間,deque則是一種雙向開口的連續線型空間,所謂雙向開口,意思是可以在頭尾兩端分别做元素的插入和删除操作。vector當然也可以在頭尾兩端進行操作。但是其頭部操作效率很差。
deque和vector的差異:
1:deque允許在常數時間内對頭部元素插入和删除操作。
2:deque沒有所謂容量(capacity)的概念,因為他是動态的以分段連續空間組合而成的,随時可以增加一段新的空間并連結起來,換句話說,像vector那樣“因舊空間不足而重新配置一塊更大的空間,然後複制元素,再釋放舊空間”,deque是不會讓這件事情發生的。是以,deque也沒有必要提供所謂的空間保留(reserve)功能。
deque的空間到底是怎麼樣的?
deque是由一段一段的定量連續空間組成,一旦要在deque的前端或者尾端增加新的空間,便需要配置一段定量連續的空間,串聯在整個deque的頭端或者尾端。deque的最大任務,便是在這些分段的定量連續空間上,維護其整體連續的假象,并提供随機存取的接口,避開vector中"重新配置,複制,釋放"的輪回,這樣做雖然友善插入删除,但是其代價是複雜的疊代器架構。
雙端隊列底層是一段假象的連續空間,實際是分段連續的,為了維護其“整體連續”以及随機通路的假象,落在了deque的疊代器身上,是以deque的疊代器設計就比較複雜,如下圖所示:
那deque是如何借助其疊代器維護其假想連續的結構呢?
3.2 deque的中控器
上圖我們看到有一個所謂的map,那麼這個map是什麼東西呢?
其實,deque采用一塊所謂的map (注意,不是STL的map容器)作為主要,這裡所謂的map是一塊連續的空間,其中每個元素都是指針,指向另一端(較大的)連續線性空間,稱為緩沖區。緩沖區才是deque的儲存空間主體。SGI版本的STL庫下允許我們指定緩沖區的大小,預設值是0表示将使用512bytes緩沖區。
實際deque類似于一個動态的二維數組
3.3 deque的疊代器
deque是分段連續的空間,維持其邏輯意義上的 ” 整體連續 “ 假象的任務落在了疊代器的 operator++ 和 operator-- 兩個運算子身上。
首先我們可以想到,deque的疊代器一定是非常複雜的?那麼我們不妨從deque的疊代器的需求入手,deque疊代器必須能夠指出分段連續空間(緩沖區)在哪裡,其次他必須能夠判斷自己是否已經處于其所在緩沖區的邊緣,如果是,一旦前進或者後退時就必須跳躍至下一個或者上一個緩沖區。為了能夠正确的跳躍,deque必須随時掌握管控中心(map)。
3.4 deque的缺陷
與vector比較,deque的優勢是:頭部插入和删除時,不需要搬移元素,效率特别高,而且在擴容時,也不需要搬移大量的元素,是以其效率是必vector高的。
與list比較,其底層是連續空間,空間使用率比較高,不需要存儲額外字段。
deque有一個緻命缺陷:不适合周遊。
因為在周遊時,deque的疊代器要頻繁的去檢測其是否移動到某段小空間的邊界,導緻效率低下,而序列式場景中,可能需要經常周遊,是以在實際中,需要線性結構時,大多數情況下優先考慮vector和list,deque的應用并不多,而目前能看到的一個應用就是,STL用其作為stack和queue的底層資料結構。
4.為什麼選擇deque作為stack和queue的底層預設容器
stack是一種後進先出的特殊線性資料結構,是以隻要具有push_back()和pop_back()操作的線性結構,都可以作為stack的底層容器,比如vector和list都可以;
queue是先進先出的特殊線性資料結構,隻要具有 push_back和pop_front操作的線性結構,都可以作為queue的底層容器,比如list。但是STL中對stack和queue預設選擇deque作為其底層容器,主要是因為:
1. stack和queue不需要周遊(是以stack和queue沒有疊代器),隻需要在固定的一端或者兩端進行操作。
2. 在stack中元素增長時,deque比vector的效率高(擴容時不需要搬移大量資料);queue中的元素增長時,deque不僅效率高,而且記憶體使用率高。
結合了deque的優點,而完美的避開了其缺陷。