1. 程式人生 > >C++筆記(6):標準模板庫STL:容器、叠代器和算法

C++筆記(6):標準模板庫STL:容器、叠代器和算法

strong 並且 pty 優先級隊列 決定 image left sig 樹結構

STL(Standard Template Library)是C++標準庫的一部分。
STL的代碼從廣義上講分為三類:容器、叠代器和算法。

1.容器

2.叠代器

3.算法

--------------------------------------------------------------------------------------------------------------------------

1.容器  

順序容器
容器是特定類型對象的集合。順序容器為程序員提供控制元素存儲和訪問順序的能力。元素順序由加入容器的順序決定。
順序容器包括:vector、list、deque。
vector
  vector將元素保存在連續的存儲空間,相當於數組

,因此可以通過下標隨機的訪問,速度很快。但是在容器的中間位置添加和刪除元素會非常耗時。

  因為一次插入和刪除操作都需要移動插入和刪除位置之後的所有元素,來保持連續存儲。而且添加元素有時還需要分配額外的存儲空間,拷貝數據到新空間,並釋放老的空間。

  在創建一個vector 後,它會自動在內存中分配一塊連續的內存空間進行數據存儲,初始的空間大小可以預先指定也可以由vector 默認指定,這個大小即capacity ()函數的返回值。當存儲的數據超過分配的空間時vector 會重新分配一塊內存塊,但這樣的分配是很耗時的,在重新分配空間時它會做這樣的動作:
  首先,vector 會申請一塊更大的內存塊,然後將原來的數據拷貝到新的內存塊中,,接著銷毀掉原內存塊中的對象,最後將原來的內存空間釋放掉。
如果vector 保存的數據量很大時,這樣的操作一定會導致糟糕的性能所以說vector 不是在什麽情況下性能都好,只有在預先知道它大小的情況下vector 的性能才是最優的。

list
  是一個線性鏈表結構

,它的數據由若幹個節點構成,每一個節點都包括一個信息塊(即實際存儲的數據)、一個前驅指針和一個後驅指針。它無需分配指定的內存大小且可以任意伸縮,這是因為它存儲在非連續的內存空間中,並且由指針將有序的元素鏈接起來。
  容器使用鏈表在任何位置添加和刪除都很快。但代價是不支持隨機的訪問。為了訪問一個元素,需要遍歷整個容器。不支持下標運算符。
  雖然隨機檢索的速度不夠快,但是它可以迅速地在任何節點進行插入和刪除操作。因為list 的每個節點保存著它在鏈表中的位置,插入或刪除一個元素僅對最多三個元素有所影響,不像vector 會對操作點之後的所有元素的存儲地址都有所影響,這一點是vector無法比擬的。

deque
  是一種優化了的、對兩端元素進行添加和刪除操作的順序容器
允許較為快速地隨機訪問,但它不像vector 把所有的對象保存在一塊連續的內存塊,而是采用多個連續的存儲塊,並且在一個映射結構中保存對這些塊及其順序的跟蹤。向d兩端添加或刪除元素的開銷很小。

  它不需要重新分配空間,所以向末端增加元素比vector 更有效。

  實際上,deque 是對vector 和list 優缺點的結合,它是處於兩者之間的一種容器

vector 、list 、deque 在內存結構上的特點:

技術分享

確定選擇哪種順序容器:
根據需要確定使用哪種順序容器,下面是一些準則:
如果程序要求隨機訪問元素,應該使用vector或deque。
如果程序要求在容器的中間插入和刪除元素,應該使用list。
如果程序需要在頭尾插入和刪除元素,但不會再中間位置進行插入和刪除操作,應使用deque.如果元素個數不定,且只需從末尾插入,並需要隨機訪問,可以使用deque。
如果既需要隨機訪問元素,又需要在容器中間位置插入元素,此時就要取決於插入或刪除元素的相對性能。可以分別對它們進行測試,可以根據測試結果決定使用何種容器


