天天看點

[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

1.priority_queue

1.1 priority_queue的介紹

​​priority_queue 官方文檔介紹​​

[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

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;
}      
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

 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;
}      
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

 模拟實作:

我們剛才已經明确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;
  };      
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
​​堆中元素的删除詳細分析過程​​
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

2.容器擴充卡

2.1 什麼是擴充卡

擴充卡是一種設計模式(設計模式是一套被反複使用的、多數人知曉的、經過分類編目的、代碼設計經驗的總結),該種模式是将一個類的接口轉換成客戶希望的另外一個接口。

2.2 STL标準庫中stack和queue的底層結構

雖然stack和queue中也可以存放元素,但在STL中并沒有将其劃分在容器的行列,而是将其稱為容器擴充卡,這是因為stack和隊列隻是對其他容器的接口進行了包裝,STL中stack和queue預設使用deque,比如:

[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

3.deque 

3.1deque的介紹

vector是單向開口的連續線型空間,deque則是一種雙向開口的連續線型空間,所謂雙向開口,意思是可以在頭尾兩端分别做元素的插入和删除操作。vector當然也可以在頭尾兩端進行操作。但是其頭部操作效率很差。

[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

deque和vector的差異:

1:deque允許在常數時間内對頭部元素插入和删除操作。

2:deque沒有所謂容量(capacity)的概念,因為他是動态的以分段連續空間組合而成的,随時可以增加一段新的空間并連結起來,換句話說,像vector那樣“因舊空間不足而重新配置一塊更大的空間,然後複制元素,再釋放舊空間”,deque是不會讓這件事情發生的。是以,deque也沒有必要提供所謂的空間保留(reserve)功能。

deque的空間到底是怎麼樣的?

deque是由一段一段的定量連續空間組成,一旦要在deque的前端或者尾端增加新的空間,便需要配置一段定量連續的空間,串聯在整個deque的頭端或者尾端。deque的最大任務,便是在這些分段的定量連續空間上,維護其整體連續的假象,并提供随機存取的接口,避開vector中"重新配置,複制,釋放"的輪回,這樣做雖然友善插入删除,但是其代價是複雜的疊代器架構。

[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

雙端隊列底層是一段假象的連續空間,實際是分段連續的,為了維護其“整體連續”以及随機通路的假象,落在了deque的疊代器身上,是以deque的疊代器設計就比較複雜,如下圖所示:

[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

那deque是如何借助其疊代器維護其假想連續的結構呢?

[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,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)。

[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解
[ C++ ] STL priority_queue(優先級隊列)使用及其底層模拟實作,容器擴充卡,deque(雙端隊列)原理了解

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的優點,而完美的避開了其缺陷。