原創 by zoe.zhang
0.寫在前面的話
我是在2011年學的C++,但是那一年恰好是C++11新標准的一年,但是大學上學的C++還是基於C++98的風格的,使用的編譯器也是VC6.0,啊,插一句話,雖然VC6現在看起來有些簡陋,而且也不支持C++新標准,但是因為它的輕便,以及有些年代感的編碼界面,我自己感覺它就像是一柄鈍劍,加上是我接觸的第一個編譯器,因此對它還是懷有敬意的。當然,現在用的VS2013,編程友好,功能強大,也是非常棒的了。它是支持C++11標准的。C++11相對C++98來說有了很多新變化,根據書上說,是為C++打磨了一套新的體系。當年的C++學習的基礎現在想來真的很是淺顯,不過語言真的很有趣啊,閱讀C++ primer這本大厚書的時候,居然很多時候並不覺得枯燥,反而仿佛看到一扇新的大門一樣,忍不住驚嘆設計的巧妙,啊,不,美妙。
總的來說,自己在學習C++的路上還有許多事情和實踐需要做。
這一篇文章是和C++中的容器相關,基本當年大一的時候是沒有接觸過的內容,如今捧卷來讀,只覺自己知道得太少。好了,切入正題。
C++11的標准庫得到了廣泛的贊譽以及被眾多程序員接受和使用。STL標准庫中封裝了許多復雜優秀的數據結構/容器,也提供了大量的基於迭代器的以及等常用的數據結構算法和操作,
在C++標准中,STL被組織為下面的17個頭文件:<algorithm>、<deque>、<functional>、<iterator>、<array>、<vector>、<list>、<forward_list>、<map>、<unordered_map>、<memory>、<numeric>、<queue>、<set>、<unordered_set>、<stack>和<utility>等等。
這一章主要是關於容器的學習記錄。容器主要分為順序容器(如vector,list等),關聯容器(map,set等),對於這些容器C++提供了插入、排序,刪除、查找等等一系列通用或者部分特殊的常用操作。這里要稍微理解一些這些容器與我們在算法中經常見到的數據結構中的關系。
vector:類似於增強型數組結構的封裝;list:鏈表式結構;
set/map:基於高效的平衡二叉樹(紅黑樹),紅黑樹具有很好的統計特性,作為關聯容器的內部結構。關於紅黑樹/AV-L樹放到其他章節再討論。
還有一點為什么容器類被稱作容器,因為容器實際上是一種類模板,可以像容器一樣存放大多數類型的對象。
1.迭代器
迭代器是使用容器的基礎,除掉數組、vector、string等對象可以使用下標運算符,迭代器(iterator)是一種更加通用的機制。在使用容器之前需要深刻理解迭代器的用法,STL提供的泛型算法是基於迭代器對象或者說是一對迭代器生成的范圍之上的。此處指出一下:嚴格上string類型不屬於容器,但是它支持很多與容器類型的操作,也支持迭代器。迭代器提供了對對象的間接訪問,可以從一個元素移動到下一個元素,(在C++11里,對於對象的>/</++/--/==/!=等運算符的定義要注意理解。)迭代器某種意義上有點像更寬泛意義上的指針,在行為上也有點像,它有無效和有效之分,有效的迭代器指向某個元素或者指向尾后元素。
(1)獲取迭代器:迭代器類型
有迭代器的容器類型都有擁有返回迭代器的成員。我們主要是使用迭代器,有時候並不關心迭代器的類型,所以可以由編譯器決定,也可以顯示指定。
auto it_b = v.begin(), it_end = v.end(); // 由編譯器決定 auto cit_b = v.cbegin(), cit_end = v.cbegin(); // 返回常量迭代器 vector<int>::iterator it; //顯示定義 vector<int>::const_iterator cit;//顯示定義,常量,能夠讀取但是不修改
(2)檢查容器是否為空
這里有一個特殊的迭代器,稱為“尾后迭代器”,某種程度上類似於一個邊界條件,(end iterator),用於構成迭代器范圍。通常使用容器第一步需要判斷容器是否為空,容器為空時,begin和end函數返回的是同一個迭代器,均為尾后迭代器。或者迭代器遍歷容器完畢結束的條件,就是到達尾后迭代器。
if (v.begin() != v.end()){}
(3)迭代器的基本操作:
- *iter:解引用,返回迭代器所指向元素的引用;如果解引用所得到的對象是類,通過箭頭運算符將解引用和成員訪問結合到一起,iter->mem;
- ++iter,--iter:迭代器從一個元素移動下一個元素;幾乎所有迭代器都支持++操作,++操作相對來說更通用,在反向迭代器時要注意理解。(注意:所有標准庫容器都支持++遞增運算的迭代器,一些容器的迭代器提供了額外的操作,比如vector和string,array,deque的迭代器運算相對豐富)(forward_list迭代器不支持--遞減運算符操作)
- ==,!=:判斷兩個迭代器是否指向同一元素(包括尾后迭代器)
(4)迭代器范圍:
迭代器范圍的概念是標准庫的基礎。一個迭代器范圍由一對迭代器表示,這里的迭代器區間是左閉合區間[begin,end),從begin開始,但是不包含end。如果begin與end相等,那么范圍為空,如果begin和end不等,那么范圍至少包含一個元素。
(5)迭代器的種類
除了為容器提供的用於訪問數據提供的迭代器之外,標准庫還提供了其他幾種迭代器。
插入迭代器:綁定在容器上,可以向容器內插入數據;它接受一個數據,生成一個迭代器,當通過insert iterator進行賦值的時候,迭代器調用容器操作來向容器指定位置插入一個元素。
流迭代器;綁定在輸入或輸出流上,用來遍歷關聯的的IO流;
反向迭代器:迭代器向后移動,雖然也使用++運算符,但是是向后移動的;(C++ primer 圖10.2 )
移動迭代器:顧名思義,用於移動元素。
2.順序容器
順序容器中,元素的排放順序是與其加入容器時的位置相對應的,這種順序不依賴於元素的值,與元素加入容器時的位置縣桂英。
關聯容器中,元素的位置由相關聯的關鍵字值決定的。也就是所關聯容器中元素的位置與關鍵字關聯。
(1)順序容器的種類:順序容器都提供了快速順序訪問元素的能力。
- vector:可變大小數組,支持快速隨機訪問,可快速增長,但是插入或刪除元素可能很慢。
- deque:雙端隊列,支持快速隨機訪問,在首尾插入、刪除速度很快。
- list:雙向鏈表,雙向順序訪問/遍歷(鏈表不支持元素的隨機訪問),list在任何位置的插入和刪除速度都很快,很方便。
- forward_list:單向鏈表,單向順序訪問。
- array:固定大小數組,支持隨機快速訪問,不能刪除或填加元素。
- string:字符串容器。
關於順序容器的使用有一些總結:
a. 除了array固定大小,其他順序容器都提供高效、靈活的內存管理,可以添加、刪除、擴張和收縮容器。
b. string 和vector等容器將元素保存在連續的內存空間中,即其元素的內存是連續存儲的,可以支持下標操作。
c. 通常使用vector是很好的選擇,除非你有更好的理由選擇其他容器。
d. 如果在讀取時需要插入元素,但是隨后需要使用隨機訪問元素,根據情況可以選擇sort重排vector,更通用情況是輸入階段使用list,輸入完成后,將list中的內容放入到一個vector中。
(2)容器庫的操作
容器型的操作可以看成包括3種類型:一種是通用的,一種各自針對順序容器、關聯容器、無序容器等,還有一種是針對特殊的容器類型。
容器通用操作包括:類型別名,構造函數(默認,拷貝,迭代器范圍構造c(b,e),列表初始化),賦值和swap,大小,添加和刪除(insert,emplace,erase,clear),關系運算符,獲取迭代器.
(3)順序容器的操作
(4)vector對象是如何增長的:關於內存空間分配的一些討論;
(5)string庫的額外操作
3.關聯容器
關聯容器中的元素是根據關鍵字來保存和訪問的,順序中的容器元素按照它們在容器中的位置來順序保存和訪問。
關聯容器支持的是高效的關鍵字查找和訪問。、
(1)綜述
關聯容器的類型主要有map和set兩種,根據有序無序,允許重復和不允許重復關鍵字分為8種關聯容器。按照關鍵字有序保存元素的容器定義在map和set頭文件中,無序容器保存在unordered_map和unordered_set中,有序容器實際上按順序保存元素,通過比較運算符來組織元素。無序容器使用哈希(hash)函數和關鍵字類型==來組織元素。
舉例:set——有序存儲的不允許重復關鍵字的集合; unordered_multi_set——無序存儲的通過哈希函數組織、允許關鍵字重復的集合。
對於有序容器而言,傳入的關鍵字類型,需要定義元素的比較方法,標准庫通過使用關鍵字的類型<運算符比較兩個關鍵字,來組織元素。
(2)map
map是關鍵字—值對的集合,關鍵字起到索引的作用,可以被稱為“關聯數組”,因為它可以通過下標運算符來訪問值,只不過下標運算符中傳入的是關鍵字。在文本處理上,字典是一個map一個很好的使用例子。
a. map初始化:
當定義一個map時,需要指明關鍵字和值的類型,關聯容器都定義了一個默認構造函數,創建一個指定類型的空容器。
如果需要對map或者set等容器進行初始化,可以使用列表初始化;將關聯容器初始化為另一同類型容器的拷貝,或者使用一個值范圍來進行初始化;(迭代器范圍);其中需要注意一下multimap或者multiset與map和set的不同。
//map 值初始化 map<string, string>authors = { { "jocye", "1111" }, { "Austen", "2222"}, { "Dicken", "3333"} }; //ivec是一個包含重復元素的vector set<int> iset(ivec.cbegin(), ivec.cend()); //不包含重復元素 multiset<int> miset(ivec.cbegin(), ivec.cend());//包含重復元素
b.map的使用:
//統計出現次數——下標運算符 map<string, size_t> word_count; string word; while (cin >> word) ++word_count[word]; for (const auto &w : word_count) cout << w.first << "occurs" << w.second << ((w.second > 1) ? "times" : "time") << endl;
這里用到了下標運算符[],下標運算符在map中的特點是,會根據傳入的對象去尋找對應的key-value對,如果元素不在map中,會自動創建一個新元素,初始化value為0‘;
map中的元素為pair類型,pair的first成員保存關鍵字,second成員保存對應的值。
(3)set
set是關鍵字的簡單集合,set支持的是高效的關鍵字查詢操作,檢查一個關鍵字是否在set中,set是最有用的,常見一個場景,定義set保存要忽略的元素,或者檢查屏蔽等。
a. 定義一個set的時候,應當指明關鍵字類型,其初始化可參考上文map的初始化問題。
b.set的使用:
//使用set忽略想忽略的單詞 map<string, size_t> word_count; set<string> exclude = { "the", "but", "and", "or", "an" };//列表初始化 string word; while (cin >> word) { if (exclude.find(word) == exclude.end()) ++word_count[string]; }
這里是set一個最常見的用法,set.find(obj),調用返回一個迭代器,如果給定關鍵字在set中,就指向該關鍵字,如果不在,find返回尾后迭代器。
(4)關聯容器迭代器
map<string, int> word_count; auto map_it = word_count.begin(); //獲取map迭代器 //*map_it是pair類型;可以通過map_it->first;map_it->second 訪問鍵值對 // 注意:map_it->first 關鍵字是const 不可改變 set<int> myset = { 0, 1, 2, 3, 5, 6, 7, 8 }; set<int>::iterator set_it = myset.begin(); //顯式定義迭代器 不管是begin還是cbegin返回的都是const_iterator
(5)關聯容器相關操作
- 遍歷操作:遞增迭代器遍歷
auto map_it = word_count.cbegin(); while (map_it != word_count.cend()) { cout << i; cout<<(map_it->first)<<"occurs"<< (map_it->second) << endl; ++map_it; }
- 添加元素
//set 插入操作:不允許重復元素 vector<int> vec = { 2, 4, 6, 8, 2, 4, 6, 8 }; set<int> set2; set2.insert(vec.cbegin(), vec.cend()); //set2 有4個元素 重復元素不插入 接受一對迭代器 set2.insert({ 1, 3, 5, 7, 1, 3, 5, 7 }); //再插入4個元素 接受一個初始化器列表 //map 向map添加元素:所添加的元素類型是pair word_count.insert({ "word", 1 }); //{}花括號構造初始化一個pair word_count.insert(make_pair("word", 1));//make_pair顯示構造 word_count.insert(pair<string, size_t>("word", 1));
insert函數會返回值,對於不包含重復關鍵字的容器,添加單一元素的insert會返回一個pair,pair的的第一個元素是一個迭代器,指向給定關鍵字的元素,第二個元素second,是一個bool值,如果關鍵字已經在容器中,bool值為false,insert不做任何事。如果關鍵字不在,元素插入容器中,bool值為true。對於允許重復關鍵字的容器,接受單一元素的容器的insert會返回一個指向新元素的迭代器。
- 刪除元素
c.erase(k);//k是一個關鍵字,返回一個size_type的值,刪除元素的數量; c.erase(it);//it是一個迭代器,刪除迭代器it指向的元素 c.erase(b, e);//一對迭代器所表示的范圍元素
- 訪問元素
訪問元素是很重要的一個方法,因為關聯容器的一個意義所在就是提供了高效得對元素的訪問。主要有find和count兩個方法,find返回迭代器,count可以計算出元素出現的次數。
對於map和unordered_map(不允許重復)的類型中,下標運算符提供了最簡單的訪問和提取元素值的方法,但是下標運算符會將未在map中的元素添加到map中。因此如果不想改變map,所以應該使用find方法。
set<int> myset = { 1, 2, 3, 4, 5, 6, 7, 8 }; myset.find(1); //返回一個迭代器 指向key == 1; 如果未找到,返回set2.end(); myset.count(1);//返回1; myset.lower_bound(2); //返回一個迭代器 指向第一個關鍵字不小於k ==2 的元素 myset.upper_bound(2); //返回一個迭代器 指向第一個關鍵字大於k ==2 的元素 注意是大於;左閉合區間 myset.equal_range(2); //返回一個pair,pair兩個元素均是迭代器 表示關鍵字==k的元素范圍----常用於multiset/multimap; // map multimap<string, string> author; string search_item = "Alain"; for (auto beg = author.lower_bound(search_item), end = author.upper_bound(search_item); beg != end; ++beg) cout << beg->second; // 如果上限和下限返回相同迭代器,則給定關鍵字不在容器中
(6)無序容器
無序容器使用hash function和關鍵字類型==運算符來組織元素,無序容器在存儲組織上為一組桶,每個桶保存零個或者多個元素,無序容器使用哈希函數將元素將元素映射到桶。
訪問元素的步驟:a、計算元素的哈希值,指出這個元素是放在哪一個桶中(具有相同哈希值的元素保存在相同的桶中)b、桶中存在多個元素時,通過順序搜索這些元素來查找我們想要的那個。
哈希技術能夠獲得更好的平均性能。無序容器的性能依賴於哈希函數的質量和桶的數量和大小。
4.泛型算法
標准庫容器提供了大量的泛型算法,這些算法獨立於特定容器,這些算法是運行於迭代器之上的,不會去做容器的操作。這一概念一定要牢記在心理,也是泛型算法之所以通用的關鍵所在。
大多是算法都定義在頭文件algorithm中,還有numeric中定義了數值的泛型算法。
泛型算法的知識還是相當多的,同時對迭代器的理解也要更深刻,這里我只列舉一些基礎和淺顯的實例,更多知識應當參閱C++primer中。
此外還要注意一點,泛型算法很大一部分跟順序容器的聯系比較緊密,我們通常不對關聯容器使用泛型算法,因為對於關聯容器,關鍵字通常是const的,所以不能將關聯容器傳遞給修改或者重排容器的算法。關聯容器雖然可以使用讀取元素的算法,比如說泛型算法中的find方法,但是其效率遠遠比不上關聯容器自己定義的專用find方法,這一點要注意一下。
對於泛型算法而言,主要可以分為三類:讀取元素、改變元素或者重排元素;
除了少數例外,標准庫算法都是對一個范圍內的元素進行操作,也就是所說的“輸入范圍”,該輸入范圍通常使用要處理的第一個元素和最后一個元素之后位置的兩個迭代器表示。(再度表明左閉合區間)
(1)標准庫find方法
vector<int> vec = { 41, 42, 43, 44, 45, 46 }; int val = 42; //find算法:參數表(迭代器1,迭代器2,要查找的值val) auto result = find(vec.cbegin(), vec.cend(), val); //如果找到,返回該元素的迭代器;如果找不到,返回vec.cend(); cout << "the value" << val << (result == vec.cend() ? "is not present" :"is present") << endl; //find可以用在數組中查找值 int ia[] = { 27, 20, 31, 34, 109, 11 }; int *ib = ia; int ii = 27; int *result = find(begin(ia), end(ia), ii); //傳入位置指針 auto result2 = find(ia, ia + 4, ii); // 范圍
(2)accumulate:求和,接收三個參數,定義在頭文件numeric中;
//accumulate 的第三個參數類型決定函數使用哪個加法運算符 第三個參數是初值 string sum = accumulate(v.cbegin(), v.cend(), string(""));
(3)重排容器元素的算法:sort unique;
void elim(vector<string> &words) { sort(words.begin(), words.end());//重排 auto end_unique = unique(words.begin(), words.end()); words.erase(end_unique, words.end()); }