更新日期:2019-09-11
概述
在使用容器之前,您應該至少了解 C++
的基礎知識,以及數據結構的基礎知識(文內就不描述數據結構的內容了)。文內默認 C++
版本為 C++ 17
。
我們來看一下什么叫容器
容器:
儲存一系列元素的對象。每一個容器都管理其元素的存儲空間并允許開發者通過迭代器和成員函數訪問其內部元素。C++
的 STL 容器 通常用于實現我們在程序中經常使用到的數據結構,例如
- 棧
- 隊列/雙端隊列
- 鏈表
- 樹
- 關聯集合
- 動態數組
在 C++
與 Java
等靜態類型語言中,被儲存的對象必須是同一類型。
分類:
-
序列容器:一種各元素之間有順序關系的線性表,是一種線性結構的可序群集。
- 順序性容器中的每個元素均有固定的位置,除非用刪除或插入的操作改變這個位置。
- 順序容器的元素排列次序與元素值無關,而是由元素添加到容器里的次序決定
序列容器適配器
-
關聯容器
- 關聯式容器是非線性的樹結構,更準確的說是二叉樹結構。
- 各元素之間沒有嚴格的物理上的順序關系,也就是說元素在容器中并沒有保存元素置入容器時的邏輯順序。
- 但是關聯式容器提供了另一種根據元素特點排序的功能,這樣迭代器就能根據元素的特點“順序地”獲取元素。
- 元素是有序的集合,默認在插入的時候按升序排列(
set
,multiset
,map
,multimap
)!
無序關聯容器
序列容器
序列容器用作以線性方式存儲同一類型對象的數據結構。
Vector
我們常常稱 vector
是動態數組,基于數組實現。其在內存中是一段連續的區域。何謂動態?動態就是說 vector
有自動的內存管理功能。可以動態的改變vector的長度,并隨著元素的增加與減小來自動改變數組大小,它提供了直接添加尾部元素或者刪除元素的方法!所以它的時間是固定的!然而,他要在頭部與中間插入或者刪除元素是線性的時間復雜度!
特點:他可以反轉序列,所以它可以反向遍歷可反轉序列!(基于他的rbegin,rend)
定義與初始化:
要調用頭文件
#include<vector>
定義與初始化:
vector<int> v;//默認初始化
vector<int> v(v1);//用v1初始化v
vector<int> v(v1.begin(),v1.end());//用v1初始化v
vector<int> v(10);//定義一個大小為10的數組!
vector<int> v(10,1)//定義個全為1而且長度為10的數組
方法:
a.front(),a.rbegin() //首元素
a.back(),a.rend() //末尾元素
v.push_back() //增
v.insert() //由于insert重載方法比較多
// 1.v.insert(p,t)//將t插到p的前面
// 2.v.insert(p,n,t)//將n個t插入p之前
// 3.v.insert(p,i.j)//將區間[i,j)的元素插入到p之前
v.pop_back() //刪
v.erase(t,k)
// 1.v.erase(t,k)//刪除他們之間的元素
// 2.v.erase(p)//刪除p指向的元素
v.chear===v.erase(begin(),end());
遍歷
//下標法
for(int i = 0; i < v.size(); i++) {
std::cout << v[i];
}
//迭代器法
for(vector<int>::const_iterator iterator = v.begin();
iterator != v.end();
iterator++) {
std::cout << *iterator;
}
// 上述代碼在現代 C++ 中可以使用 auto 關鍵字
//迭代器法
for(auto iterator = v.begin();
iterator != v.end();
iterator++) {
std::cout << *iterator;
}
// 甚至,可以使用范圍循環
for(auto it : v) {
std::cout << *it;
}
Array
數組 Array
是靜態連續數組。他的長度固定,使用沒有調節大小的操作,但是他有一些有意義的成員函數,如operator[] 和at(),當然有很多STL算法用于array,如copy(),for_each()。我們后面會詳細介紹一些C++知識點!
/**
* 導入頭文件
*/
#include <array>
deque
雙端隊列,他的實現類\似與vector,支持隨機訪問,但是它訪問首元素的插入(push_front())與刪除(pop_front())的時間是固定的!而且他的執行速度要比vector快很多!所以題目中有大量的操作發生在序列的起始位置與結尾處,我們就要考慮用deque!
調用頭文件:
#include<deque>
初始化與定義已經在序列要求里面,而且方法與vector類似,只是多了push_front()
, pop_front()
,我們就不做過多的闡述
list
雙向鏈表,list在鏈表中的任意一個位置插入與刪除一個元素時間是固定的!但是他不能隨機訪問,優點是元素的快速插入與刪除!從容器中插入與刪除元素之后i,迭代器指向元素將不變,不會移動已有元素,只是修改鏈表信息。
#include<list>
我們來看一下他的鏈表獨有成員函數!
void sort() //使用<運算符對鏈表進行排序,時間復雜度O(NlogN)
void merge(list<T,Alloc>&x) //將x與調用鏈表合并,要求:兩個鏈表必須要已經排好序!元素將保存在調用鏈表中,x為空,這個時間復雜度為線性!
void remove(const T &val)//刪除val的所有實例
void splice(iterator pos,list<T,Alloc>x)//將鏈表x的內容加到pos的前面,時間復雜度為固定時間
void unique() //去重,線性時間
forword_list
參加noip的同學注意了!他是C++11新添加的容器類!它主要實現了單向鏈表,只需要正向迭代器,他是不可逆轉容器,他功能比較少,但是它比較簡單!
序列容器適配器
stack
適配器,它可以將任意類型的序列容器轉換為一個堆棧,一般使用deque作為支持的序列容器。元素只能后進先出(LIFO)。不能遍歷整個stack。他給vector提供了棧接口!
與queue類似,如果要使用棧中元素,先用top檢索,然后用pop將他從棧中刪除,這個太過于常用不介紹方法!
queuue
他是一個配適器類,ostream_iterator就是一個配適器,讓輸出流能夠使用迭代器接口,同樣它實現了隊列接口!它不僅不允許隨機訪問元素,而且還不能遍歷隊列!元素只能先進先出(FIFO).
方法:
bool empty()//判斷是否為空
front()//隊首元素的訪問
back()//隊尾元素的訪問
push(x)//隊尾插入x
pop()//刪除隊首元素
priority_queue
另一個配適器,他與queue基本一樣,但是他的最大元素被移動到隊首(生活不總是公平對,隊列也一樣),內部區別在于底層結構不一樣,他用的是vector,當然我們可以修改確定拿個元素放在隊首的比較方式!
priority_queue<int> X //大根堆,默認初始化
priority_queue<int, vector<int>, greater<int>> x //小根堆,運用了預定義函數greater<int>!
以下內容摘自C++API:
包含priority_queue 的頭文件是 <queue>
priority_queue類的主要成員:
priority_queue(); //默認構造函數,生成一個空的排序隊列
priority_queue(const queue&); //拷貝構造函數
priority_queue& operator=(const priority_queue &); //賦值運算符重載
priority_queue 的私有成員:
value_type; //priority_queue中存放的對象類型,它和priority_queue中的T類型相同
priority_queue(const Compare& comp); //構造生成一個空的priority_queue對象,使用comp作為priority_queue的comparison
priority_queue(const value_type* first, const value_type* last); //帶有兩個參數的構造 函數,使用默認的Comparison作為第三個參數
size_type; //正整數類型,和Sequence::size_type類型一樣。
bool empty() const; //判斷優先級隊列是否為空,為空返回true,否則返回false
size_type size() const; //返回優先級隊列中的元素個數
const value_type& top() const(); //返回優先級隊列中第一個元素的參考值。
void push(const value_type& x); //把元素x插入到優先級隊列的尾部,隊列的長度加1
void pop(); //刪除優先級隊列的第一個值,前提是隊列非空,刪除后隊列長度減1
關聯容器
它運用了鍵值對(value-key),與java類似的map,例如hashmap,有點在于他提供了利用key快速訪問功能,它的底層結構應該是一種樹來實現的,所以他才有如此快的查找速度,最簡單的set,他的鍵值對類型是一致的,而且唯一,元素默認按升序排列。map他的鍵值對類型不同,鍵是唯一的,元素默認按鍵的升序排列。!而muilti_sset/map 鍵可以不唯一。
迭代器在關聯容器中對操作:
m.lower_bound(k)//返回一個迭代器,指向鍵不小于 k 的第一個元素
m.upper_bound(k)//返回一個迭代器,指向鍵大于 k 的第一個元素
m.equal_range(k)//返回一個迭代器的 pair 對象。它的 first 成員等價于m.lower_bound(k) //。而 second 成員則等價于 m.upper_bound(k)
map
map 是鍵-值對的集合。map 類型通常可理解為關聯數組:可使用鍵作為下標來獲取一個值,正如內置數組類型一樣。而關聯的本質在于元素的值與某個特定的鍵相關聯,而并非通過元素在數組中的位置來獲取。
定義與初始化
map<int,string> map1; //默認為空
m.insert()
// 1.m.insert(e)//e是一個用在m上的value_kry 類型的值。如果鍵(e.first不在m中,則插入一個值為e.second 的新元素;如果該鍵在m中已存在,則保持m不變。該函數返回一個pair類型對象,包含指向鍵為e.first的元素的map迭代器,以及一個 bool 類型的對象,表示是否插入了該元素
// 2.m.insert(begin,end)//begin和end是標記元素范圍的迭代器,其中的元素必須為m.value_key 類型的鍵-值對。對于該范圍內的所有元素,如果它的鍵在 m 中不存在,則將該鍵及其關聯的值插入到 m。返回 void 類型
// 3.m.insert(iter,e)//e是一個用在m上的 value_key 類型的值。如果鍵(e.first)不在m中,則創建新元素,并以迭代器iter為起點搜索新元素存儲的位置。返回一個迭代器,指向m中具有給定鍵的元素
m.count(k) //返回m中k的出現次數
m.find() //如果m容器中存在按k索引的元素,則返回指向該元素的迭代器。如果不存在,則返回超出末端迭代器.
m.erase() //具體與序列該方法一致!
set
支持插入,刪除,查找等操作,就像一個集合一樣。所有的操作的都是嚴格在logn時間之內完成,效率非常高。set和multiset的區別是:set插入的元素不能相同,但是multiset可以相同。Set默認自動排序。使用方法類似list。
set容器的定義和使用
set 容器的每個鍵都只能對應一個元素。以一段范圍的元素初始化set對象,或在set對象中插入一組元素時,對于每個鍵,事實上都只添加了一個元素。
vector<int> ivec;
for(vector<int>::size_type i = 0; i != 10; ++i) {
ivec.push_back(i);
ivec.push_back(i);
}
set<int> iset(ivec.begin(), ivec.end());
cout << ivec.size() << endl;//20個
cout << iset.size() << endl;// 10個
添加
set<string> set1;
set1.insert("the"); //第一種方法:直接添加
set<int> iset2;
iset2.insert(ivec.begin(), ivec.end());//第二中方法:通過指針迭代器
獲取:
set<int> iset;
for(int i = 0; i<10; i++)
iset.insert(i);
iset.find(1)// 返回指向元素內容為1的指針
iset.find(11)// 返回指針iset.end()
iset.count(1)// 存在,返回1
iset.count(11)// 不存在,返回0
由于其他兩個不常用我們不做過多介紹!有興趣的童鞋可以去CPPAPI或者 CPP底層源碼參考學習!
無序關聯容器
- unordered_set
- unordered_multiset
- unordered_map
- unordered_multimap
底層結構基于哈希表,主要與提高添加與刪除元素得速度與提高查找算法得效率!無序關聯容器(unordered_set、unordered_multiset、unordered_map和 unordered_multimap)使用鍵和哈希表,以便能夠快速存取數據。
下面簡要地介紹這些概念。哈希函數(hash function)將鍵轉換為索引值。例如,如果鍵為string對象,哈希函數可能將其中每個字符的數字編碼相加,再計算結果除以13的余數,從而得到 一個0~12的索引。
而無序容器將使用13個桶(bucket)來存儲string,所有索引為4的string都將存儲在第4個桶中。如果您要在容器中搜索鍵,將對鍵執行哈希函數,進而只在索引對應的桶中搜索。理想情況下,應有足夠多的桶,每個桶只包含為數不多的string。
C++11庫提供了模板hash<key>
,無序關聯容器默認使用該模板。為各種整型、浮點型、指針以及一些模板類(如string)定義了該模板的具體化。
X(n, hf, eq)//創建一個至少包含n個桶的空容器,并將hf用作哈希函數,將eq用作鍵值相等謂詞。如果省略了eq,則將ke
y_equal( )用作鍵值相等謂詞;如果也省略了hf,則將hasher( )用作哈希函數
X a(n, hf, eq)//創建一個名為a的空容器,它至少包含n個桶,并將hf用作哈希函數,將eq用作鍵值相等謂詞。如果省略eq,則將key_equal( )用作鍵值相等謂詞;如果也省略了hf,則將hasher( )用作哈希函數
X(i, j, n, hf, eq)//創建一個至少包含n個桶的空容器,將hf用作哈希函數,將eq用作鍵值相等謂詞,并插入區間[i, j]中的元素。如果省略了eq,將key_equal( )用作鍵值相等謂詞;如果省略了hf,將hasher( )用作哈希函數;如果省略了n,則包含桶數不確定
X a(i, j, n, hf, eq)//創建一個名為a的的空容器,它至少包含n個桶,將hf用作哈希函數,將eq用作鍵值相等謂詞,并插入區間[i, j]中的元素。如果省略了eq,將key_equal( )用作鍵值相等謂詞;如果省略了hf,
將hasher( )用作哈希函數;如果省略了n,則包含桶數不確定
b.hash_function( )//返回b使用的哈希函數
b.key_eq( )//返回創建b時使用的鍵值相等謂詞
b.bucket_count( )//返回b包含的桶數
b.max_bucket_count ( )//返回一個上限數,它指定了b最多可包含多少個桶
b.bucket(k)//返回鍵值為k的元素所屬桶的索引
b.bucket_size(n)//返回索引為n的桶可包含的元素數
b.begin(n)//返回一個迭代器,它指向索引為n的桶中的第一個元素
b.end(n)//返回一個迭代器,它指向索引為n的桶中的最后一個元素
b.cbegin(n)//返回一個常量迭代器,它指向索引為n的桶中的第一個元素
b.cend(n)//返回一個常量迭代器,它指向索引為n的桶中的最后一個元素
b.load_factor()//返回每個桶包含的平均元素數
b.max_load_factor()//返回負載系數的最大可能取值;超過這個值后,容器將增加桶
b.max_load_factor(z)//可能修改最大負載系統,建議將它設置為z
a.rehash(n)//將桶數調整為不小于n,并確保a.bucket_count( )> a.size( ) / a.max_load_factor( )
a.reserve(n)//等價于a.rehash(ceil(n/a.max_load_factor( ))),
其中ceil(x)返回不小于x的最小整數
用法基本都是一致,所以我在這里給大家一個可以尋找到一些數據結構的方法的路徑!故沒有給大家分析與解讀他們的一些存儲與方法作用圖用于理解!
小結
我們一般在用一些基本的數據結構的時候,為了方便與解題技巧我們一般會用到容器!當然具體方法太多,所以建議用什么學什么!
- 有序容器(除了list):存儲底層vector,只是添加了不同的接口!
- deque(隊列):它不像vector 把所有的對象保存在一塊連續的內存塊,而是采用多個連續的存儲塊,并且在一個映射結構中保存對這些塊及其順序的跟蹤。向deque 兩端添加或刪除元素的開銷很小,它不需要重新分配空間。
- list(列表):是一個線性鏈表結構,它的數據由若干個節點構成,每一個節點都包括一個信息塊(即實際存儲的數據)、一個前驅指針和一個后驅指針。它無需分配指定的內存大小且可以任意伸縮,這是因為它存儲在非連續的內存空間中,并且由指針將有序的元素鏈接起來。
- 后面的關聯與無序關聯都是用的一種樹狀結構!
用法與選擇:
- 當數組大小未知時,和需要高效的查詢功能,用vector!高效地隨機存儲。
- 不使用連續的內存空間,而且可以隨意地進行動態操作,有大量的插入、刪除操作,用list!
- 需要在兩端進行push 、pop用daque!它兼顧了數組和鏈表的優點,它是分塊的鏈表和多個數組的聯合。所以它有被list 好的查詢性能,有被vector 好的插入、刪除性能。 如果你需要隨即存取又關心兩端數據的插入和刪除,那么deque 是最佳之選。
- 需要查找或者打表可以選擇map與set,當然一定條件下我們可以優秀考慮用無序關聯容器!