容器的方法
STL中的容器類型對外提供了統一的公共接口。這些接口的差別在於它們提供哪些操作,但是如果兩個容器提供了相同的操作,則它們的接口(函數名字和參數個數)應該相同。
? 一些操作適用於所有容器類型。
? 另外一些操作則只適用於順序或關聯容器類型。
? 還有一些操作只適用於順序或關聯容器類型的一個子集。

順序容器提供的操作
下面我們分別來介紹這些操作,如果某個操作只適用於某個容器類型,我們會專門指出。


為了定義一個容器類型的對象,必須先包含相關的頭文件:
#include <vector>
#include <list>
#include <deque>

它們都定義在std名字空間中

所有的容器都是類模板,要定義某種特殊的容器,必須在容器名後加一對尖括號,尖括號裏面提供容器中存放的元素的類型:
vector<string> svec;
list<int> ilist;
deque<int> items;

所有容器類型都定義了默認構造函數,用於創建指定類型的空容器對象。默認構造函數不帶參數。

容器初始化:
C<T> c; 創建一個名為c的容器,容器類型為C,如vector或list,T為容器內元素類型。適用於所有容器;
C c1(c); 創建容器c的副本,c1和c必須具有相同的容器類型和元素類型,適用於所有容器;
C c(b,e); 創建容器c,元素是叠代器b,e標示範圍內的副本,適用於所有容器;
C c(n,t); 創建容器c,元素為n個值為t的值,t的類型必須是容器C的元素類型或者可以轉換為該類型,只適用於順序容器;
C c(n); 創建具有n個值初始化元素的容器c,元素類型為值n的類型,只適用於順序容器;

順序容器的方法

a)begin和end
返回容器的叠代器,通過叠代器我們可以訪問容器內的元素。
std::vector<int>::iterator iter = c.begin();
c.begin()
c.end()
c.rbegin()
c.rend();

b)添加元素:
c.push_back();在容器尾部添加值為t的元素,返回void;
c.push_front();在容器頭部添加值為t的元素,返回void,只適用於list和deque;
c.insert(p,t);在叠代器p所指向的元素前面插入值為t的元素,返回指向t的叠代器;
c.insert(p,n,t);在叠代器p所指向的元素前面插入n個值為t的元素,返回void;
c.insert(p,b,e);在叠代器p所指向的元素前面插入由叠代器b和e標記的範圍內的元素,返回void;

c)獲得容器大小:
c.size();返回容器內元素個數,返回類型為c::size_type;
c.max_size();返回容器內最多可容納的元素個數,返回類型為c::size_type;
c.empty();測試容器內是否有元素;
c.resize(n);調整容器大小,使其能容納n個元素;
c.resize(n,t);調整容器大小,使其能容納n個元素,新添加元素以t初始化;

d)訪問容器元素:
c.front();返回容器內第一個元素的引用,如果c為空,該操作未定義;
c.back();返回容器內最後一個元素的引用,如果c為空,該操作未定義;
c[n] at方法;返回下標為n的元素的引用,n越界時,該操作未定義,只用於vector和deque;

e)刪除元素:
c.erase(p);刪除叠代器p指向的元素,返回一個指向被刪除元素後面的元素的叠代器;
c.erase(b,e);刪除叠代器b和e標記範圍內的所有元素,返回一個指向被刪除元素段後面的元素的叠代器
c.clear();刪除容器內的所有元素,返回void;
c.pop_back();刪除容器的最後一個元素,返回void;
c.pop_front();刪除容器的第一個元素,返回void,只適用於list和deque;
特別註意:刪除元素可能會使叠代器失效,所以每次操作之後都應該及時更新叠代器;

f)賦值操作:
cl = c2;刪除cl的所有元素,將c2的所有元素復制給c1,c1和c2的容器類型及元素類型必須相同;
cl.swap(c2);交換c1和c2中的所有元素,c1和c2的容器類型及元素類型必須相同;
c.assign(b,e);重新給c賦值,內容為b和e所標記範圍內的元素,b和e必須不是指向c中的元素的叠代器
c.assign(n,t);將c中的元素重新設置為n個值為t的元素;

