C++容器詳解
什麼是容器
首先,我們必須理解一下什麼是容器,在C++ 中容器被定義為:在資料儲存上,有一種物件型別,它可以持有其它物件或指向其它對像的指標,這種物件型別就叫做容器。很簡單,容器就是儲存其它物件的對 象,當然這是一個樸素的理解,這種“物件”還包含了一系列處理“其它物件”的方法,因為這些方法在程式的設計上會經常被用到,所以容器也體現了一個好處, 就是“容器類是一種對特定程式碼重用問題的良好的解決方案”。
容器還有另一個特點是容器可以自行擴充套件。在解決問題時我們常常不知道我們需要儲存多少個物件,也就是說我們不知道應該建立多大的記憶體空間來儲存我們的物件。 顯然,陣列在這一方面也力不從心。容器的優勢就在這裡,它不需要你預先告訴它你要儲存多少物件,只要你建立一個容器物件,併合理的呼叫它所提供的方法,所 有的處理細節將由容器來自身完成。它可以為你申請記憶體或釋放記憶體,並且用最優的演算法來執行您的命令。
容器是隨著面嚮物件語言的誕生而提出的,容器類在面嚮物件語言中特別重要,甚至它被認為是早期面嚮物件語言的基礎。在現在幾乎所有的面向物件的語言中也都伴隨著一個容器集,在C++ 中,就是標準模板庫(STL )。
和其它語言不一樣,C++ 中處理容器是採用基於模板的方式。標準C++ 庫中的容器提供了多種資料結構,這些資料結構可以與標準演算法一起很好的工作,這為我們的軟體開發提供了良好的支援!
通用容器的分類
STL 對定義的通用容器分三類:順序性容器、關聯式容器和容器介面卡。
順序性容器 是 一種各元素之間有順序關係的線性表,是一種線性結構的可序群集。順序性容器中的每個元素均有固定的位置,除非用刪除或插入的操作改變這個位置。這個位置和 元素本身無關,而和操作的時間和地點有關,順序性容器不會根據元素的特點排序而是直接儲存了元素操作時的邏輯順序。比如我們一次性對一個順序性容器追加三 個元素,這三個元素在容器中的相對位置和追加時的邏輯次序是一致的。
關聯式容器 和 順序性容器不一樣,關聯式容器是非線性的樹結構,更準確的說是二叉樹結構。各元素之間沒有嚴格的物理上的順序關係,也就是說元素在容器中並沒有儲存元素置 入容器時的邏輯順序。但是關聯式容器提供了另一種根據元素特點排序的功能,這樣迭代器就能根據元素的特點“順序地”獲取元素。
關聯式容器另一個顯著的特點是它是以鍵值的方式來儲存資料,就是說它能把關鍵字和值關聯起來儲存,而順序性容器只能儲存一種(可以認為它只儲存關鍵字,也可以認為它只儲存值)。這在下面具體的容器類中可以說明這一點。
容器介面卡 是一個比較抽象的概念, C++的 解釋是:介面卡是使一事物的行為類似於另一事物的行為的一種機制。容器介面卡是讓一種已存在的容器型別採用另一種不同的抽象型別的工作方式來實現的一種機 制。其實僅是發生了介面轉換。那麼你可以把它理解為容器的容器,它實質還是一個容器,只是他不依賴於具體的標準容器型別,可以理解是容器的模版。或者把它 理解為容器的介面,而介面卡具體採用哪種容器型別去實現,在定義介面卡的時候可以由你決定。
下表列出STL 定義的三類容器所包含的具體容器類:
標準容器類 |
特點 |
順序性容器 |
|
vector |
從後面快速的插入與刪除,直接訪問任何元素 |
deque |
從前面或後面快速的插入與刪除,直接訪問任何元素 |
list |
雙鏈表,從任何地方快速插入與刪除 |
關聯容器 |
|
set |
快速查詢,不允許重複值 |
multiset |
快速查詢,允許重複值 |
map |
一對多對映,基於關鍵字快速查詢,不允許重複值 |
multimap |
一對多對映,基於關鍵字快速查詢,允許重複值 |
容器介面卡 |
|
stack |
後進先出 |
queue |
先進先出 |
priority_queue |
最高優先順序元素總是第一個出列 |
vector ,deque 和 list
順序性容器:
向量 vector :
是一個線性順序結構。相當於陣列,但其大小可以不預先指定,並且自動擴充套件。它可以像陣列一樣被操作,由於它的特性我們完全可以將vector 看作動態陣列。
在建立一個vector 後,它會自動在記憶體中分配一塊連續的記憶體空間進行資料儲存,初始的空間大小可以預先指定也可以由vector 預設指定,這個大小即capacity ()函式的返回值。當儲存的資料超過分配的空間時vector 會重新分配一塊記憶體塊,但這樣的分配是很耗時的,在重新分配空間時它會做這樣的動作:
首先,vector 會申請一塊更大的記憶體塊;
然後,將原來的資料拷貝到新的記憶體塊中;
其次,銷燬掉原記憶體塊中的物件(呼叫物件的解構函式);
最後,將原來的記憶體空間釋放掉。
如果vector 儲存的資料量很大時,這樣的操作一定會導致糟糕的效能(這也是vector 被設計成比較容易拷貝的值型別的原因)。所以說vector 不是在什麼情況下效能都好,只有在預先知道它大小的情況下vector 的效能才是最優的。
vector 的特點:
(1) 指定一塊如同陣列一樣的連續儲存,但空間可以動態擴充套件。即它可以像陣列一樣操作,並且可以進行動態操作。通常體現在push_back() pop_back() 。
(2) 隨機訪問方便,它像陣列一樣被訪問,即支援[ ] 操作符和vector.at()
(3) 節省空間,因為它是連續儲存,在儲存資料的區域都是沒有被浪費的,但是要明確一點vector 大多情況下並不是滿存的,在未儲存的區域實際是浪費的。
(4) 在內部進行插入、刪除操作效率非常低,這樣的操作基本上是被禁止的。Vector 被設計成只能在後端進行追加和刪除操作,其原因是vector 內部的實現是按照順序表的原理。
(5) 只能在vector 的最後進行push 和pop ,不能在vector 的頭進行push 和pop 。
(6) 當動態新增的資料超過vector 預設分配的大小時要進行記憶體的重新分配、拷貝與釋放,這個操作非常消耗效能。 所以要vector 達到最優的效能,最好在建立vector 時就指定其空間大小。
雙向連結串列list
是一個線性連結串列結構,它的資料由若干個節點構成,每一個節點都包括一個資訊塊(即實際儲存的資料)、一個前驅指標和一個後驅指標。它無需分配指定的記憶體大小且可以任意伸縮,這是因為它儲存在非連續的記憶體空間中,並且由指標將有序的元素連結起來。
由於其結構的原因,list 隨機檢索的效能非常的不好,因為它不像vector 那樣直接找到元素的地址,而是要從頭一個一個的順序查詢,這樣目標元素越靠後,它的檢索時間就越長。檢索時間與目標元素的位置成正比。
雖然隨機檢索的速度不夠快,但是它可以迅速地在任何節點進行插入和刪除操作。因為list 的每個節點儲存著它在連結串列中的位置,插入或刪除一個元素僅對最多三個元素有所影響,不像vector 會對操作點之後的所有元素的儲存地址都有所影響,這一點是vector 不可比擬的。
list 的特點:
(1) 不使用連續的記憶體空間這樣可以隨意地進行動態操作;
(2) 可以在內部任何位置快速地插入或刪除,當然也可以在兩端進行push 和pop 。
(3) 不能進行內部的隨機訪問,即不支援[ ] 操作符和vector.at() ;
(4) 相對於verctor 佔用更多的記憶體。
雙端佇列deque
是一種優化了的、對序列兩端元素進行新增和刪除操作的基本序列容器。它允許較為快速地隨機訪問,但它不像vector 把所有的物件儲存在一塊連續的記憶體塊,而是採用多個連續的儲存塊,並且在一個對映結構中儲存對這些塊及其順序的跟蹤。向deque 兩端新增或刪除元素的開銷很小。它不需要重新分配空間,所以向末端增加元素比vector 更有效。
實際上,deque 是對vector 和list 優缺點的結合,它是處於兩者之間的一種容器。
deque 的特點:
(1) 隨機訪問方便,即支援[ ] 操作符和vector.at() ,但效能沒有vector 好;
(2) 可以在內部進行插入和刪除操作,但效能不及list ;
(3) 可以在兩端進行push 、pop ;
三者的比較
下圖描述了vector 、list 、deque 在記憶體結構上的特點:
vector 是一段連續的記憶體塊,而deque 是多個連續的記憶體塊, list 是所有資料元素分開儲存,可以是任何兩個元素沒有連續。
vector 的查詢效能最好,並且在末端增加資料也很好,除非它重新申請記憶體段;適合高效地隨機儲存。
list 是一個連結串列,任何一個元素都可以是不連續的,但它都有兩個指向上一元素和下一元素的指標。所以它對插入、刪除元素效能是最好的,而查詢效能非常差;適合 大量地插入和刪除操作而不關心隨機存取的需求。
deque 是介於兩者之間,它兼顧了陣列和連結串列的優點,它是分塊的連結串列和多個數組的聯合。所以它有被list 好的查詢效能,有被vector 好的插入、刪除效能。 如果你需要隨即存取又關心兩端資料的插入和刪除,那麼deque 是最佳之選。
關聯容器
set, multiset, map, multimap 是一種非線性的樹結構,具體的說採用的是一種比較高效的特殊的平衡檢索二叉樹—— 紅黑樹結構。(至於什麼是紅黑樹,我也不太理解,只能理解到它是一種二叉樹結構)
因為關聯容器的這四種容器類都使用同一原理,所以他們核心的演算法是一致的,但是它們在應用上又有一些差別,先描述一下它們之間的差別。
set ,又稱集合,實際上就是一組元素的集合,但其中所包含的元素的值是唯一的,且是按一定順序排列的,集合中的每個元素被稱作集合中的例項。因為其內部是通過連結串列的方式來組織,所以在插入的時候比vector 快,但在查詢和末尾新增上被vector 慢。
multiset ,是多重集合,其實現方式和set 是相似的,只是它不要求集合中的元素是唯一的,也就是說集合中的同一個元素可以出現多次。
map ,提供一種“鍵- 值”關係的一對一的資料儲存能力。其“鍵”在容器中不可重複,且按一定順序排列(其實我們可以將set 也看成是一種鍵- 值關係的儲存,只是它只有鍵沒有值。它是map 的一種特殊形式)。由於其是按連結串列的方式儲存,它也繼承了連結串列的優缺點。
multimap , 和map 的原理基本相似,它允許“鍵”在容器中可以不唯一。
關聯容器的特點是明顯的,相對於順序容器,有以下幾個主要特點:
1, 其內部實現是採用非線性的二叉樹結構,具體的說是紅黑樹的結構原理實現的;
2, set 和map 保證了元素的唯一性,mulset 和mulmap 擴充套件了這一屬性,可以允許元素不唯一;
3, 元素是有序的集合,預設在插入的時候按升序排列。
基於以上特點,
1, 關聯容器對元素的插入和刪除操作比vector 要快,因為vector 是順序儲存,而關聯容器是鏈式儲存;比list 要慢,是因為即使它們同是鏈式結構,但list 是線性的,而關聯容器是二叉樹結構,其改變一個元素涉及到其它元素的變動比list 要多,並且它是排序的,每次插入和刪除都需要對元素重新排序;
2, 關聯容器對元素的檢索操作比vector 慢,但是比list 要快很多。vector 是順序的連續儲存,當然是比不上的,但相對鏈式的list 要快很多是因為list 是逐個搜尋,它搜尋的時間是跟容器的大小成正比,而關聯容器 查詢的複雜度基本是Log(N) ,比如如果有1000 個記錄,最多查詢10 次,1,000,000 個記錄,最多查詢20 次。容器越大,關聯容器相對list 的優越性就越能體現;
3, 在使用上set 區別於vector,deque,list 的最大特點就是set 是內部排序的,這在查詢上雖然遜色於vector ,但是卻大大的強於list 。
4, 在使用上map 的功能是不可取代的,它儲存了“鍵- 值”關係的資料,而這種鍵值關係採用了類陣列的方式。陣列是用數字型別的下標來索引元素的位置,而map 是用字元型關鍵字來索引元素的位置。在使用上map 也提供了一種類陣列操作的方式,即它可以通過下標來檢索資料,這是其他容器做不到的,當然也包括set 。(STL 中只有vector 和map 可以通過類陣列的方式操作元素,即如同ele[1] 方式)
容器介面卡
STL 中包含三種介面卡:棧stack 、佇列queue 和優先順序priority_queue 。
介面卡是容器的介面,它本身不能直接儲存元素,它儲存元素的機制是呼叫另一種順序容器去實現,即可以把介面卡看作“它儲存一個容器,這個容器再儲存所有元素”。
STL 中提供的三種介面卡可以由某一種順序容器去實現。預設下stack 和queue 基於deque 容器實現,priority_queue 則基於vector 容器實現。當然在建立一個介面卡時也可以指定具體的實現容器,建立介面卡時在第二個引數上指定具體的順序容器可以覆蓋介面卡的預設實現。
由於介面卡的特點,一個介面卡不是可以由任一個順序容器都可以實現的。
棧stack 的特點是後進先出,所以它關聯的基本容器可以是任意一種順序容器,因為這些容器型別結構都可以提供棧的操作有求,它們都提供了push_back 、pop_back 和back 操作;
佇列queue 的特點是先進先出,介面卡要求其關聯的基礎容器必須提供pop_front 操作,因此其不能建立在vector 容器上;
優先順序佇列priority_queue 介面卡要求提供隨機訪問功能,因此不能建立在list 容器上。