[CPP] STL 簡介

STL 即標準模板庫(Standard Template Library),是 C++ 標準庫的一部分,裏面包含了一些模板化的通用的數據結構和算法。STL 基於模版的實現,所以可以支持自定義的數據結構。html

STL 中一共有 6 大組件:node

  • 容器 (container)
  • 迭代器 (iterator)
  • 算法 (algorithm)
  • 分配器 (allocator)
  • 仿函數 (functor)
  • 容器適配器 (container adapter)

參考資料:c++

仿函數

仿函數 (Functor) 的本質就是在結構體中重載 () 運算符算法

例如:設計模式

struct Print
{ void operator()(const char *s) const { cout << s << endl; } };
int main()
{
    Print p;
    p("hello");
}

這一律念將會在 priority_queue 中使用(在智能指針的 unique_ptr 自定義 deleter 也會用到)。數組

容器

容器 (Container) 在 STL 中又分爲序列式容器 (Sequence Containers) ,關聯式容器 (Associative Containers) 和無序容器 (Unorderde Containers) .網絡

序列式容器

常見的序列式容器包括有:vector, string, array, deque, list, forward_list .數據結構

vector/stringless

底層實現:vector內存連續、自動擴容的數組,實質仍是定長數組。數據結構和算法

特色:

  • 隨機訪問:重載 [] 運算符
  • 動態擴容:插入新元素前,若是 size == capacity 時,那麼擴容爲當前容量的 2 倍,並拷貝原來的數據
  • 支持 ==, !=, <, <=, >, >= 比較運算
    • C++20 前,經過上述 6 個重載運算符實現;C++20中,統一「封裝」爲一個 <=> 運算符 (aka, three-way comparsion )。
    • 不難理解,時間複雜度爲 \(O(n)\)

PS:string 的底層實現與 vector 是相似的,一樣是內存連續、自動擴容的數組(但擴容策略不一樣)。


array (C++11)

底層實現:array內存連續的固定長度的數組,其本質是對原生數組的直接封裝。

特色(主要是與 vector 比較):

  • 支持 6 種比較運算符,支持 [] 隨機訪問
  • 丟棄自動擴容,以得到跟原生數組同樣的性能
  • 不支持 pop_front/back, erase, insert 這些操做。
  • 長度在編譯期肯定。vector 的初始化方式爲函數參數(如 vector<int> v(10, -1),長度可動態肯定),但 array 的長度須要在編譯期肯定,如 array<int, 10> a = {1, 2, 3} .

須要注意的是,arrayswap 方法複雜度是 \(\Theta(n)\) ,而其餘 STL 容器的 swap\(O(1)\),由於只須要交換一下指針。


deque

又稱爲「雙端隊列」。

底層實現:多個不連續的緩衝區,而緩衝區中的內存是連續的。而每一個緩衝區還會記錄首指針和尾指針,用來標記有效數據的區間。當一個緩衝區填滿以後便會在以前或者以後分配新的緩衝區來存儲更多的數據。

特色:

  • 支持 [] 隨機訪問
  • 線性複雜度的插入和刪除,以及常數複雜度的隨機訪問。

list

底層實現:雙向鏈表。

特色:

  • 不支持 [] 隨機訪問
  • 常數複雜度的插入和刪除

forwar_list (C++11)

底層實現:單向鏈表。

特色:

  • 相比 list 減小了空間開銷
  • 不支持 [] 隨機訪問
  • 不支持反向迭代 rbegin(), rend()

關聯式容器

關聯式容器包括:set/multisetmap/multimapmulti 表示鍵值可重複插入容器。

底層實現:紅黑樹。

特色:

  • 內部自排序,搜索、移除和插入擁有對數複雜度。
  • 對於任意關聯式容器,使用迭代器遍歷容器的時間複雜度均爲 \(O(n)\)

自定義比較方式:

  • 若是是自定義數據類型,重載運算符 <
  • 若是是 int 等內置類型,經過仿函數
struct cmp { bool operator()(int a, int b) { return a > b; } };
set<int, cmp> s;

無序容器

無序容器 (Unorderde Containers) 包括:unordered_set/unordered_multiset,unordered_map/unordered_multimap .

底層實現:哈希表。在標準庫實現裏,每一個元素的散列值是將值對一個質數取模獲得的,

特色:

  • 內部元素無序
  • 在最壞狀況下,對無序關聯式容器進行插入、刪除、查找等操做的時間複雜度會與容器大小成線性關係 。這一狀況每每在容器內出現大量哈希衝突時產生。

在實際應用場景下,假設咱們已知鍵值的具體分佈狀況,爲了不大量的哈希衝突,咱們能夠自定義哈希函數(仍是經過仿函數的形式)。

struct my_hash { size_t operator()(int x) const { return x; } };
unordered_map<int, int, my_hash> my_map;
unordered_map<pair<int, int>, int, my_hash> my_pair_map;

小結

四種操做的平均時間複雜度比較:

  • 增:在指定位置插入元素
  • 刪:刪除指定位置的元素
  • 改:修改指定位置的元素
  • 查:查找某一元素
Containers 底層結構
vector/deque vector: 動態連續內存
deque: 連續內存+鏈表
\(O(n)\) \(O(n)\) \(O(1)\) \(O(n)\)
list 雙向鏈表 \(O(1)\) \(O(1)\) \(O(1)\) \(O(n)\)
forward_list 單向鏈表 \(O(1)\) \(O(n)\) \(O(1)\) \(O(n)\)
array 連續內存 不支持 不支持 \(O(1)\) \(O(n)\)
set/map/multiset/multimap 紅黑樹 \(O(\log{n})\) \(O(\log{n})\) \(O(\log{n})\) \(O(\log{n})\)
unordered_{set,multiset}
unordered_{map,multimap}
哈希表 \(O(1)\) \(O(1)\) \(O(1)\) \(O(1)\)