--------------------------------------------------------------------------------------------------------------------------

關聯容器

map、set, multiset, multimap 是一種非線性的樹結構,具體的說采用的是一種比較高效的特殊的平衡檢索二叉樹—— 紅黑樹。
  1.map ,提供一種“鍵- 值”關系的一對一的數據存儲能力。其“鍵”在容器中不可重復,且按一定順序排列(其實我們可以將set 也看成是一種鍵- 值關系的存儲,只是它只有鍵沒有值。它是map 的一種特殊形式)。由於其是按鏈表的方式存儲,它也繼承了鏈表的優缺點。
  2.set ,又稱集合,實際上就是一組元素的集合,但其中所包含的元素的值是唯一的,且是按一定順序排列的,集合中的每個元素被稱作集合中的實例。因為其內部是通過鏈表的方式來組織,所以在插入的時候比vector 快,但在查找和末尾添加上被vector 慢。
  3.multiset ,是多重集合,其實現方式和set 是相似的,只是它不要求集合中的元素是唯一的,也就是說集合中的同一個元素可以出現多次。
  4.multimap , 和map 的原理基本相似,它允許“鍵”在容器中可以不唯一。

相對於順序容器,有以下幾個主要特點:
1, 其內部實現是采用非線性的二叉樹結構,具體的說是紅黑樹的結構原理實現的;
2, set 和map 保證了元素的唯一性,mulset 和mulmap 擴展了這一屬性,可以允許元素不唯一;
3, 元素是有序的集合默認在插入的時候按升序排列

map
map類型可理解為關聯數組,關聯的本質在於元素的值與某個特定的鍵相關聯,而與元素的位置無關。
pair類型
為了講map,得先將pair類型:pair就是一個兩個類型的組合,比如一個人的學號就可以是pair<int,string>,其中的int是學號,string是名字。
map就是一個容器,裏面裝的就是若幹個pair。每個pair的第一個元素,稱為鍵第二個元素,稱為值。對於普通的map,每個鍵都對應一個值。
map類定義了3種類型,分別為key_type、mapped_type、以及vaule_type。分別表示了鍵的類型、值的類型,以及一個pair類型,pair的第一個元素是鍵,第二個元素是值。

定義pair對象
pair<T1,T2> p1;創建一個空的pair對象,兩個元素類型分別是T1和T2,值初始化;
pair<T1,T2> p1(v1,v2);創建一個pair對象,具有兩個元素v1和v2,類型分別是T1和T2;
make_pair(v1,v2);以v1和v2作為值創建一個pair對象,元素類型分別是v1和v2的類型;

p1.first;返回鍵值;
p1.second;返回值;
p1 < p2;小於運算遵循字典次序;
p1 == p2;如果兩個pair對象的first成員和second成員依次相等,則兩個pair對象相等;

map對象的定義:
map<k,v> m;創建一個空的map對象m,其鍵和值的類型分別為k,v;
map<k,v> m(m2);創建m2的副本m,m必須具有和m2相同的鍵類型和值類型;
map<k,v> m(b,e);創建一個map對象m,存儲叠代器[b,e)標記範圍內的所有元素的副本

map定義的類型:
map<k,v>::key_type;鍵的類型
map<k,v>::mapped_type;元素的類型
map<k,v>::value_type;
pair<const map<k,v>::key_type,map<k,v>::mapped_type>

a)添加元素:
m.insert(e);//e為map的value_type類型的值。如果鍵e.first不存在,則添加一個值為e.second的值,如果存在,則m不變。該操作返回一個pair類型的對象,包含一個指向新添加元素的map叠代器,以及一個bool值,表示插入是否成功;
m.insert(b,e);將叠代器b和e標示區間內的所有元素插入到m中,所插入的元素必須為m.value_typpe類型,返回void;
m.insert(iter,e);如果m中不存在鍵e.first,則在m中以iter為起點搜索新元素的位置並插入,返回一個指向新添加元素的叠代器。
m[1000] = "張三";在map中查找鍵為1000的元素,如果沒有,則插入一個新的鍵值對,鍵為1000,值為張三。

