本部分主要是討論“容器和演算法”,具體內容包括:順序容器、關聯容器以及泛型演算法。第9章深入探討vector和其他順序容器類型,第10章介紹關聯容器,即不是順序排列,而是按鍵排序的,第11章介紹泛型演算法,這些演算法通常作用於容器或序列中某一範圍的元素。所謂泛型指的就是這些演算法可以作用於不同的容器類型,而這些容器又可以容納多種不同類型的元素。
九、順序容器
將單一類型元素聚集起來成為容器,然後根據位置來儲存和訪問這些元素,這就是順序容器,順序容器的元素排列次序與元素值無關,而是由元素添加到容器裡的次序決定,標準庫定義了三種順序容器類型:vector、list和deque(雙端隊列),他們的差別在於訪問元素的方式,以及添加或刪除元素相關操作的運行代價,標準庫還提供了三種容器適配器:stack、queue、priority_queue,適配器是根據原始的容器類型所提供的操作,通過定義新的操作介面,來適應基礎的容器類型。
1、順序容器類型:容器只定義了少量的操作,大多數額外操作則由演算法庫提供。
vector:支援快速隨機訪問;list:支援快速插入/刪除;deque:雙端隊列
2、順序容器的定義:所有的容器類型都定義了預設建構函式,用於建立指定類型的空容器物件。預設建構函式不帶參數。
#include<vector><br />#include<list><br />#include<deque></p><p>vector<string> svec;<br />list<int> ilist;<br />deque<Sales_item> items;
3、將一個容器複製給另一個容器時,類型必須匹配,容器類型和元素類型都必須相同。
vector<int> ivec;<br />vector<int> ivec2(ivec); //ok,ivec is vector<int><br />list<int> ilist(ivec); //error,ivec is not list<int><br />vector<double> dvec(ivec); //error,ivec holds int not double
4、初始化為一段元素的副本:儘管不能直接將一種容器內的元素複製給另一個容器,但系統允許通過傳遞一對迭代器間接實現該功能。使用迭代器時,不要求容器類型相同,容器內的元素類型也可以不相同,只要它們相互相容,能夠將要複製的元素轉化為所構建的新容器的元素類型,即可實現複製。
list<string> slist(svec.begin(),svec.end());<br />vector<string>::iterator mid=svec.begin()+svec.size()/2;<br />deque<string> front(svec.begin(),mid);<br />deque<string> back(mid,svec.end());
5、建立順序容器時,可顯式指定容器大小和一個元素初始化式,接受容器大小做形參的建構函式只適用於順序容器,而關聯容器不支援這種初始化。
const list<int>::size_type list_size=64;<br />list<string> slist(list_size,"eh?");
6、容器的容器,注意在指定容器元素為容器類型時,必須使用空格。否則系統會認為>>是單個符號,為右移操作符。
vector< vector<string> > lines;//ok,space required between close></p><p>vector< vector<string>> lines;//error,>>treated as shift operator
7、常用迭代器運算
*iter;//返回迭代器iter所指向的元素的引用<br />iter->mem;//對iter進行解引用,擷取指定元素中名為mem的成員。等效於(*iter).mem<br />iter1==iter2;//比較兩個迭代器是否相等,當兩個迭代器指向同一個容器<br />iter1!=iter2;//中的同一個元素,或者都指向end(),兩個迭代器相等<br />
8、vector和deque容器的迭代器提供額外的運算:迭代器算術運算,以及使用除了==和!=之外的關係操作符來比較兩個迭代器。
iter+n;<br />iter-n;<br />iter1+=iter2;<br />iter1-=iter2;//這是迭代器加減法的複合賦值運算,將iter1加上或減去iter2的運算結果賦給iter1<br />iter1-iter2;//兩個迭代器的減法,其運算結果加上右邊的迭代器即得左邊的迭代器
9、使用左閉合區間[first,last)的編程意義:當first與last相等時,迭代器範圍為空白;當first與last不相等時,迭代器範圍內至少有一個元素,而且first指向該區間中的第一個元素。此外,通過若干次自增運算可以使first的值不斷增大,直到first==last為止。
10、begin和end成員
c.begin():返回一個迭代器,它指向容器c的第一個元素;c.end():返回一個迭代器,它指向容器c的最後一個元素的下一位置
c.rbegin():返回一個逆序迭代器,它指向容器c的最後一個元素;c.rend():返回一個逆序迭代器,它指向容器c的第一個元素前面的位置
11、在順序容器中添加元素:
c.push_back(t);//在容器c的尾部添加值為t的元素,返回void類型<br />c.push_front(t);//在容器c的前端添加值為t的元素,返回void類型,只適用list和deque容器類型<br />c.insert(p,t);//在迭代器p所指向的元素前面插入值為t的新元素,返回指向新添加元素的迭代器<br />c.insert(p,n,t);//在迭代器p所指向的元素前面插入n個值為t的新元素,返回void類型<br />c.insert(p,b,e);//在迭代器p所指向的元素前面插入由迭代器b和e標記的範圍內的元素,返回void類型
12、避免儲存end操作返回的迭代器,如下面代碼,該段代碼將導致死迴圈,問題在於這個程式將end操作返回的迭代器值儲存在名為last的局部變數中,迴圈體中實現了元素的添加運算,添加元素會使得儲存在last中的迭代器失效。
vector<int>::iterator first=v.begin(),last=v.end();</p><p>while(first!=last){<br /> first=v.insert(first,32);<br /> ++first;<br />}
為了避免儲存end迭代器,可以在每次做完插入運算後重新計算end迭代器值:
while(first!=v.end()){<br /> first=v.insert(first.42);<br /> ++first;<br />}
13、所有的容器類型都支援用關係操作符來實現兩個容器的比較,比較的容器必須具有相同的容器類型,而且其元素類型也必須相同。
/*<br />ivec1:1 3 5 7 9 12<br />ivec2:0 2 4 6 8 10 12<br />ivec3:1 3 9<br />ivec4:1 3 5 7<br />ivec5:1 3 5 7 9 12<br />*/<br />ivec1<ivec2;//false<br />ivec2<ivec1;//true<br />ivec1<ivec3;//true<br />ivec1<ivec4;//false<br />ivec1==ivec4;//false<br />ivec1==ivec5;//true
14、容器大小的操作
c.size();//返回容器c中的元素個數。傳回型別為c::size_type<br />c.max_size();//返回容器c可容納的最多元素個數,傳回型別為c::size_type<br />c.empty();//返回標記容器大小是否為0的布爾值<br />e.resize(n);//調整容器c的長度大小,使其容納n個元素,如果n<c.size(),則刪除多出來的元素,否則添加<br /> //採用值初始化的新元素<br />e.resize(n,t);//調整c的大小,使其能容納n個元素,所有添加的元素值都為t
15、訪問元素:
c.back();//返回容器c的最後一個元素的引用。如果c為空白,則該操作未定義<br />c.front();//返回容器c的第一元素的引用。<br />c[n];//返回下標為n的元素的引用,該操作只適用於vector和deque<br />c.at(n);//返回下標為n的元素的引用,如果下標越界,則該操作未定義,該操作只適用於vector和deque
16、刪除元素
c.erase(p);//刪除迭代器p所指向的元素,返回一個迭代器,它指向被刪除元素後面的元素<br />e.erase(b,e);//刪除迭代器b和e所標記的範圍內的元素<br />c.clear();//刪除容器c內的所有元素,返回void<br />c.pop_back();//刪除容器c的最後一個元素,返回void<br />c.pop_front();//刪除容器c的第一個元素,返回void,只適用於list或deque容器
17、賦值與swap
c1=c2;//刪除容器c1的所有元素,然後將c2的值複製給c1<br />c1.swap(c2);//交換內容<br />c.assign(b,e);//重新設定c的元素,b和e必須不是指向c中元素的迭代器<br />c.assign(n,t);//將容器c重新設定為儲存n個值為t的元素
18、容器的capacity(容量)和size(長度)的區別:size指容器當前擁有的元素個數;而capacity則指容器在必須分配心儲存空間之前可以儲存的元素個數。
19、string類型的尋找操作,這些操作都返回string::size_type類型的值
s.find(args);//在s中尋找args的第一次出現<br />s.rfine(args);//在s中尋找args的最後一次出現<br />s.find_first_of(args);//在s中尋找args的任一字元的第一次出現<br />s.find_last_of(args);//在s中尋找args的任一字元的最後一次出現<br />s.find_first_not_of(args);//在s中尋找第一個不屬於args的字元<br />s.find_last_not_of(args);//在s中尋找最後一個不屬於args的字元
20、容器適配器:包括容器適配器、迭代器適配器和函數適配器。使用適配器是,必須包含相關的標頭檔:
#include<stack><br />#include<queue>
十、關聯容器
關聯容器和順序容器的本質區別在於:關聯容器通過鍵儲存和讀取元素,而順序容器則通過元素在容器中的位置順序儲存和訪問元素,雖然關聯容器的大部分行為與順序容器相同,但其獨特之處在於支援鍵的使用。關聯容器支援通過鍵來高效地尋找和讀取元素,兩個基本的關聯容器類型是map和set,map的元素以鍵-值(key-value)對的形式組織,鍵用作元素在map中的索引,而值則表示所儲存和讀取的資料。set僅包含一個鍵,並有效地支援關於某個鍵是否存在的查詢。
1、關聯容器類型包括:(1) map,關聯陣列,元素通過鍵來儲存和讀取;(2) set,大小可變的集合,支援通過鍵實現的快速讀取;(3) multimap,支援同一個鍵多次出現的map類型;(4) multiset,支援同一個鍵多次出現的set類型
2、一般來說,如果希望有效地儲存不同值的集合,那麼使用set容器比較合適,而map容器則更適用於儲存每個鍵所關聯的值的情況。
3、pair類型,該類型在utility標頭檔中定義
#include<utility></p><p>pair<T1,T2> p1;//建立一個空的pair對象,它們的兩個元素分別是T1和T2類型,採用值初始化</p><p>pair<T1,T2> p1(v1,v2);//建立一個pair對象,它們的兩個元素分別是T1和T2類型,其中first成員初始化為v1,<br /> //而second初始化為v2</p><p>make_pair(v1,v2);//以v1和v2值建立一個新的pair對象,其元素類型分別是v1和v2的類型</p><p>p.first;//返回p中名為first的資料成員</p><p>p.second;//返回p中名為second的資料成員
舉例說明如下:
pair<string,string> anno;<br />pair<string,int>word_count;</p><p>typedef pair<string,string> Author;<br />Author proust("Xue","Jamy");</p><p>//pair對象的操作<br />pair<string,string> author("James","Joye");<br />string firstBook;<br />if(author.first="Xue" && author.second=="Jamy")<br /> firstBook="The Love Story";</p><p>//產生新的pair對象<br />pair<string,string> next_auth;<br />string first,last;<br />while(cin>>first>>last){<br /> next_auth=make_pair(first,second);<br />}<br />
4、關聯容器共用大部分但並非全部的順序容器操作,關聯容器不提供front,push_front,pop_front,back,push_back以及pop_back操作。關聯容器不能通過容器大小來定義,因為這樣的話就無法知道鍵所對應的值是什麼,關聯容器還可以根據鍵來排列元素。
5、map類型的定義:必須具備map標頭檔
map<string,int> word_count;</p><p>map<k,v> m;//建立一個名為m的空map對象,其鍵和值的類型分別為k和v<br />map<k,v> m(m2);//建立m2的副本m,m和m2必須有相同的鍵類型和實值型別<br />map<k,v> m(b,e);//建立map類型的對象m,儲存迭代器b和c標記的範圍內所有的元素的副本,元素的類型必須<br /> //能夠轉換為pair<const k,v>
6、實際應用中,鍵類型必須定義<操作符,而且該操作符應能正確的工作。至於是否支援其他的關係或相當運算,則不做要求。
7、map定義的類型
map<K,V>::key_type //在map容器中,用作索引的鍵的類型<br />map<K,V>::mapped_type //在map容器中,鍵所關聯的值的類型<br />map<K,V>::value_type //一個pair類型,它的first元素具有const map<K,V>::key_type類型,<br /> //而second元素則為map<K,V>::mapped_type類型</p><p>//map迭代器進行解引用將產生pair類型的對象<br />map<string,int>::iterator map_it=word_count.begin();<br />cout<<map_it->first;<br />cout<<" "<<map_it->second;<br />map_it->first="new key"; //Error:key is const<br />++map_it->second; //OK:we can change value through an iterator
8、給map添加元素:可以用insert成員實現,或者先用下標操作符擷取元素,然後給擷取的元素賦值。使用下標訪問map與使用下標訪問數組或vector的行為截然不同,用下標訪問不存在的元素將導致在map容器中添加一個新的元素,它的鍵即為該下標值。
map<string,int> word_count;<br />word_count["Anna"]=1;//如果word_count中沒有Anna的元素,則插入</p><p>m.insert(e);//e是一個用在m上的value_type類型的值。<br />m.insert(beg,end);//beg和end是標記元素範圍的迭代器<br />m.insert(iter,e);//e是一個用在m上的value_type類型的值。</p><p>word_count.insert(map<string,int>::value_type("Anna",1));<br />word_count.insert(make_pair("Anna",1));
9、尋找並讀取map中的元素
m.count(k);//返回m中k的出現次數<br />m.find(k);//如果m容器中存在按k索引的元素,則返回指向該元素的迭代器,如果不存在,則返回end()
10、從map對象中刪除元素
m.erase(k);//刪除m中鍵為k的元素。返回size_type類型的值,表示刪除的元素個數</p><p>m.erase(p);//從m中刪除迭代器p所指向的元素。p必須指向m中確實存在的元素,而且不能等於m.end(),返回void</p><p>m.erase(b,e);//從m中刪除一段範圍內的元素,返回void
11、set類型:只是單純的鍵的集合。
set容器支援大部分的map操作,兩種例外包括:set不支援下標操作符,而且沒有定義mapped_type類型,在set容器中,value_type不是pair類型,而是與key_type相同的類型,它們指的是set中儲存的元素類型。
12、set容器的定義和使用:必須包含set標頭檔
與map容器一樣,set容器的每個鍵都只能對應一個元素,以一段範圍的元素初始化set對象,或在set對象中插入一組元素時,對於每個鍵,事實上都只添加一個元素。
vector<int> ivec;<br />for(vector<int>::size_type i=0;i!=10;++i){<br /> ivec.push_back(i);<br /> ivec.push_back(i);<br />}//ivec有20個元素,即兩個0~9的副本</p><p>set<int> iset(ivec.begin(),ivec.end());<br />cout<<ivec.size()<<endl; //20<br />cout<<iset.size()<<endkl//10:iset只能儲存不同元素的值
13、multimap和multiset類型:允許一個鍵對應多個執行個體。multimap和multiset所支援的操作分別與map和set的操作相同,只有一個例外:multimap不支援下標運算。因為這類容器中,某個鍵可能對應多個值。
(1) 元素的添加和刪除
multimap<string,string> authors;</p><p>//Add Elements<br />authors.insert(make_pair(string("Xue"),string("Love")));<br />authors.insert(make_pair(string("Xue"),string("Hello")));</p><p>//Del<br />string search_item("Xue");<br />multimap<string,string>::size_type cnt=authors.erase(search_item);
(2) 與眾不同的面向迭代器的解決方案
m.lower_bound(k);//返回一個迭代器,指向鍵小於k的第一個元素<br />m.upper_bound(k);//返回一個迭代器,指向鍵大於k的第一個元素<br />m.equal_bound(k);//返回一個迭代器的pair對象,它的first成員等價於m.lower_bound(k).<br /> //而second成員等價於m.upper_bound(k)
(3) equal_range函數:返回儲存一對迭代器的pair對象,如果該值存在,則pair對象中的第一個迭代器指向該鍵關聯的第一個執行個體,第二個迭代器指向該鍵關聯的最後一個執行個體的下一個位置,如果找不到匹配的元素,則pair對象中的兩個迭代器都將指向此鍵應該插入的位置。
十一、泛型演算法
標準庫容器定義的操作非常少,標準庫沒有給容器添加大量的功能函數,而是選擇提供一組演算法,這些演算法大都不依賴特定的容器類型,可作用在不同類型的容器和不同類型的元素中,本章詳細介紹泛型演算法以及對迭代器更詳盡的描述。泛型演算法本來從來不執行容器操作,只是單獨依賴迭代器和迭代器操作實現,演算法基於迭代器及其操作實現,而並非基於容器操作。
1、使用泛型演算法必須包含algorithm標頭檔#include<algorithm>.標準庫還定義了一組泛化的算術演算法,其命名習慣與泛型演算法相同,使用這些演算法則必須包含numberic標頭檔#include<numeric>。
2、用於指定累加起始值的第三個實參是必要的,因為accumulate對將要累加的元素類型一無所知。
//將sum設定為vec的元素之和再加上42<br />int sum=accumulate(vec.begin(),vec.end(),42);</p><p>//把string型的vector容器的元素串連起來<br />string sum=accumulate(v.begin(),v.end(),string(""))'
3、find_first_of的使用:在第一段範圍內尋找與第二段範圍中任意元素匹配的元素,然後返回一個迭代器,指向第一個匹配的元素,如果找不到匹配元素,則返回第一個範圍的end迭代器。以下例子統計有多少個名字同時出現在這兩個列表中。
size_t cnt=0;<br />list<string>::iterator it=roster1.begin();<br />while((it=find_first_of(it,roster1.end(),roster2.begin(),roster2.end())){<br /> ++cnt;<br /> ++it;<br />}<br />cout<<"Found"<<cnt<br /> <<"name on both rosters"<<endl;
4、寫容器元素的演算法,有些演算法直接將資料寫入到輸入序列,另外一些則帶有一個額外的迭代器參數指定寫入目標。
(1) 寫入輸入序列的元素:本質上是安全的,只會寫入與制定輸入範圍數量相同的元素。
fill(vec.begin(),vec.end(),0);
(2) 不檢查寫入操作的演算法:fill_n函數帶有的參數包括一個迭代器、一個計數器以及一個值。注意不能對沒有元素的空容器調用
fill_n函數(或者類似的演算法)
vector<int> vec;//empty vector<br />fill_n(vec.begin(),10,0);//Error:attempts to write 10 elements in vec
(3) 確保演算法有足夠的元素儲存輸入資料的一種方法是使用插入迭代器,插入迭代器可以給基礎容器添加元素的迭代器,通常用迭代器給容器元素賦值時,被賦值的是迭代器所指向的元素,而是用插入迭代器賦值時,則會在容器中添加一個新元素,其值等於賦值運算的右運算元的值。為了說明如何安全使用寫容器的演算法,下面使用back_inserter,必須包含iterator標頭檔。
//引入back_inserter<br />vector<int> vec;<br />fill_n(back_inserter(vec),10,0);</p><p>//寫入到目標迭代器的演算法:copy從輸入範圍讀取元素,然後賦值給ivec<br />vector<int> ivec;<br />copy(ilist.begin(),ilist.end(),back_inserter(ivec));</p><p>//上述例子的效率比較差,用下面方法代替<br />vector<int> ivec(ilist.begin(),ilist.end());</p><p>//這個調用將所有值為0的執行個體替換成42<br />replace(ilist.begin(),ilist.end,0,42);</p><p>//ilist沒有改變,ivec儲存ilist的一份副本,在ivec中所有0都變成了42<br />vector<int> ivec;<br />replace_copy(ilist.begin(),ilist.end(),back_inserter(ivec),0,42);
5、對容器元素重新排序的演算法,一個執行個體:統計段落中使用了多少個由6個或以上的字母組成的單詞。
(1) 去除重複:
sort(words.begin(),words.end());<br />vector<string>::iterator end_unique=unique(words.begin(),words.end());<br />words.erase(end_unique,words.end());
(2) 定義需要的使用函數:
bool isShorter(const string &s1,const string &s2)<br />{<br /> return s1.size()<s2.size();<br />}</p><p>bool GT6(const string &s)<br />{<br /> return s.size()>=6;<br />}
(3) 排序演算法,stable_sort演算法保留相等元素的原始相對位置。
stable_sort(words.begin(),words.end(),isShorter);
(4) 統計長度不小於6的單詞
vector<string>::size_type wc=count_if(words.begin(),words.end(),GT6);