《C++Primer》第十一章-泛型算法-學習筆記(3)-泛型算法的結構
日誌:
1,2020-03-16 筆者提交文章的初版V1.0
作者按:
最近在學習C++ primer,初步打算把所學的記錄下來。
傳送門/推廣
《C++Primer》第二章-變量和基本類型-學習筆記(1)
《C++Primer》第三章-標準庫類型-學習筆記(1)
《C++Primer》第八章-標準 IO 庫-學習筆記(1)
《C++Primer》第十二章-類-學習筆記(1)
泛型算法的結構
正如所有的容器都建立在一致的設計模式上一樣,算法也具有共同的設計基礎。理解標準算法庫的設計基礎有利於學習和使用算法。C++ 提供了超過一百個算法,瞭解它們的結構顯然要比死記所有的算法更好。
算法最基本的性質
是需要使用的迭代器種類
。所有算法都指定了它的每個迭代器形參
可使用的迭代器類型。如果形參必須爲隨機訪問迭代器
則可提供vector 或 deque 類型的迭代器,或者提供指向數組的指針。而其他容器的迭代器不能用在這類算法上。
另一種算法分類的方法,則如本章開頭介紹的一樣,根據對元素的操作將算法分爲下面幾種:
- 只讀算法,不改變元素的值順序。
- 給指定元素賦新值的算法。
- 將一個元素的值移給另一個元素的算法。
正如本節後續部分所介紹的,C++ 還提供了另外兩種算法模式:一種模式由算法所帶的形參定義;另一種模式則通過兩種函數命名和重載的規範定義。
算法的形參模式
任何其他的算法分類都含有一組形參規範。理解這些形參規範有利於學習新的算法——只要知道形參的含義,就可專注於瞭解算法實現的操作。大多數算法採用下面四種形式之一:
alg (beg, end, other parms);
alg (beg, end, dest, other parms);
alg (beg, end, beg2, other parms);
alg (beg, end, beg2, end2, other parms);
其中,alg 是算法的名字,beg 和 end 指定算法操作的元素範圍。我們通常將該範圍稱爲算法的“輸入範圍”
。儘管幾乎所有算法都有輸入範圍,但算法是否使用其他形參取決於它所執行的操作。這裏列出了比較常用的其他形參:dest、beg2 和 end2,它們都是迭代器。這些迭代器在使用時,充當類似的角色。除了這些迭代器形參之外,有些算法還帶有其他的菲迭代器形參,它們是這些算法特有的。
帶有單個目標迭代器的算法
dest 形參
是一個迭代器
,用於指定存儲輸出數據的目標對象。算法假定無論需要寫入多少個元素都是安全的。
調用這些算法時,必須確保輸出容器有足夠大的容量存儲輸出數據,這正是通常要使用插入迭代器或者 ostream_iterator來調用這些算法的原因。如果使用容器迭代器調用這些算法, 算法將假定容器裏有足夠多個需要的元素。
如果 dest 是容器上的迭代器,則算法將輸出內容寫到容器中已存在的元素上。更普遍的用法是,將 dest 與某個插入迭代器(第 11.3.1 節)或者ostream_iterator 綁定在一起。插入迭代器在容器中添加元素,以確保容器有足夠的空間存儲輸出。ostream_iterator
則實現寫輸出流的功能
,無需要考慮所寫的元素個數。
帶第二個輸入序列的算法
有一些算法帶有一個 beg2 迭代器形參,或者同時帶有 beg2 和 end2 迭代器形參,來指定它的第二個輸入範圍。這類算法通常將聯合兩個輸入範圍的元素來完成計算功能。 算法同時使用 beg2 和 end2 時,這些迭代器用於標記完整的第二個範圍。也就是說,此時,算法完整地指定了兩個範圍
:beg 和 end 標記第一個輸入範圍,而 beg2 和 end2 則標記第二個輸入範圍。
帶有 beg2 而不帶 end2 的算法將 beg2 視爲第二個輸入範圍的首元素,但沒有指定該範圍的最後一個元素。這些算法假定以 beg2 開始的範圍至少與 beg和 end 指定的範圍一樣大。
與寫入 dest 的算法一樣,只帶有 beg2 的算法也假定以 beg2開始的序列與 beg 和 end 標記的序列一樣大。
算法的命名規範
標準庫使用一組相同的命名和重載規範, 瞭解這些規範有助於更容易地學習標準庫。它們包括兩種重要模式:第一種模式包括測試輸入範圍內元素的算法
,第二種模式則應用於對輸入範圍內元素重新排序的算法
。
區別帶有一個值或一個謂詞函數參數的算法版本很多算法通過檢查其輸入範圍內的元素實現其功能。 這些算法通常要用到標準關係操作符:== 或 <。其中的大部分算法會提供第二個版本的函數,允許程序員提供比較或測試函數取代操作符的使用。
重新對容器元素排序的算法要使用 < 操作符。這些算法的第二個重載版本帶有一個額外的形參,表示用於元素排序的不同運算:
sort (beg, end); // use < operator to sort the elements
sort (beg, end, comp); // use function named comp to sort the elements
檢查指定值的算法默認使用 == 操作符。系統爲這類算法提供另外命名的(而非重載的)版本,帶有謂詞函數(第 11.2.3 節)形參。帶有謂詞函數形參的算法
,其名字帶有後綴 _if:
find(beg, end, val); // find first instance of val in the input range
find_if(beg, end, pred); // find first instance for which pred is true
//find 算法查找一個指定的值,而 find_if 算法則用於查找一個使謂詞函數 pred 返回非零值的元素。
上述兩個算法都在輸入範圍內尋找指定元素的第一個實例。其中,find 算法查找一個指定的值,而 find_if 算法則用於查找一個使謂詞函數 pred 返回非零值的元素。
標準庫爲這些算法提供另外命名的版本,而非重載版本,其原因在於這個兩種版本的算法帶有相同數目的形參。對於排序算法,只要根據參數的個數就很容易消除函數調用的歧義。而對於查找指定元素的算法,不管檢查的是一個值還是謂詞函數,函數調用都需要相同個數的參數。此時,如果使用重載版本,則可能導致二義性(第 7.8.2 節),儘管這個可能出現的機率很低。因此,標準庫爲這些算法提供兩種不同名字的版本,而沒有使用重載。
區別是否實現複製的算法版本
無論算法是否檢查它的元素值,都可能重新排列輸入範圍內的元素。在默認情況下,這些算法將重新排列的元素寫回其輸入範圍。標準庫也爲這些算法提供另外命名的版本,將元素寫到指定的輸出目標。此版本的算法在名字中添加了_copy 後綴:
reverse(beg, end); //寫回到輸入
reverse_copy(beg, end, dest);//寫到dest中
reverse 函數的功能就如它的名字所意味的:將輸入序列中的元素反射重新排列。其中,第一個函數版本將自己的輸入序列中的元素反向重排。而第二個版本,reverse_copy,則複製輸入序列的元素,並將它們逆序存儲到 dest 開始的序列中。
容器特有的算法
list 容器上的迭代器是雙向的,而不是隨機訪問類型。由於 list 容器不支持隨機訪問,因此,在list容器上不能使用需要隨機訪問迭代器的算法。這些算法包括 sort 及其相關的算法。 還有一些其他的泛型算法,如 merge、remove、reverse 和 unique,雖然可以用在 list 上,但卻付出了性能上的代價。如果這些算法利用 list 容器實現的特點,則可以更高效地執行。
如果可以結合利用 list 容器的內部結構,則可能編寫出更快的算法。 與其他順序容器所支持的操作相比,標準庫爲 list 容器定義了更精細的操作集合,使它不必只依賴於泛型操作。 表 11.4 列出了 list 容器特有的操作,其中不包括要求支持雙向或更弱的迭代器類型的泛型算法,這類泛型算法無論是用在list 容器上,還是用在其他容器上,都具有相同的效果。
表 11.4. list 容器特有的操作
list 容器特有的操作 | 作用 |
---|---|
lst.merge(lst2) lst.merge(lst2, comp) | 將lst2 的元素合併到 lst 中。這兩個 list 容器對象都必須排序。lst2 中的元素將被刪除。合併後,lst2 爲空。返回 void 類型。第一個版本使用 < 操作符,而第二個版本則使用 comp 指定的比較運算 |
lst.remove(val) lst.remove_if(unaryPred) | 調用lst.erase 刪除所有等於指定值或使指定的謂詞函數返回非零值的元素。 返回 void 類型 |
lst.reverse() | 反向排列 lst 中的元素 |
lst.sort | 對 lst 中的元素排序 |
lst.splice(iter, lst2) | 將lst2 的元素移到 lst 中迭代器 iter 指向的元素前面 。在 lst2 中刪除移出的元素。第一個版本將 lst2 的所有元素移到 lst 中;合併後,lst2 爲空。lst 和 lst2 不能是同一個 list 對象。 |
lst.splice(iter, lst2, iter2) | 將lst2 的元素移到 lst 中迭代器 iter 指向的元素前面 。在 lst2 中刪除移出的元素。lst.splice的第二個版本,只移動 iter2 所指向的元素,這個元素必須是 lst2 中的元素。 在這種情況中,lst 和lst2 可以是同一個 list 對象。也就是說,可在一個 list對象中使用 splice 運算移動一個元素。 |
lst.splice(iter, beg, end) | 將lst2 的元素移到 lst 中迭代器 iter 指向的元素前面 。在 lst2 中刪除移出的元素。lst.splice的第三個版本,移動迭代器 beg 和 end 標記的範圍內的元素。beg 和 end 照例必須指定一個有效的範圍。這兩個迭代器可標記任意 list 對象內的範圍,包括 lst。 當它們指定 lst 的一段範圍時,如果 iter 也指向這個範圍的一個元素,則該運算未定義。 |
lst.unique() lst.unique(binaryPred) | 調用 erase 刪除同一個值的團結副本 。第一個版本使用 ==操作符判斷元素是否相等;第二個版本則使用指定的謂詞函數實現判斷 |
對於 list 對象,應該優先使用 list 容器特有的成員版本,而不是泛型算法。
大多數 list 容器特有的算法類似於其泛型形式中已經見過的相應的算法,但並不相同:
l.remove(val); // removes all instances of val from 1
l.remove_if(pred); // removes all instances for which pred is true from 1
l.reverse(); // reverses the order of elements in 1
l.sort(); // use element type < operator to compare elements
l.sort(comp); // use comp to compare elements
l.unique(); // uses element == to remove adjacent duplicates
l.unique(comp); // uses comp to remove duplicate adjacent copies
list 容器特有的算法與其泛型算法版本之間有兩個至關重要的差別。
- 其中一個差別是
remove
和unique
的 list 版本修改了其關聯的基礎容器:真正刪除了指定的元素。例如,list::unique 將 list 中第二個和後續重複的元素刪除出該容器。與對應的泛型算法不同,list 容器特有的操作能添加和刪除元素。 - 另一個差別是 list 容器提供的
merge
和splice
運算會破壞它們的實參。使用 merge 的泛型算法版本時,合併的序列將寫入目標迭代器指向的對象,而它的兩個輸入序列保持不變。但是,使用 list 容器的 merge 成員函數時,則會破壞它的實參 list 對象——當實參對象的元素合併到調用 merge 函數的list 對象時,實參對象的元素被移出並刪除。
小結
C++ 標準化過程做出的更重要的貢獻之一是:創建和擴展了標準庫。容器和算法庫是標準庫的基礎。 標準庫定義了超過一百個算法。幸運的是,這些算法具有相同的結構,使它們更易於學習和使用。
算法與類型無關:它們通常在一個元素序列上操作,這些元素可以存儲在標準庫容器類型、內置數組甚至是生成的序列(例如讀寫流所生成的序列)上。算法基於迭代器操作,從而實現類型無關性。大多數算法使用一對指定元素範圍的迭代器作爲其頭兩個實參。其他的迭代器實參包括指定輸出目標的輸出迭代器,或者用於指定第二個輸入序列的另一個或一對迭代器。
迭代器可通過其所支持的操作來分類。 標準庫定義了五種迭代器
類別:輸入、輸出、前向、雙向和隨機訪問迭代器
。如果一個迭代器支持某種迭代器類別要求的運算,則該迭代器屬於這個迭代器類別。
正如迭代器根據操作來分類一樣,算法的迭代器形參
也通過其所要求的迭代器操作來分類。只需要讀取其序列的算法通常只要求輸入迭代器的操作。而寫目標迭代器的算法則通常只要求輸出迭代器的操作,依此類推。
查找某個值的算法通常提供第二個版本,用於查找使謂詞函數
返回非零值的元素。對於這種算法,第二個版本的函數名字以_if 後綴標識
。類似地,很多算法提供所謂的複製版本
,將(修改過的)元素寫到輸出序列,而不是寫回輸入範圍。這種版本的名字以 _copy
結束。
第三種模式是考慮算法是是否對元素讀、寫或者重新排序。算法從不直接改變它所操縱的序列的大小。(如果算法的實參是插入迭代器,則該迭代器會添加新元素,但算法並不直接這麼做。)算法可以從一個位置將元素複製到另一個位置,但不直接添加或刪除元素。