b)查詢元素:
m.count(k);返回m中k鍵出現的次數,返回值只能是0或1;
m.find(k);如果m中存在鍵為k的元素,則返回指向該元素的叠代器,否則,返回m.end();

c)刪除元素:
m.erase(k);刪除m中鍵為k的元素,返回值為所刪除元素的個數,只能是0或1,類型為size_type;
m.erase(p);刪除m中叠代器p所指向的元素,p所指向的元素必須存在,返回void;
m.erase(b,e);刪除m中由叠代器[b,e)標示範圍內的元素,[b,e)必須是有效範圍,返回void。

set類型
  set容器單單存儲鍵的集合。
  set容器支持map的大部分的操作,以下為set容器不支持的操作:
set不支持下標操作;
沒有定義mapped_type,value_type對應的是key_type,即元素的類型;
與map一樣,set容器中的鍵也必須唯一,而且不能修改。

multimap和multiset
元素的添加刪除:
insert 操作每次都會添加一個元素;
erase 帶有一個鍵參數的該操作將刪除擁有該鍵的所有元素,返回刪除的元素個數,而帶有一個或一對叠代器參數,則只刪除指定的元素,返回void。

查找元素:
m.count,m.find()
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)

--------------------------------------------------------------------------------------------------------------------------

容器適配器

適配器是容器的接口,它本身不能直接保存元素,它保存元素的機制是調用另一種順序容器去實現。
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 容器上。


a)容器適配器通用的操作和類型:
size_type 一種類型,存儲此適配器類型最大對象的長度;
value_type 元素類型;
container_type 基礎容器的類型;
A a;創建一個新的容器適配器;
A a(c);創建一個新的容器適配器a,初始化為c的副本;
關系操作符:==,!=,<,<=,>,>=。

b)適配器的初始化:
stack<int> stk();
stack<int> stk2(stk);

c)覆蓋基礎容器類型
stack<int,vector<int> > stk;

d)棧適配器的操作:
s.empty();返回棧是否為空;
s.size();返回棧中的元素個數;
s.pop();刪除棧頂元素,返回void;
s.top();返回棧頂元素,但不刪除該元素;
s.push(item);在棧頂壓入新元素;

e)隊列和優先級隊列的操作:
q.empty();返回隊列是否為空;
q.size();返回隊列中的元素個數;
q.pop();刪除隊首元素,返回void;
q.front();返回隊首元素,但不刪除元素,只適用於隊列;
q.back();返回隊尾元素,但不刪除元素,只適用於隊列;
q.top();返回具有最高優先級的元素,但不刪除該元素,只適用於優先級隊列;
q.push(item);對於queue,在隊尾壓入一個元素;對於priority_queue,在基於優先級的適當位置插入一個元素。

--------------------------------------------------------------------------------------------------------------------------

2.叠代器

  叠代器提供對一個容器中的對象的訪問方法。叠代器就如同一個指針。

  事實上,C++的指針也是一種叠代器。但是叠代器不僅僅是指針。
  除了使用下標來訪問 vector 對象的元素外,標準庫還提供了另一種訪問元素的方法:使用叠代器。

  叠代器是一種檢查容器內元素並遍歷元素的數據類型。
  標準庫為每一種標準容器(包括 vector)定義了一種叠代器類型。叠代器類型提供了比下標操作更通用化的方法:所有的標準庫容器都定義了相應的叠代器類型,而只有少數的容器支持下標操作。因為叠代器對所有的容器都適用,現代 C++ 程序更傾向於使用叠代器而不是下標操作訪問容器元素,即使對支持下標操作的 vector 類型也是這樣。

