1. 程式人生 > 實用技巧 >C++ 知識總結 P03:容器

C++ 知識總結 P03:容器

容器

容器可以分為三類:序列式容器、關聯容器、無序容器,此外還有一些容器介面卡。

序列容器

arrayvectorarray 與C中的陣列類似,是一種大小固定的儲存連續的容器;vector 也是儲存連續的,但它的長度可以動態調整。相對於陣列型別,這兩種容器更為安全。由於 arrayvector 都是連續儲存的容器,能夠高效地利用索引訪問元素,但在中間部分插入/刪除元素比較低效。

deque :一種雙端佇列,也是一種連續儲存的容器,能夠高效地在首尾部新增/刪除元素,但在中間部分插入/刪除元素比較低效。

listforward_listlist 是由雙向連結串列實現的,forward_list

是由單向連結串列實現的,因此這兩種容器能夠高效地插入/刪除元素,但很難進行隨機訪問。

關聯容器

mapmap 型別儲存鍵值對,通常是以二叉樹實現的,預設以 key 作為比較的依據。map 中的 key 不允許重複,而 multimapmap 的唯一的區別是元素可以重複。

setset 型別只儲存鍵值,其它特性與 map 相同,multisetset 的唯一區別是元素可以重複。

由於關聯容器的底層實現是二叉樹(紅黑樹),因此預設情況下關聯容器中的 keys 是有序的。如果使用關聯容器時對排序沒有需求,可以選擇下面的無序容器。

無序容器

關聯容器中的四種類型都有對應的無序版本:unordered_map

, unordered_multimap, unordered_set, unordered_multiset;無序容器通常是以雜湊表實現的。

容器介面卡

stack 預設的容器是 dequequeue 預設的容器是 dequepriority_queuequeue 功能類似,但是會按照壓入元素的值排序,彈出值最大的元素。預設使用的容器是 vector

如何選擇容器

根據《C++標準庫》中的描述,預設情況下應該選擇 vector,如果需要經常在首尾部插入或移除元素,應該選擇 deque,如果需要經常在中間部分插入或移除元素,應該選擇 list
如果需要經常查詢元素,應該選擇 unordered_set

,如果需要使用鍵值對,應該選擇 unordered_map,如果在上述兩種場景下,需要滿足有序,應該選擇 set / map

容器的操作

容器實現了大量的功能,以成員函式的形式提供這些功能,這裡不在贅述,可以檢視 C++ Reference / Container。但是僅僅閱讀文件可能無法弄清楚這些成員函式的具體表現,需要在實踐中多累積經驗。

下面是一些我認為靈活使用的方法。

pair 與 tuple

儘管 pair 提供了 first, second 來訪問元素,但是還可以使用 get<idx>() 函式來訪問 pair, tuple, 甚至 array 的值,並且該函式還可以作為左值使用 get<1>(p) = 10

pair, tuple 都提供來比較的能力,按照先比較第一個元素,相等時再比較第二個元素,……,以這樣的順序進行比較。

array

array.data() 會返回指向首元素的指標,這樣就可以像使用陣列一樣處理 array 的資料。如:

array<int, 10> a {1, 2, 3, 4, 5};
int* p = a.data();
for (int i = 0; i < 10; ++i) {
    cout << *(p+i) << endl;
}

vector也提供了這個功能。

vector 與 deque

vectordeque 非常相似,只有在雙端插入移除元素的需求時才會使用 deque。儘管 vector 也提供來訪問首個元素的成員函式 front,但是沒有像 deque 一樣提供插入移除首個元素的成員函式。

藉助 initlist 來實現初始化一個值的容器。

// 如果想初始化一個 大小為 n 的 vector / deque
vector<type> vec(n);
// 如果想初始化一個 第一個元素為 n 的 vector / deque
vector<type> vec({n});

list 與 forward_list

相比與前面的 vectordequelistforward_list 增加來很多不同的能力:排序 sort,逆序 reverse,去重 unique,合併 merge,移動元素 splice,去除元素 remove 等。

forward_list 沒有實現 size(),原因可能是由於這種容器的設計目的是快速的插入,每次插入操作都去維護一個儲存 size 的變數顯然會降低效率;而每次獲得 size 都遍歷一遍連結串列的實現也是低效的,因為人們容易濫用 size() 成員函式。索性就沒有實現,而且對於使用者而言也不難自己實現。

map 與 multimap

map 中的元素是有序的,在 map 模板中,除前兩個引數用來指定 key 與 value 的型別外,還可以提供第三個引數,用於指定排序時使用的比較方法。因此可以有兩種方式來改變預設的排序規則:1) 指定 map 模板中的排序引數;2) 修改所使用型別的比較函式。

預設的排序準則是 std::less<>,如果像降序排列,可以使用 std::greater<>,尖括號內為 key 的型別。或者想使用自定義的排序準則,可以自定義一個比較的函式:

struct StrLenCmp {
   bool operator() (const string& lhs, const string& rhs) const {
      return lhs.size() < rhs.size();
   }
};
map<string, int, StrLenCmp> m;

儘管使用這樣自定義的函式甚至可以實現以 value 值進行排序,但是 map 中只能保證 key 值唯一,不能保證 value 值唯一,因此使用 value 作為排序依據得到的排序結果無法保證。如果真的需要使用 value 排序,可以將 pair 放入一個 vector 中進行排序。

set 與 multiset

setmap 同理,也可以設定排序準則。

unordered 容器

unordered 容器可以在模板引數中指定一個 hash 函式,也可以在模板引數中指定一個用於比較相等的函式,對於小型程式用的的概率不是很大,使用預設的引數就好。

補充:bitset

bitset 用來儲存一串布林值,使用 vector<bool> 也可以。具體的用法:

bitset<4> flag("1100");
bitset<16> flags(0xffff);

這種工具提供了下標訪問,翻轉 flip,以及用於檢查全部位的 any all。

其它細節

  • 容器無法以 引用 作為元素,但是可以以 指標 作為元素。
  • 一般情況下,容器在不指定其它構造方法的情況下,都可以推斷容器會進行預設構造。但是對於不是通過物件實現的型別,如基本型別,是沒有預設構造的,都需要進行手動地初始化。
  • clear() 成員函式會將容器置為空容器,不是把所有元素設為預設值。

列表初始化

列表初始化為 C++ 提供來統一的初始化方式。不管是基本型別,還是容器,以及自定義的型別,都可以使用列表初始化。在自定義型別中的建構函式中使用初始化列表,那麼就可以使用列表初始化來進行,這兩個是對應的。