- 大多數算法都定義在頭文件algorithm中。標準庫還在頭文件numeric中定義了 一組數值泛型算法
- 一般情況下,這些算法并不直接操作容器,而是遍歷由兩個迭代器指定的一個元素范圍(參見9.2.1節,第296頁)來進行操作。通常情況下,算法遍歷范圍,對其中每個元素進行一些處理。例如,假定我們有一個int的vector,希望知道vector中是否包含一個特定值。回答這個問題最方便的方法是調用標準庫算法find
- 傳遞給find的前兩個參數是表示元素范圍的迭代器,第三個參數是一個值。find將范圍中每個元素與給定值進行比較。它返回指向第一個等于給定值的元素的迭代器。如果范圍中無匹配元素,則 find返回第二個參數來表示搜索失敗。因此,我們可以通過比較返回值和第二個參數來判斷搜索是否成功。我們在輸出語句中執行這個檢測,其中使用了條件運算符(參見4.7節,第 134頁)來報告搜索是否成功。
- 由于find操作的是迭代器,因此我們可以用同樣的find函數在任何容器中查找值。 例如,可以用find在一個string的 list中查找一個給定值:
?
- 此例中我們使用了標準庫begin和 end函數(參見3.5.3節,第 106頁)來獲得指向ia 中首元素和尾元素之后位置的指針,并傳遞給find。還可以在序列的子范圍中查找,只需將指向子范圍首元素和尾元素之后位置的迭代器指針)傳遞給find。例如,下面的語句在ia[l]、?ia[2]和 ia[3]中查找給定元素:
- / / 在 從 ia[l]開始,直 至 (但不包含)ia[4]的范圍內查找元素? auto result = find(ia + 1, ia + 4, val);
算法如何工作
- 為了弄清這些算法如何用于不同類型的容器,讓我們更近地觀察一下find。find的工作是在一個未排序的元素序列中查找一個特定元素。概念上,find應執行如下步驟:
- 1.訪問序列中的首元素。
- 2.比較此元素與我們要查找的值。
- 3.如果此元素與我們要查找的值匹配,find返回標識此元素的值。
- 4.否則,find前進到下一個元素,重復執行步驟2和3。
- 5.如果到達序列尾,find應停止。
- 6.如果find到達序列末尾,它應該返回一個指出元素未找到的值。此值和步驟3返回的值必須具有相容的類型。
- 這些步驟都不依賴于容器所保存的元素類型。因此,只要有一個迭代器可用來訪問元素,find就完全不依賴于容器類型(甚至無須理會保存元素的是不是容器)。
迭代器令算法不依賴于容器,……
- 在上述find函數流程中,除了第2步外,其他步驟都可以用迭代器操作來實現:利用迭代器解引用運算符可以實現元素訪問;如果發現匹配元素,find可以返回指向該元素的迭代器;用迭代器遞增運算符可以移動到下一個元素;尾后迭代器可以用來判斷find是否到達給定序列的末尾;find可以返回尾后迭代器(參見9.2.1節,第296頁)來表示未找到給定元素。
……,但算法依賴于元素類型的操作
- 雖然迭代器的使用令算法不依賴于容器類型,但大多數算法都使用了一個(或多個)元素類型上的操作。例如,在步驟2中,find用元素類型的==運算符完成每個元素與給定值的比較。其他算法可能要求元素類型支持<運算符。不過,我們將會看到,大多數算法提供了一種方法,允許我們使用自定義的操作來代替默認的運算符。
?
關鍵概念:算法水遠不會執行容器的操作
- 泛型算法本身不會執行容器的操作,它們只會運行于迭代器之上,執行迭代器的操作。泛型算法運行于迭代器之上而不會執行容器操作的特性帶來了一個令人驚訝但非常必要的編程假定:算法永遠不會改變底層容器的大小。算法可能改變容器中保存的元的值,也可能在容器內移動元素,但永遠不會直接添加或刪除元素。
- 如我們將在10.4.1節(第358頁)所看到的,標準庫定義了一類特殊的迭代器,稱為插入器(inserter).與普通迭代器只能遍歷所綁定的容器相比,插入器能做更多的事情。當給這類迭代器賦值時,它們會在底層的容器上執行插入操作。因此,當一個算法操作一個這樣的迭代器時,迭代器可以完成向容器添加元素的效果,但算法自身永遠不會做這樣的操作
10.2初識泛型算法
- 標準庫提供了超過100個算法。幸運的是,與容器類似,這些算法有一致的結構。比起死記硬背全部100多個算法,理解此結構可以幫助我們更容易地學習和使用這些算法。在本章中,我們將展示如何使用這些算法,并介紹刻畫了這些算法的統一原則。附錄A按操作方式列出了所有算法。
- 除了少數例外,標準庫算法都對一個范圍內的元素進行操作。我們將此元素范圍稱為“輸入范圍”。接受輸入范圍的算法總是使用前兩個參數來表示此范圍,兩個參數分別是指向要處理的第一個元素和尾元素之后位置的迭代器。
- 雖然大多數算法遍歷輸入范圍的方式相似,但它們使用范圍中元素的方式不同。理解算法的最基本的方法就是了解它們是否讀取元素、改變元素或是重排元素順序。
10.2.1只讀算法
- 一些算法只會讀取其輸入范圍內的元素,而從不改變元素。find就是這樣一種算法,我們在10.1節練習(第337頁)中使用的count函數也是如此。另一個只讀算法是accumulate,它定義在頭文件numeric中。accumulate函數接受三個參數,前兩個指出了需要求和的元素的范圍,第三個參數是和的初值。假定vec是一個整數序列,則:
- intsum=accumulate(vec.cbegin(),vec.cend(),0);? ??//對vec中的元素求和,和的初值是0。這條語句將sum設置為vec中元素的和,和的初值被設置為0。
- accumulate的第三個參數的類型決定了函數中使用哪個加法運算符以及返 回值的類型。
算法和元素類型
- accumulate將第三個參數作為求和起點,這蘊含著一個編程假定:將元素類型加到和的類型上的操作必須是可行的。即,序列中元素的類型必須與第三個參數匹配,或者能夠轉換為第三個參數的類型。在上例中,vec中的元素可以是int,或者是double, long long或任何其他可以加到int上的類型。 下面是另一個例子,由 于 string定義了+運算符,所以我們可以通過調用 accumulate來將vector中所有string元素連接起來:
- string sum = accumulate (v . cbegin () , v . cend () , string (“”));
- 此調用將v中每個元素連接到一個string上,該string初始時為空串。注意,我們通過第三個參數顯式地創建了一個string。將空串當做一個字符串字面值傳遞給第三個參數是不可以的,會導致一個編譯錯誤。
- stringsum=accumulate(v.cbegin(),v.cend(),"**);? ?//錯誤:const char*上沒有定義+運算符
- 原因在于,如果我們傳遞了一個字符串字面值,用于保存和的對象的類型將是const char*。如前所述,此類型決定了使用哪個+運算符。由于const char*并沒有+運算符,此調用將產生編譯錯誤。
- 對于只讀取而不改變元素的算法,通常最好使用cbegin () 和 cend ( ) (參見 'Pm^ 9.2.3節,第298頁 ) 但是,如果你計劃使用算法返回的迭代器來改變元素的值,就需要使用begin ()和 e n d ()的結果作為參數
操作兩個序列的算法
- 另一個只讀算法是equal,用于確定兩個序列是否保存相同的值。它將第一個序列中的每個元素與第二個序列中的對應元素進行比較。如果所有對應元素都相等,則返回true,否則返回false。此算法接受三個迭代器:前兩個(與以往一樣)表示第一個序列中的元素范圍,第三個表示第二個序列的首元素:
- equal(rosterl.cbegin(),rosterl.cend(),roster2.cbegin());? ?//roster2中的元素數目應該至少與rosterl-樣多
- 由于equal利用迭代器完成操作,因此我們可以通過調用equal來比較兩個不同類型的容器中的元素。而且,元素類型也不必一樣,只要我們能用==來比較兩個元素類型即可。
- 例如,在此例中,rosterl可以是vector<string>,而roster2是list<const char*>
- 但是.equal基于一個非常重要的假設:它假定第二個序列至少與第一個序列一樣長。此算法要處理第一個序列中的每個元素,它假定每個元素在第二個序列中都有一個與之對應的元素。
- 那些只接受一個單一迭代器來表示第二個序列的算法,都假定第二個序列至少與第一個序列一樣長
10.2.2寫容器元素的算法
- -些算法將新值賦予序列中的元素。當我們使用這類算法時,必須注意確保序列原大小至少不小于我們要求算法寫入的元素數目。記住,算法不會執行容器操作,因此它們自身不可能改變容器的大小。
- 一些算法會自己向輸入范圍寫入元素。這些算法本質上并不危險,它們最多寫入與給定序列一樣多的元素。
- 例如,算法fill接受一對迭代器表示一個范圍,還接受一個值作為第三個參數。fill 將給定的這個值賦予輸入序列中的每個元素。
- fill (vec.begin () , vec.end() , 0) ; // 將每個元素重置為 0
- fill(vec.begin(), vec.begin() + vec.size()/2, 10);? ?/ / 將容器的一個子序列設置為10
- 由于fill向給定輸入序列中寫入數據,因此,只要我們傳遞了一個有效的輸入序列,寫 入操作就是安全的
關鍵概念:迭代器參數
- 一些算法從兩個序列中讀取元素“構成這兩個序列的元素可以來自于不同類型的容器。例如,第一個序列可能保存于一個vector中,而第二個序列可能保存于一個list,deque、內置數組或其他容器中。而且,兩個序列中元素的類型也不要求嚴格匹配。算法要求的只是能夠比較兩個序列中的元素,例如,對equal算法,元素類型不要求相同,但是我們必須能使用==來比較來自兩個序列中的元素...
- 操作兩個序列的算法之間的區別在于我們如何傳遞第二個序列。一些算法,例如equal,接受三個迭代器:前兩個表示第一個序列的范圍,第三個表示第二個序列中的首元素其他算法接受四個迭代器:前兩個表示第一個序列的元素范圍,后兩個表示第二個序列的范圍。
- 用一個單一迭代器表示第二個序列的算法都假定第二個序列至少與第一個一樣長。
- 確保算法不會試圖訪問第二個序列中不存在的元素是程序員的責任。例如,算法equal會將其第一個序列中的每個元素與第二個序列中的對應元素進行比較。如果第二個序列是第一個序列的一個子集,則程序會產生一個嚴重錯誤--equal會試圖訪問第二個序列中末尾之后(不存在)的元素
算法不檢查寫操作
- 一些算法接受一個迭代器來指出一個單獨的目的位置。這些算法將新值賦予一個序列中的元素,該序列從目的位置迭代器指向的元素開始。例如,函數fill_n接受一個單迭代器、一個計數值和一個值。它將給定值賦予迭代器指向的元素開始的指定個元素。我們可以用fill_n將一個新值賦予vector中的元素:
- vector<int> vec; // 空 vector? ? ?/ / 使用vec,賦予它不同值
- fill_n (vec .begin () , vec. size () , 0) ; // 將所有元素重置為 0
- 函數fill_n假定寫入指定個元素是安全的。即,如下形式的調用
- fill_n(dest, n, val)? ?//fill_n假定dest指向一個元素,而從dest開始的序列至少包含n 個元素
- 一個初學者非常容易犯的錯誤是在一個空容器上調用(或類似的寫元素的算法):
- vector<int> vec; // 空向量? ? ? ? ? ?
- (vec.begin(), 10, 0);? ?? ?/ / 災難:修 改 vec中的10個 (不存在) 元素,這個調用是一場災難。我們指定了要寫入10個元素,但vec中并沒有元素,它是空的。 這條語句的結果是未定義的。
- 向目的位置迭代器寫入數據的算法假定目的位置足夠大,能容納要寫入的元素?
介紹backjnserter
- 一種保證算法有足夠元素空間來容納輸出數據的方法是使用插入迭代器(insertiterator).插入迭代器是一種向容器中添加元素的迭代器。通常情況,當我們通過一個迭代器向容器元素賦值時,值被賦予迭代器指向的元素。而當我們通過一個插入迭代器賦值時,一個與賦值號右側值相等的元素被添加到容器中。
- 我們將在10.4.1節中(第358頁)詳細介紹插入迭代器的內容。但是,為了展示如何用算法向容器寫入數據,我們現在將使用back_inserter,它是定義在頭文件iterator中的一個函數
- back_inserter接受一個指向容器的引用,返回一個與該容器綁定的插入迭代器。當我們通過此迭代器賦值時,賦值運算符會調用push_back將一個具有給定值的元素添加到容器中:
- vector<int>vec;//空向量? auto it=back_inserter(vec);//通過它賦值會將元素添加到vec中*it=42;//vec中現在有一個元素,值為42
- 我們常常使用back_inserter來創建一個迭代器,作為算法的目的位置來使用。例如:vector<int>vec;//空向量? ?//正確:back_inserter創建一個插入迭代器,可用來向vec添加元素
- fill_n(back_inserter(vec),10,0);//添加10個元素到vec
- 在每步迭代中,向給定序列的一個元素賦值。由于我們傳遞的參數是back_inserter返回的迭代器,因此每次賦值都會在vec上調用push_back。最終,這條fill_n調用語句向vec的末尾添加了10個元素,每個元素的值都是0.
拷貝算法
- 拷貝(copy)算法是另一個向目的位置迭代器指向的輸出序列中的元素寫入數據的算法。此算法接受三個迭代器,前兩個表示一個輸入范圍,第三個表示目的序列的起始位置。此算法將輸入范圍中的元素拷貝到目的序列中。傳遞給copy的目的序列至少要包含與輸入序列一樣多的元素,這一點很重要。
- 我們可以用copy實現內置數組的拷貝,如下面代碼所示:
- intal[]={0,1,2,3,4,5,6,7,8,9};
- inta2[sizeof(al)/sizeof(*al)];//a2與al大小一樣
- //ret指向拷貝到a2的尾元素之后的位置
- auto ret=copy(begin(al),end(al),a2);//把al的內容拷貝給a2
- 此例中我們定義了一個名為a2的數組,并使用sizeof確保a2與數組al包含同樣多的元素(參見4.9節,第 139頁)。接下來我們調用copy完成從al到 a2的拷貝。在調用 copy后,兩個數組中的元素具有相同的值。 copy返回的是其目的位置迭代器(遞增后)的值。即,ret恰好指向拷貝到a2的 尾元素之后的位置。
- 多個算法都提供所謂的“拷貝”版本。這些算法計算新元素的值,但不會將它們放置在輸入序列的末尾,而是創建一個新序列保存這些結果。
- 例如,replace算法讀入一個序列,并將其中所有等于給定值的元素都改為另一個值。此算法接受4個參數:前兩個是迭代器,表示輸入序列,后兩個一個是要搜索的值,另一個是新值。它將所有等于第一個值的元素替換為第二個值:
- replace (i1st.begin(), ilst.end(), 0, 42);?/ / 將所有值為0 的元素改為42
- 此調用將序列中所有的0 都替換為42。如果我們希望保留原序列不變,可以調用replace_copy此算法接受額外第三個迭代器參數,指出調整后序列的保存位置:
- replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);??/ / 使 用 back_inserter按需要增長目標序列
此調用后,ilst并未改變,ivec包含ilst的一份拷貝,不過原來在ilst中值為0 的 元素在ivec中都變為42。
1 0 .2 .3 重排容器元素的算法
- 某些算法會重排容器中元素的順序,一個明顯的例子是sort。調用sort會重排輸 入序列中的元素,使之有序,它是利用元素類型的〈運算符來實現排序的。
- 例如,假定我們想分析一系列兒童故事中所用的詞匯。假定已有一個vector,保存了多個故事的文本。我們希望化簡這個vector,使得每個單詞只出現一次,而不管單詞 在任意給定文檔中到底出現了多少次
- 為了便于說明問題,我們將使用下面簡單的故事作為輸入:
消除重復單詞?
- 為了消除重復單詞,首先將vector排序,使得重復的單詞都相鄰出現。一旦vector 排序完畢,我們就可以使用另一個稱為unique的標準庫算法來重排vector,使得不重復的元素出現在vector的開始部分。由于算法不能執行容器的操作,我們將使用vector 的erase成員來完成真正的刪除操作:
- 節,第 311頁)。我們刪除從end_unique開始直至words末尾的范圍內的所有元素。 這個調用之后,words包含來自輸入的8 個不重復的單詞。 值得注意的是,即使words中沒有重復單詞,這樣調用era se也是安全的。在此情況下,unique會返回words.end () 因此,傳遞給erase 的兩個參數具有相同的值:
- words.end迭代器相等意味著傳遞給e ra se 的元素范圍為空。刪除一個空范圍沒有什么不良后果,因此程序即使在輸入中無重復元素的情況下也是正確的。