iterator 類型
每個標準庫容器類型都定義了一個名為 iterator 的成員,用來定義自己的叠代器。如:
vector<int>::iterator iter;
list<string>::iterator iter2;
begin 和 end 操作
每種容器都定義了一對命名為 begin 和 end 的函數,用於返回叠代器。如果容器中有元素的話,由 begin 返回的叠代器指向第一個元素:
vector<int>::iterator iter = ivec.begin();
把 iter 初始化為由名為 vector 操作返回的值。假設 vector 不空,初始化後,iter 即指該元素為 ivec[0]。
vector<int>::iterator iter2 = ivec.end()
由 end 操作返回的叠代器指向 vector 的末端元素的下一個,被稱為超出末端叠代器。


叠代器的自增和解引用運算
叠代器類型定義了一些操作來獲取叠代器所指向的元素,叠代器類型可使用解引用操作符(*)來訪問叠代器所指向的元素:
*iter = 0;

叠代器支持的其他方法
比較:用 == 或 != 操作符來比較兩個叠代器,如果兩個叠代器對象指向同一個元素,則它們相等,否則就不相等。

使用叠代器遍歷容器:
for (vector<string>::iterator iter = ivec.begin(); iter != ivec.end(); ++iter)
{
std::cout<<*iter<<std::endl;
}

const_iterator
每種容器類型還定義了一種名為 const_iterator 的類型,該類型只能用於讀取容器內元素,但不能改變其值。
當我們對普通 iterator 類型解引用時,得到對某個元素的非 const。而如果我們對 const_iterator 類型解引用時,則可以得到一個指向 const 對象的引用,如同任何常量一樣,該對象不能進行重寫。
除了是從叠代器讀取元素值而不是對它進行賦值之外,這個循環與前一個相似。由於這裏只需要借助叠代器進行讀,不需要寫,這裏把 iter 定義為 const_iterator 類型。當對 const_iterator 類型解引用時,返回的是一個 const 值。不允許用 const_iterator: 進行賦值
使用 const_iterator 類型時,我們可以得到一個叠代器,它自身的值可以改變,但不能用來改變其所指向的元素的值。可以對叠代器進行自增以及使用解引用操作符來讀取值,但不能對該元素賦值。

區分const_iterator 與const的iterator
不要把 const_iterator 對象與 const 的 iterator 對象混淆起來。聲明一個 const 叠代器時,必須初始化叠代器。一旦被初始化後,就不能改變它的值:

vector<int> nums(10);
const vector<int>::iterator cit = nums.begin();
*cit = 1;
++cit;
因為不能改寫元素值。const 叠代器這種類型幾乎沒什麽用處:一旦它被初始化後,只能用它來改寫其指向的元素,但不能使它指向任何其他元素。

叠代器的算術操作
除了一次移動叠代器的一個元素的增量操作符外,vector 叠代器(list不支持隨機訪問所以不支持這些算數操作)也支持其他的算術操作。,包括:

iter + n
iter - n
iter1 - iter2
vector<int>::iterator mid = vi.begin() + vi.size() / 2;

叠代器失效
任何改變 vector 長度的操作都會使已存在的叠代器失效。
例如,在調用 push_back 之後,就不能再信賴指向 vector 的叠代器的值了。因為它們可能已經失效了,這時候我們需要重新為叠代器賦值。

for(iter = v.begin(); iter != v.end(); iter++)
{
if(*iter == 20)
{
v.push_back(30);
iter = v.begin();
}
}

--------------------------------------------------------------------------------------------------------------------------

3.算法

容器中提供的方法是有限的,因此STL提供了算法來對容器進行各種操作。

這些算法是獨立於任何容器的,可以適用於不同類型的容器和不同類型的元素。
順序容器只提供了很少的操作,比如增加刪除、訪問元素,獲得第一個元素或結束為止之後的叠代器。
但是不支持比如查找特定元素、替換和刪除一個特定值,排序等。
為此STL中定義了一組泛型算法。

所謂泛型就是指他們可以用於不同類型的元素和多種容器類型。他們實現了一些經典算法的公共接口,比如排序和搜索。

大部分的算法都定義在algorithm頭文件中,numeric文件中定義了一組處理數值的泛型算法。
這些算法並不直接操作容器,而是通過叠代器來進行操作。