容器適配器

容器適配器 (Container Adapter) 其實並非容器(我的理解是對容器的一種封裝),它們不具備容器的某些特色(如:有迭代器、有 clear() 函數……)。

常見的適配器:stackqueuepriority_queue

對於適配器而言,能夠指定某一容器做爲其底層的數據結構。

stack

  • 默認容器:deque
  • 不支持隨機訪問,不支持迭代器
  • top, pop, push, size, empty 操做的時間複雜度均爲 \(O(1)\)

指定容器做爲底層數據結構:

stack<TypeName, Container> s;  // 使用 Container 做爲底層容器

queue

  • 默認容器:deque
  • 不支持隨機訪問,不支持迭代器
  • front, push, pop, size, empty 操做的時間複雜度均爲 \(O(1)\)

指定容器:

queue<int, vector<int>> q;

priority_queue

又稱爲 「優先隊列」 。

  • 默認容器:vector
  • \(O(1)\)top, empty, size
  • \(O(\log{n})\) : push, pop

模版參數解析:

priority_queue<T, Container = vector<T>, Compare = less<T>> q;
// 經過 Container 指定底層容器,默認爲 vector
// 經過 Compare 自定義比較函數,默認爲 less,元素優先級大的在堆頂,即大頂堆
priority_queue<int, vector<int>, greater<int>> q;
// 傳入 greater<int> 那麼將構造一個小頂堆
// 相似的,還有 greater_equal, less_equal

迭代器

迭代器 (Iterator) 實際上也是 GOF 中的一種設計模式:提供一種方法順序訪問一個聚合對象中各個元素,而又不需暴露該對象的內部表示。

迭代器的分類以下圖所示。

各容器的迭代器

STL 中各容器/適配器對應使用的迭代器以下表所示。

Container Iterator
array 隨機訪問迭代器
vector 隨機訪問迭代器
deque 隨機訪問迭代器
list 雙向迭代器
set / multiset 雙向迭代器
map / multimap 雙向迭代器
forward_list 前向迭代器
unordered_{set, multiset} 前向迭代器
unordered_{map, multimap} 前向迭代器
stack 不支持迭代器
queue 不支持迭代器
priority_queue 不支持迭代器

迭代器失效

迭代器失效是由於向容器插入或者刪除元素致使容器的空間變化或者說是次序發生了變化,使得原迭代器變得不可用。所以在對 STL 迭代器進行增刪操做時,要格外注意迭代器是否失效。

網絡上搜索「迭代器失效」,會發現不少這樣的例子,在一個 vector 中去除全部的 2 和 3,故意用一下迭代器掃描(你們都知道能夠用下標):

int main()
{
    vector<int> v = {2, 3, 4, 6, 7, 8, 9, 3, 2, 2, 2, 2, 3, 3, 3, 4, 5, 6};
    for (auto i = v.begin(); i != v.end(); i++)
    {
        if (*i==2 || *i==3) v.erase(i), i--;
        // correct code should be
        // if (*i==2 || *i==3) i=v.erase(i), i--;
    }
    for (auto i = v.begin(); i != v.end(); i++)
        cout << *i << ' ';
}

我好久以前用 Dev C++ (應該是內置了很古老的 MinGW)寫代碼的時候,印象中也遇到過這種狀況,v.erase(i), i-- 這樣的操做是有問題的。 erase(i) 會使得 i 及其後面的迭代器失效,從而發生段錯誤。

但如今 MacOS (clang++ 12), Ubuntu16 (g++ 5.4), Windows (mingw 9.2) 上測試,這段代碼都沒有問題,而且能輸出正確結果。編譯選項爲:

g++ test.cpp -std=c++11 -O0

實際上也不難理解,由於是連續內存,i 指向的內存位置,在 erase 以後被其餘數據覆蓋了(這裏的行爲就跟咱們使用普通數組同樣),但該位置仍然在 vector 的有效範圍以內。在上述代碼中,當 i = v.begin() 時,執行 erase 後,對 i 進行自減操做,這已是一種未定義行爲。

我猜應該是 C++11 後(或者是後來的編譯器更新),對迭代器失效的這個問題進行了優化。

雖然可以正常運行,但我認爲最好仍是嚴謹一些,更嚴格地遵循迭代器的使用規則:if (*i==2 || *i==3) i=v.erase(i), i--; .

如下爲各種容器可能會發生迭代器失效的狀況:

  • 數組型 (vector, deque)
    • insert(i)erase(i) 會發生數據挪動,使得 i 後的迭代器失效,建議使用 i = erase(i) 獲取下一個有效迭代器。
    • 內存從新分配:當 vector 自動擴容時,可能會申請一塊新的內存並拷貝原數據(也有多是在當前內存的基礎上,再擴充一段連續內存),所以全部的迭代器都將失效。
  • 鏈表型 (list, forward_list):insert(i)erase(i) 操做不影響其餘位置的迭代器,erase(i) 使得迭代器 i 失效,指向數據無效,i = erase(i) 可得到下一個有效迭代器,或者使用 erase(i++) 也可(在進入 erase 操做前已完成自增)。
  • 樹型 (set/map):與鏈表型相同。
  • 哈希型 (unodered_{set_map}):與鏈表型相同。
相關文章
相關標籤/搜索