所以前面我們說叠代器是算法和容器的橋梁。

通常情況下算法會遍歷叠代器範圍內的元素,對每個元素進行處理。
假如有個int的vector,需要知道vector是否包含一個特定的值。這時可以調用標準庫算法find。
vector<int>::iterator iter = find(vec.begin(), vec.end(), 20);
if(iter != vec.end())
{
//搜索成功!
}

傳遞給find的前兩個參數表示元素範圍的叠代器,第三個參數是用來搜索的值。
如果不存在這個值find會返回第二個叠代器。[)
可以使用find在任何容器中查找。因為指針是一種特殊的叠代器,所以也可以在find上使用指針。
int array[6]={20,339,398,0, 92,930};
int* p = find(array, array + 6, 0);
if(p != array + 6)
{
//搜索成功
}

由於find不依賴於容器,僅僅通過叠代器訪問容器中的元素,所以才會如此通用。
但是算法依賴於元素提供的一些操作。比如find,他需要依賴於元素提供的==運算符,完成每個元素與指定值的比較。其他算法可能需要元素類型支持<運算符。

標準庫提供了超過100個算法。這些算法有一致的結構,這可以幫助我們更容易的學習和使用這些算法。這些算法都對一個叠代器範圍內的元素進行操作。這個範圍是一個左閉右開的區間。

只讀算法
一些算法只會讀取某個範圍的元素,不會改變元素。find就是這樣的算法。
另一個是accumulate算法,用於對叠代器範圍求和。第三個參數是和的初值。
int sum = accumulate(vec.begin(), vec.end(), 0);
第三個參數作為初始值,這裏有一個假設:元素類型必須與第三個參數匹配,必須支持元素類型與第三個參數類型相加。

比如string類型支持+運算。可以使用accumulate將容器中的string元素連接起來:
string sum = accumulate(vec.begin(), vec.end(), string(""));
需要註意第三個參數不能寫成“”,因為const char*上沒有定義+運算符。

修改容器元素的算法
一些算法會修改容器中元素的值。但算法永遠不會修改容器的長度。
比如fill算法用來填充叠代器指定的範圍的值。
fill(vec.begin(), vec.end(), 0);
將所有元素置0,。

重排容器元素的算法
某些算法會改變容器中元素的順序。比如sort,它會重新排序容器中的元素,使用<運算符來實現的。
sort接收兩個叠代器,表示要排序的元素範圍。
sort(vec.begin(), vec.end());

向算法傳遞函數
謂詞
謂詞是一個可調用的表達式,它返回的結果是可以被用作條件的值。標準庫使用謂詞分為兩類:一元謂詞和二元謂詞。
一元謂詞只接受一個參數,二元謂詞接收兩個參數。
接收謂詞參數的算法會對容器中的元素調用謂詞,元素類型必須能夠轉換為謂詞參數的類型。
還繼續看sort方法的特殊版本,該版本接收一個二元謂詞,用這個謂詞代替<來對元素進行比較。
下面是一個二元謂詞:
bool isShorter(int a, int b)
{
return a < b;
}

sort(vec.begin(), vec.end(), isShorter);
count_if用來統計滿足謂詞的元素個數:

bool operate(int a)
{
return a < 60;
}

int nCount = count_if(vec.begin(), vec.end(), operate);

算法的一般形式
除了參數之外,算法還有自己的一套命名和重載規範。
一些算法使用重載形式來傳遞一個謂詞, unique用來重新整理給定的序列,將相鄰重復元素刪除。:

unique(vec.begin(), vec.end();//使用==比較元素。
unique(vec.begin(), vec.end(), compare);//使用compare比較元素

*_if版本算法
接受一個值的算法通常有另一個不同名的版本,該版本接受一個謂詞代替元素的值。
find(beg, end, val);
find_if(beg, end, 謂詞)

--------------------------------------------------------------------------------------------------------------------------

C++筆記(6):標準模板庫STL:容器、叠代器和算法