泛型算法(lambda表達式、function類模板、bind函數適配器、迭代器類別、鏈表數據結構獨有的算法)

文章目錄

  • 概念
    • find()函數
    • 迭代器令算法不依賴于容器
    • 但算法依賴于元素類型的操作
    • 算法永遠不會執行容器的操作
  • 只讀算法
    • accumulate()函數
    • 從兩個序列中讀取元素(equal函數為例)
      • 迭代器作為參數形成兩個序列
    • equal()
  • 寫容器元素的算法
    • 概念
    • fill()
    • fill_n()
    • 插入迭代器back_inserter
    • 插入迭代器是否與“標準庫算法不會改變它們所操作的容器的大小”相悖
    • 拷貝(copy)算法
    • replace算法
    • 這里的容器大小指的是元素數量
  • 定制操作
    • 謂詞
    • stable
    • 可調用對象
    • lambda表達式
      • 概念
      • 原理
      • 捕獲列表
        • find_if
        • for_each
      • 捕獲方式
        • 值捕獲
          • mutable
        • 引用捕獲
          • 修改引用捕獲的變量
        • 隱式捕獲
    • function模板
      • 概念
      • function與重載函數
    • bind函數
      • 概念
      • 使用placeholders名字
      • 作用
        • 削減參數數量
        • 重排參數順序
      • 綁定引用參數
  • 泛型算法結構——迭代器類別
    • 概念
    • 不太重要的概念
    • 迭代器類別
      • 輸入迭代器
      • 輸出迭代器
      • 前向迭代器
      • 雙向迭代器
      • 隨機訪問迭代器
  • 算法的命名規范
    • _if版本的算法
    • _copy版本的算法
  • list和forward_list獨有的算法
    • 概念
    • 鏈表數據結構特有的splice算法
    • 多數鏈表特有的算法都與其通用版本很相似,但不完全相同。


概念

一般情況下,泛型算法本身不會執行容器的操作,它們只會運行于迭代器之上,執行迭代器的操作。

泛型算法的一大優點是 “泛型”,也就是一個算法可用于多種不同的數據類型,算法與所操作的數據結構分離。這對編程效率的提高是非常巨大的。

要做到算法與數據結構分離,重要的技術手段就是使用迭代器作為兩者的橋梁。算法從不操作具體的容器,從而也就不存在與特定容器綁定,不適用于其他容器的問題。算法只操作迭代器,由迭代器真正實現對容器的訪問。不同容器實現自己特定的迭代器(但不同迭代器是相容的),算法操作不同迭代器就實現了對不同容器的訪問。

因此,并不是算法應該改變或不該改變容器的問題。 為了實現與數據結構的分離,為了實現通用性,算法根本就不該知道容器的存在。 算法訪問數據的唯一通道是迭代器。是否改變容器大小,完全是迭代器的選擇和責任。

除了少數例外,標準庫算法都對一個范圍內的元素進行操作。我們將此元素范圍稱為“輸入范圍”。接受輸入范圍的算法總是使用前兩個參數來表示此范圍,兩個參數分別是指向要處理的第一個元素和尾元素之后位置的迭代器。


find()函數

以find()函數為例來理解。傳遞給find的前兩個參數是表示元素范圍的迭代器,第三個參數是一個值。find將范圍中每個元素與給定值進行比較。它返回指向第一個等于給定值的元素的迭代器。如果范圍中無匹配元素,則find返回第二個參數來表示搜索失敗。因此,我們可以通過比較返回值和第二個參數來判斷搜索是否成功。

在這里插入圖片描述

由于指針就像內置數組上的迭代器,因此我們也可以用find在數組中查找值:

在這里插入圖片描述


迭代器令算法不依賴于容器

在find函數流程中,除了比較大小外,其他步驟都可以用迭代器操作來實現:

  1. 利用迭代器解引用運算符可以實現元素訪問;
  2. 如果發現匹配元素,find可以返回指向該元素的迭代器;
  3. 用迭代器遞增運算符可以移動到下一個元素;
  4. 尾后迭代器可以用來判斷find是否到達給定序列的末尾;
  5. find可以返回尾后迭代器來表示未找到給定元素。

但算法依賴于元素類型的操作

雖然迭代器的使用令算法不依賴于容器類型,但大多數算法都使用了一個(或多個)元素類型上的操作。 例如,find用元素類型的==運算符完成每個元素與給定值的比較。不過,我們將會看到,大多數算法提供了一種方法,允許我們使用自定義的操作來代替默認的運算符。


算法永遠不會執行容器的操作

泛型算法運行于迭代器之上而不會執行容器操作的特性帶來了一個令人驚訝但非常必要的編程假定:算法永遠不會改變底層容器的大小。算法可能改變容器中保存的元素的值,也可能在容器內移動元素,但永遠不會直接添加或刪除元素。



只讀算法

一些算法只會讀取其輸入范圍內的元素,但不改變元素。
例如下面這三個:

  • find()
  • count():接收一對迭代器和一個值作為參數,返回給定值在序列中出現的次數。

accumulate()函數

定義在頭文件numeric中。接受三個參數,前兩個指出需要求和的元素的范圍,第三個參數時和的初值,其類型決定了函數中使用哪個加法運算符以及返回值的類型(這也就要求序列中元素的類型必須與第三個參數匹配,或者能夠轉換為第三個參數的類型)。

下面是另一個例子,由于string定義了+運算符,所以我們可以通過調用accumulate來將vector中所有string元素連接起來:

string sum = accumulate(v.cbegin(), v.cend(), string(""));

此調用將v中每個元素連接到一個string上,該string初始時為空串。注意,我們通過第三個參數顯式地創建了一個string。如果單純將空串當做一個字符串字面值傳遞給第三個參數是不可以的,會導致一個編譯錯誤。

// error:const char*上沒有定義+運算符
string sum =  accumulate(v.cbegin(), v.cend(), "");

原因在于,如果我們傳遞了一個字符串字面值,用于保存和的對象的類型將是const char*。如前所述,此類型決定了使用哪個+運算符。由于const char*并沒有+運算符,此調用將產生編譯錯誤。

對于只讀取而不改變元素的算法,通常最好使用cbegin()和cend()。但是,如果計劃使用算法返回的迭代器來改變元素的值,就需要使用begin()和end()的結果作為參數。


從兩個序列中讀取元素(equal函數為例)

迭代器作為參數形成兩個序列

一些算法從兩個序列中讀取元素。構成這兩個序列的元素可以來自于不同類型的容器。兩個序列中元素的類型也不要求嚴格匹配。算法要求的只是能夠比較兩個序列中的元素。

操作兩個序列的算法之間的區別在于我們如何傳遞第二個序列。 一些算法接受三個迭代器:前兩個表示第一個序列的范圍,第三個表示第二個序列中的首元素。 其他算法接受四個迭代器:前兩個表示第一個序列的元素范圍,后兩個表示第二個序列的范圍。

用一個單一迭代器表示第二個序列的算法都假定第二個序列至少與第一個一樣長。確保算法不會試圖訪問第二個序列中不存在的元素是程序員的責任。


equal()

equal():用于確定兩個序列是否保存相同的值。它將第一個序列中的每個元素與第二個序列中的對應元素進行比較。如果所有對應元素都相等,則返回true,否則返回false。 此算法接受三個迭代器:前兩個表示第一個序列中的元素范圍,第三個表示第二個序列的首元素(第二個序列中的元素數目應大于等于第一個序列)。

根據上面的概念,我們可以比較不同容器中不同的元素類型,只要兩者能通過“ == ” 進行比較即可,因此,兩個序列可以是vector和list<const char*>。

在這里插入圖片描述

在這里插入圖片描述

那些只接受一個單一迭代器來表示第二個序列的算法,都假定第二個序列至少與第一個序列一樣長。



寫容器元素的算法

概念

一些算法將新值賦予序列中的元素。 當我們使用這類算法時,必須注意確保序列原大小不小于我們要求算法寫入的元素數目。 記住,算法不會執行容器操作,因此它們自身不可能改變容器的大小。

一些算法會自己向輸入范圍寫入元素。這些算法本質上并不危險,它們最多寫入與給定序列一樣多的元素。

但是有一些算法接受一個迭代器和一個計數值來劃定范圍,然后寫入元素。這種算法假定目的位置足夠大,能容納要寫入的元素,也就是說算法不檢查寫操作。檢查目標輸入序列是否為空是程序員的責任,對空容器調用這些算法得到的結果是未定義的(但是可以通過back_insert(),來實現對空容器的操作)。

分別以 fill() 和 fill_n() 為例來探究:

fill()

接受一對迭代器表示一個范圍,還接受一個值作為第三個參數。將給定值賦予序列中的每個元素。

給定范圍有效:
在這里插入圖片描述
給定范圍越界:
在這里插入圖片描述
給定容器為空:
在這里插入圖片描述


fill_n()

fill_n接受一個單迭代器、一個計數值和一個給定值。他將給定值賦予迭代器指向的元素開始的計數值個元素。

給定范圍有效:
在這里插入圖片描述

給定范圍越界:
在這里插入圖片描述

給定容器為空:
在這里插入圖片描述


插入迭代器back_inserter

  • 是定義在頭文件iterator中的一個函數。
  • 是一種保證算法有足夠元素空間來容納輸出數據的方法
  • 是一種向容器中添加元素的迭代器。
  • 接受一個指向容器的引用,返回一個與該容器綁定的插入迭代器。
  • 當我們通過此迭代器賦值時,賦值運算符會調用push_back將一個具有給定值的元素添加到容器中。

實例:
在這里插入圖片描述
也可以使用back_inserter來創建一個迭代器,作為算法的目的位置來使用:
在這里插入圖片描述
這樣可以避免對空容器操作的未定義行為。具體實現:

在每步迭代中,fill_n向給定序列的一個元素賦值。由于我們傳遞的參數是back_inserter返回的迭代器,因此每次賦值都會在vc上調用push_back(是向空容器添加元素,而不再是簡簡單單的對空容器的元素賦值這一未定義行為)。最終,這條fill_n調用語句向vec的末尾添加了10個元素,每個元素的值都是0。

當我們向fill_n傳遞back_inserter時,雖然最終效果是向容器添加了新的元素,但對fill_n來說,根本不知道這回事兒。它仍然像往常一樣(通過迭代器)向元素賦予新值,只不過這次是通過back_inserter來賦值,而back_inserter選擇將新值添加到了容器而已。

插入迭代器是否與“標準庫算法不會改變它們所操作的容器的大小”相悖

嚴格來說,標準庫算法根本不知道有“容器”這個東西。它們只接受迭代器參數,運行于這些迭代器之上,這些迭代器只能順序或隨機訪問容器中的元素,造成的效果就是算法只能讀取元素、改變元素值、移動元素,但無法添加或刪除元素。

但當我們傳遞給算法插入器,例如back_inserter時,由于這類迭代器能調用下層容器的操作(如push_back)來向容器插入元素,造成的算法執行的效果就是向容器中添加了元素。

因此,關鍵要理解:標準庫算法從來不直接操作容器,它們只操作迭代器,從而間接訪問容器。能不能插入和刪除元素,不在于算法,而在于傳遞給它們的迭代器是否具有這樣的能力。


拷貝(copy)算法

拷貝算法是另一個向目的位置迭代器指向的輸出序列中的元素寫入數據的算法。

  • 接受三個迭代器,前兩個迭代器設置范圍,第三個表示目標序列的起始位置。
  • 將前兩個迭代器劃定的輸入范圍中的元素拷貝到第三個迭代器指定的目標序列中。
  • 后面的序列必須能容納前面的元素

可以實現內置數組的拷貝:

  int a1[] = {0, 1, 2, 3, 4};int a2[sizeof(a1)/sizeof(*a1)]; // 與a1大小一樣auto ret = copy(begin(a1), end(a1), a2); // 把a1的內容拷貝給a2// ret指向拷貝到a2的尾元素之后的位置

copy返回的是其目的位置迭代器(遞增后)的值。即,ret恰好指向拷貝到a2的尾元素之后的位置。

也可用來實現不改變原序列值的目的:

多個算符都提供“拷貝”版本。計算新元素的值,但不會將它們放置在輸入序列的末尾,而是創建一個新序列保存這些結果。以replace算法為例:

replace算法

replace算法讀入一個序列,并將其中所有等于給定值的元素都改為另一個值。

此算法接受4個參數:

  • 前兩個是迭代器,表示輸入序列;
  • 后兩個一個是要搜索的值;
  • 另一個是新值。它將所有等于第一個值的元素替換為第二個值。
replace(vc.begin(), vc.end(), 0, 2); // 將序列中所有0都替換為2

如果我們希望保留原序列不變,可調用replace_copy。該算法接受額外第三個迭代器參數,指出調整后序列的保存位置:

replace_copy(vc.cbegin(), vc.cend(), back_inseret(ivec), 0, 2);

調用后,vc未改變,ivec包含vc的一份拷貝,ivec中原vc對應位置的0都變為2。


這里的容器大小指的是元素數量

舉個例子:
在這里插入圖片描述
即使我們用reserve分配了至少能容納10個int的內存空間。但調用fill_n的行為仍然是未定義的。

這是因為泛型算法對于容器的要求并不是有足夠的空間,而是足夠的元素數量。

而reserve調整的是capacity的值,而非size的值(下面兩圖),換言之,此時vc依然為空,沒有任何元素。而算法又不具備向容器添加元素的能力, 因此fill_n仍然失敗。

在這里插入圖片描述
在這里插入圖片描述



定制操作

上面說過,sort算法默認使用元素類型的<運算符。但我們可能遇見下列情況:

  • 我們希望的排序順序與<所定義的順序不同
  • 序列可能保存的是未定義<運算符的元素類型

這時就需要重載sort的默認行為。

謂詞

謂詞是一個可調用的表達式,返回結果是一個能用做條件的值。

標準庫算法使用的謂詞分為兩類:一元謂詞(只接受單一參數)二元謂詞(有兩個參數)。接受謂詞參數的算法對輸入序列中的元素調用謂詞。因此,元素類型必須能轉換為謂詞的參數類型。

實例:
按照string的<運算符排序:
在這里插入圖片描述

使單詞按照長度排序,相同長度的單詞按照字典序排列:
在這里插入圖片描述

stable

帶有stable的函數可保證相等元素的原本相對次序在排序后保持不變

實例,找出vector<string>內長度大于等于5的元素:

partition有時會打亂原有相對次序:
在這里插入圖片描述

stable_partition會在排序的基礎上盡量維持原來的相對次序:
在這里插入圖片描述


可調用對象

可以向一個算法傳遞任何類別的可調用對象

可調用:對于一個對象或一個表達式,如果可以對其使用調用運算符——(),則稱它為可調用的。

可調用對象有四種:

  1. 函數
  2. 函數指針
  3. 重載了函數調用運算符的類
  4. lambda表達式

lambda表達式

概念

有時我們希望進行的操作需要更多的參數,以至于超出了算法對為此的限制(二元謂詞無法滿足需求)。此時就可以使用lambda表達式。

一個lambda表達式表示一個可調用的代碼單元。我們可以將其理解為一個未命名的內聯函數。

與任何函數類似,一個lambda具有一個返回類型、一個參數列表和一個函數體。 但與函數不同,lambda可能定義在函數內部。一個lambda表達式具有如下形式:

在這里插入圖片描述

  • capture list(捕獲列表)是一個lambda所在函數中定義的局部變量的列表(通常為空);
  • return 表示返回類型。
  • parameter list表示參數列表。
  • function body表示函數體。
  • 與普通函數不同,lambda必須使用尾置返回來指定返回類型。
  • 我們可以忽略參數列表和返回類型,但必須永遠包含捕獲列表和函數體。
  • lambda的調用方式與普通函數的調用方式相同,都是使用調用運算符。
  • lambda不能有默認實參。換言之,調用的實參數目永遠與形參數目相等。
  • lambda表達式之間不能相互賦值,即使看起來類型相同。

在lambda中忽略括號和參數列表等價于指定一個空參數列表。

關于返回類型:

  • 如果忽略返回類型,lambda根據函數體中的代碼推斷出返回類型。
  • 如果函數體只是一個return語句,則返回類型從返回的表達式的類型推斷而來。
  • 如果包含任何單一return語句之外的內容,則返回類型為void,此時lambda不能返回值。(C++11)

一個僅忽略返回類型的實例:

  [](const string& a, const string& b){return a.size() < b.size();}

空捕獲列表表明此lambda不使用它所在函數中的任何局部變量。

一個lambda表達式表示一個可調用的代碼單元。我們可以將其看為一個未命名的內聯函數:
在這里插入圖片描述


原理

當定義了lambda表達式之后,編譯器按照仿函數的形式自動生成一個lamber_uuid類(本質還是未命名的),并重載其中的operator(),當用戶進行調用的時候會自動通過該類的匿名對象來調用,使得其看起來和普通的函數一樣。

當向一個函數傳遞一個lambda時,同時定義了一個新類型和該類型的一個對象:傳遞的參數就是此編譯器生成的類類型的未命名對象。類似的,當使用auto定義一個用lambda初始化的變量時,定義了一個從lambda生成的類型的對象。

默認情況下,從lambda生成的類都包含一個對應該lambda所捕獲的變量的數據成員。類似任何普通類的數據成員,lambda的數據成員也在lambda對象創建時被初始化。


捕獲列表

  • 捕獲列表只用于局部非static變量,lambda可以直接使用局部static變量和在它所在函數之外聲明的名字。

捕獲列表允許的形式:
在這里插入圖片描述
下面我們通過實例來詳細了解:

find_if

用find_if尋找vs中長度大于5的元素。

find_if返回一個迭代器,指向第一個長度不小于給定參數的元素。如果這樣的元素不存在,返回尾后迭代器的拷貝。

for_each

通過for_each算法來打印find_if找到的符合要求的元素。

接受一個可調用對象,并對輸入序列中每個元素調用此對象。
在這里插入圖片描述

捕獲方式

值捕獲

采用值捕獲的前提是變量可以拷貝。與傳值參數不同的是,被捕獲的變量的值是在lambda創建時拷貝,而不是調用時拷貝:
在這里插入圖片描述
由于被捕獲變量的值是在創建lambda時拷貝的,因此對其后續修改不會影響到lambda內對應的值。

mutable

默認情況下,lambda函數總是一個const函數,mutable可以取消其常量性。使用該修飾符時,參數列表不可省略(即使參數為空)。

如果我們希望能改變一個被捕獲的變量的值,必須在參數列表首加上關鍵字mutable。 但是由于上面的被捕獲變量的值是在創建lambda時拷貝的,因此在lambda表達式內修改變量值也不會影響值本身:
在這里插入圖片描述


引用捕獲

在lambda函數體內使用引用捕獲的變量時,實際上使用的是引用所綁定的對象。
在這里插入圖片描述
引用捕獲與返回引用有著相同的問題:如果我們采用引用方式捕獲一個變量,就必須確保被引用的對象在lambda執行的時候是存在的。lambda捕獲的都是局部非static變量,這些變量在函數結束后就不復存在了。如果lambda可能在函數結束后執行,捕獲的引用指向的局部變量已經消失。

修改引用捕獲的變量

引用捕獲是否可以修改依賴于引用指向的是否是一個const類型。在lambda表達式內修改引用捕獲同樣會導致變量改變:
在這里插入圖片描述


隱式捕獲

隱式捕獲:通過在捕獲列表中寫一個&或者=指示編譯器推斷捕獲列表,=表示采用值捕獲,&表示采用引用捕獲。

// st為隱式捕獲,方式為值捕獲
auto f = [=]{return st;};

也可以混用隱式捕獲和顯示捕獲,但列表中第一個元素必須是一個&或者=,指定隱式捕獲方式為引用或值,且顯式捕獲的變量必須用與隱式捕獲不同的方式(顯式為引用捕獲隱式必須為值捕獲,反之亦然):

int st,c;auto f1 = [=, &c]{return st + c;}; // st隱式、值捕獲;c顯式、引用捕獲
auto f2 = [&, st]{return st + c;}; // c隱式、引用捕獲;st顯式、值捕獲

function模板

概念

C++中可調用對象(如函數指針,仿函數,lambda表達式等)的雖然都有一個比較統一的操作形式,但是定義方法五花八門,這樣就導致使用統一的方式保存可調用對象或者傳遞可調用對象時,會十分繁瑣。C++11中提供了std::function和std::bind統一了可調用對象的各種操作。

例如:

// 普通函數
int add(int a, int b){return a+b;} // lambda表達式
auto mod = [](int a, int b){ return a % b;}// 仿函數
struct divide{int operator()(int denominator, int divisor){return denominator/divisor;}
};

上面的幾種不同的可調用對象雖然類型不同,但是根據參數和返回值可以共享同一種調用形式int(int ,int),通過function就可以統一其調用形式:

std::function<int(int ,int)>  a = add; 
std::function<int(int ,int)>  b = mod ; 
std::function<int(int ,int)>  c = divide(); 

定義格式:std::function<返回值(參數列表)> 名字

  • std::function 是一個可調用對象包裝器,是一個類模板,可以容納除了類成員函數指針之外的所有可調用對象,它可以用統一的方式處理函數、函數對象、函數指針,并允許保存和延遲它們的執行。
  • std::function可以取代函數指針的作用,因為它可以延遲函數的執行,特別適合作為回調函數使用。它比普通函數指針更加的靈活和便利。

function與重載函數

在使用function的時候還有一個需要注意的點,就是我們不能將重載過的函數直接放入function對象中,因為會有二義性的問題。

例如:

int add(int x, int y);
double add(double x, double y);map<string, function<int(int, int)>> map;
map.insert({"+", add});//此時無法判斷是哪個add//所以此時就不能直接使用函數名進行插入
//可以通過使用函數指針來指向對應函數,再通過函數指針插入來消除二義性
int (*func)(int, int) = add;
map.insert({"+", func});


bind函數

概念

定義在functional中。相當于一個通用的函數適配器。

調用bind的一般形式為:

auto newCallable = bind(callable, arg_list);
  • newCallable本身是一個可調用對象
  • arg_list是一個逗號分隔的參數列表,對應給定的callable的參數。即,當我們調用newCallable時,newCallable會調用callable,并傳遞給它arg_list中的參數。

arg_list中的參數可能包含形如_n的元素,其中n是一個整數。這些參數是 “占位符”,表示newCallable的參數,它們占據了傳遞給newCallable的參數的“位置”。數值n表示生成的可調用對象中參數的位置: _1為newCallable的第一個參數,_2為第二個參數,依此類推。

使用placeholders名字

名字_n都定義在一個名為placeholders的命名空間中,這個命名空間本身定義在std命名空間。

為了使用_1我們必須做出以下聲明:

using std::placeholders::_1

說明_1定義在命名空間placeholders中,而placeholders又定義在命名空間std中。

這樣寫也有弊端:我們必須為每個_n提供單獨的using聲明,太過繁瑣。 因此我們可以使用另外一種形式:

using namespace namespace_name;

這種形式說明希望所有來自namespace_name的名字都可以在我們的程序中直接使用。

因此我們可以使用下面這兩種形式:

using namespace std::placeholders;using namespace std;
using namespace placeholders;

與bind函數一樣,placeholders命名空間也定義在functional頭文件中。


作用

削減參數數量

函數作為可調用對象,lambda表達式能出現的地方函數也可以。但問題是算法要求一元謂詞,函數的形參大于一個怎么辦?lambda可以通過捕獲列表解決一元謂詞的限制。函數可以通過bind函數適配器削減參數數量。

還是以之前find_if函數為例:

我們可以通過形如這樣的函數起到lambda表達式的作用:

bool check_size(const string& s, string::size_type sz){return s.size() >= sz;
}

但是由于find_if接受一個一元謂詞,因此傳遞給find_if的可調用對象必須接受單一參數。check_size接受兩個參數顯然不符合要求。那么怎么解決呢?lambda將sz放入捕獲列表,check_size可以使用bind函數,用一個定值作為其大小參數來調用check_size:

auto check5 = bind(check_size, _1, 5);
// _1形式亦可寫為placeholders::_1
// 前者需要添加語句 using namespace placeholders;

此bind調用只有一個占位符,表示check5只接受單一參數。占位符出現在arg_list的第一個位置,表示check5的此參數對應check_size的第一個參數。此參數是一個const string&。因此,調用check6必須傳遞給它一個string類型的參數,check6會將此參數傳遞給check_size。

將原來基于lambda的find_if調用替換為使用bind的版本:

bool check_size(const string& s, string::size_type sz){return s.size() >= sz;
}int main(int argc, char const *argv[]) {vector<string> vs = {"daa","bbasdf","ccccc","dddd","aebbbb"};sort(vs.begin(), vs.end(), isshorter);auto check5 = bind(check_size,  placeholders::_1, 5);auto flag = find_if(vs.begin(), vs.end(), check5);for_each(flag, vs.end(),[](const string& s){cout << s << " ";});cout << endl;
}

輸出結果:
在這里插入圖片描述


重排參數順序

bool isshorter(const string& s1, const string& s2){return s1.size() < s2.size();
}

按單詞長度由短至長排序:

sort(words.begin(), words.end(), isshorter);

按單詞長度由長至短排序:

sort(words.begin(), words.end(), bind(isshorter, _2, _1));

在第一個調用中,當sort需要比較兩個元素A和B時,它會調用isshorter(A,B)。

在第二個對sort的調用中,傳遞給isshorter的參數被交換過來了。因此,當sort比較兩個元素時,就好像調用isshorter(B,A)一樣。


綁定引用參數

默認情況下,bind的那些不是占位符的參數被拷貝到bind返回的可調用對象中。 但是,與lambda類似,有時對有些綁定的參數我們希望以引用方式傳遞,或是要綁定參數的類型無法拷貝。

我們想用函數替換下面lambda表達式:

ostream& os = cout;
for_each(flag, vs.end(),[&os](const string& s) {os << s << " ";});

目標功能函數:

ostream &print(ostream & os, const string&s){return os << s;
}

如果要用它替換lambda就必須用到bind:

ostream &os(cout);
for_each(flag, vs.end(), bind(print, os, _1));

但是這樣做是錯誤的,我們說,bind拷貝參數列表給返回的可調用對象,ostream是不能拷貝的。 如果希望傳遞給bind一個對象而又不拷貝它,就必須使用標準庫ref函數

for_each(flag, vs.end(), bind(print, ref(os), _1));

值得一提的是, 在編譯器中對ostream的引用不加ref是不報錯的,但是不能運行。換言之,檢查某個參數bind能否拷貝是程序員的責任。

函數ref返回一個對象,包含給定的引用。返回的對象是可拷貝的。

類似的還有cref函數,生成一個保存const引用的類。

與bind一樣,函數ref和cref也定義在頭文件functional中。



泛型算法結構——迭代器類別

概念

算法分類方式可以如同一開始所寫那樣,分為:是否讀、寫或者是重排序類中的元素。

也可以從迭代器入手進行分類:

不同的算法要求其迭代器提供的操作不同。某些算法,如find,只要求通過迭代器訪問元素、遞增迭代器以及比較兩個迭代器是否相等這些能力。其他一些算法,如sort,還要求讀、寫和隨機訪問元素的能力。算法所要求的迭代器操作可以分為5個迭代器類別(iterator category),每個算法都會對它的每個迭代器參數指明須提供哪類迭代器。

在這里插入圖片描述


不太重要的概念

類似容器,迭代器也定義了一組公共操作。一些操作所有迭代器都支持,另外一些只有特定類別的迭代器才支持。 例如,ostream_iterator只支持遞增、解引用和賦值。vector、string和deque的迭代器除了這些操作外,還支持遞減、關系和算術運算。

迭代器是按它們所提供的操作來分類的,而這種分類形成了一種層次。除了輸出迭代器之外,一個高層類別的迭代器支持低層類別迭代器的所有操作。

C++標準指明了泛型和數值算法的每個迭代器參數的最小類別。例如,find算法在一個序列上進行一遍掃描,對元素進行只讀操作,因此至少需要輸入迭代器。replace函數需要一對迭代器,至少是前向迭代器。類似的,replace_copy的前兩個迭代器參數也要求至少是前向迭代器。其第三個迭代器表示目的位置,必須至少是輸出迭代器。其他的例子類似。對每個迭代器參數來說,其能力必須與規定的最小類別至少相當。向算法傳遞一個能力更差的迭代器會產生錯誤。

值得一提的是:對于向一個算法傳遞錯誤類別的迭代器的問題,很多編譯器不會給出任何警告或提示。


迭代器類別

輸入迭代器

輸入迭代器(input iterator):只讀,不寫;單遍掃描,只能遞增。 一個輸入迭代器必須支持:

  • 用于比較兩個迭代器的相等和不相等運算符(==、!=)
  • 用于推進迭代器的前置和后置遞增運算(++)
  • 用于讀取元素的解引用運算符(*);解引用只會出現在賦值運算符的右側 (將已經解引用的輸入迭代器的值賦予變量)
  • 箭頭運算符(->),等價于(*it).member,即,解引用迭代器,并提取對象的成員

輸入迭代器只用于順序訪問。 對于一個輸入迭代器,*it++保證是有效的,但遞增它可能導致所有其他指向流的迭代器失效(私以為也就是值被讀取出來了,其他指向流的迭代器指向的元素沒有了,就會導致它們失效)。其結果就是,不能保證輸入迭代器的狀態可以保存下來并用來訪問元素。因此,輸入迭代器只能用于單遍掃描算法。(私以為類似于輸入流沒法被讀取第二次。) 算法find和accumulate要求輸入迭代器;而istream_iterator是一種輸入迭代器。


輸出迭代器

輸出迭代器(output iterator):只寫而不讀元素;單遍掃描,只能遞增。 輸出迭代器必須支持:

  • 用于推進迭代器的前置和后置遞增運算(++)
  • 解引用運算符(*),只出現在賦值運算符的左側(向一個已經解引用的輸出迭代器賦值,就是將值寫入它所指向的元素)

我們只能向一個輸出迭代器賦值一次。 用作目的位置的迭代器通常都是輸出迭代器。例如,copy函數的第三個參數就是輸出迭代器。ostream_iterator類型也是輸出迭代器。


前向迭代器

前向迭代器(forward iterator):可以讀寫元素;多遍掃描,只能遞增。這類迭代器只能在序列中沿一個方向移動。

  • 支持所有輸入和輸出迭代器的操作
  • 可以多次讀寫同一個元素
  • 可以保存前向迭代器的狀態

算法replace要求前向迭代器。forward_list上的迭代器是前向迭代器。


雙向迭代器

雙向迭代器(bidirectional iterator):可以正向/反向讀寫序列中的元素。

  • 支持所有前向迭代器的操作
  • 支持前置和后置遞減運算符(–)

算法reverse要求雙向迭代器。除了forward_list之外,其他標準庫都提供符合雙向迭代器要求的迭代器。


隨機訪問迭代器

隨機訪問迭代器(random-access iterator):提供在常量時間內訪問序列中任意元素的能力。

  • 支持雙向迭代器的所有功能
  • 用于比較兩個迭代器相對位置的關系運算符(<、<=、>和>=)
  • 迭代器和一個整數值的加減運算(+、+=、-和-=),計算結果是迭代器在序列中前進(或后退)給定整數個元素后的位置
  • 用于兩個迭代器上的減法運算符(-),得到兩個迭代器的距離
  • 下標運算符(iter[n]),與*(iter[n])等價

算法sort要求隨機訪問迭代器。array、deque、string和vector的迭代器都是隨機訪問迭代器,用于訪問內置數組元素的指針也是。



算法的命名規范

_if版本的算法

接受一個元素值的算法通常有另一個不同名的(不是重載的)版本,該版本接受一個謂詞代替元素值。 接受謂詞參數的算法都有附加的_if前綴:
在這里插入圖片描述

這兩個算法都在輸入范圍中查找特定元素第一次出現的位置。算法find查找一個指定值;算法find_if查找使得pred返回非零值的元素。

這兩個算法提供了命名上差異的版本,而非重載版本,因為兩個版本的算法都接受相同數目的參數,因此可能產生重載歧義。 雖然很罕見,但為了避免任何可能的歧義,標準庫選擇提供不同名字的版本而不是重載。


_copy版本的算法

默認情況下,重排元素的算法將重排后的元素寫回給定的輸入序列中。這些算法還提供另一個版本,將元素寫到一個指定的輸出目的位置。如我們所見,寫到額外目的空間的算法都在名字后面附加一個_copy:

一些算法同時提供_copy和_if版本。這些版本接受一個目的位置迭代器和一個謂詞:
在這里插入圖片描述

兩個算法都調用了lambda(參見10.3.2節,第346頁)來確定元素是否為奇數。在第一個調用中,我們從輸入序列中將奇數元素刪除。在第二個調用中,我們將非奇數(亦即偶數)元素從輸入范圍拷貝到v2中。



list和forward_list獨有的算法

概念

與其他容器不同,鏈表類型list和forward_list定義了幾個成員函數形式的算法。例如,它們定義了獨有的sort、merge、remove、reverse和unique。通用版本的sort要求隨機訪問迭代器,因此不能用于list和forward_list,因為這兩個類型分別提供雙向迭代器和前向迭代器。

鏈表類型定義的獨有算法中,部分算法的通用版本可以用于鏈表。(顯然上面說的sort不在“部分”中) 但代價太高。這些算法需要交換輸入序列中的元素。一個鏈表可以通過改變元素間的鏈接而不是真的交換它們的值來快速“交換”元素。 因此,這些鏈表版本的算法的性能比對應的通用版本好得多。

在這里插入圖片描述
在這里插入圖片描述


鏈表數據結構特有的splice算法

沒有通用版本。
在這里插入圖片描述

多數鏈表特有的算法都與其通用版本很相似,但不完全相同。

鏈表特有版本與通用版本間的一個至關重要的區別是鏈表版本會改變底層的容器。例如,remove的鏈表版本會刪除指定的元素。unique的鏈表版本會刪除第二個和后繼的重復元素。

類似的,merge和splice會銷毀其參數。

例如,

  • 通用版本的merge將合并的序列寫到一個給定的目的迭代器;兩個輸入序列是不變的。
  • 而鏈表版本的merge函數會銷毀給定的鏈表——元素從參數指定的鏈表中刪除,被合并到調用merge的鏈表對象中。在merge之后,來自兩個鏈表中的元素仍然存在,但它們都已在同一個鏈表中。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/443810.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/443810.shtml
英文地址,請注明出處:http://en.pswp.cn/news/443810.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

jsp,div 限制字數,超出部分用省略號代替

1.我是用struts2標簽做的&#xff1a;如下&#xff1a; <% page language"java" import"java.util.*" pageEncoding"UTF-8"%> <% taglib prefix"s" uri"/struts-tags"%> <%String path request.getContext…

C++之關聯容器

文章目錄概述及類型mapsetpair類型概念初始化默認初始化提供初始化器允許的操作可以創建一個pair類的函數可以作為容器的類型關聯容器迭代器概念map的迭代器set的迭代器是const的初始化map and setmultimap and multiset關聯容器的操作額外的類型別名關聯容器和算法刪除元素添加…

動態內存、智能指針(shared_ptr、unique_ptr、weak_ptr)、動態數組

文章目錄三種對象的分類三種內存的區別動態內存概念智能指針允許的操作智能指針的使用規范new概念內存耗盡/定位new初始化默認初始化直接初始化值初始化delete概念手動釋放動態對象空懸指針shared_ptr類格式獨有的操作make_shared函數shared_ptr的計數器通過new用普通指針初始化…

動態數組的簡單應用

文章目錄連接兩個字符串字面常量題目注意代碼輸出結果處理輸入的變長字符串題目注意代碼連接兩個字符串字面常量 題目 連接兩個字符串字面常量&#xff0c;將結果保存在一個動態分配的char數組中。重寫&#xff0c;連接兩個標準庫string對象。 注意 使用頭文件cstring的str…

二分查找算法實現

文章目錄思路代碼以二分區間作為while判定條件以給定值作為while判定條件主函數思路 while判定條件的選擇&#xff0c;注意最外層的return的內容就好。下面提供了兩個代碼版本。計算 mid 時需要防止溢出&#xff08;對應類型【如本例中的int】可能存不下&#xff09;&#xff…

Windows下Spring3.x計劃任務實現定時備份MySql數據庫

今天在空閑之余查了一下關于MySql數據庫備份的方案&#xff0c;最后結合自己的項目情況寫了一個關于Spring計劃任務的例子&#xff0c;目前我這個版本是在Windwos下測試成功&#xff0c;希望對大家有所幫助&#xff0c;不足之處還請大家多多包含&#xff0c;有什么建議盡管提出…

根據中序、前序遍歷重建二叉樹

文章目錄題目遞歸思路細節易錯代碼復雜度分析迭代思路細節易錯代碼復雜度分析題目 輸入某二叉樹的前序遍歷和中序遍歷的結果&#xff0c;請重建該二叉樹。假設輸入的前序遍歷和中序遍歷的結果中都不含重復的數字。 例如&#xff0c;給出 前序遍歷 preorder [3,9,20,15,7] 中…

深搜+剪枝

文章目錄題目思路注意代碼復雜度分析題目 給定一個 m x n 二維字符網格 board 和一個字符串單詞 word 。如果 word 存在于網格中&#xff0c;返回 true &#xff1b;否則&#xff0c;返回 false 。 單詞必須按照字母順序&#xff0c;通過相鄰的單元格內的字母構成&#xff0c…

搜索+回溯問題(DFS\BFS詳解)

文章目錄題目思路DFS思路代碼復雜度分析BFS思路代碼復雜度分析題目 地上有一個m行n列的方格&#xff0c;從坐標 [0,0] 到坐標 [m-1,n-1] 。一個機器人從坐標 [0, 0] 的格子開始移動&#xff0c;它每次可以向左、右、上、下移動一格&#xff08;不能移動到方格外&#xff09;&am…

剪繩子(動規、數論、貪心)

文章目錄題目數論思路代碼復雜度分析動規一思路代碼動規二思路代碼對最終結果取模1e97思路代碼題目 給你一根長度為 n 的繩子&#xff0c;請把繩子剪成整數長度的 m 段&#xff08;m、n都是整數&#xff0c;n>1并且m>1&#xff09;&#xff0c;每段繩子的長度記為 k[0],…

快速冪實現pow函數(從二分和二進制兩種角度理解快速冪)

文章目錄迭代實現快速冪思路int的取值范圍快速冪從二進制的角度來理解從二分法的角度來理解代碼復雜度分析進階——超級次方思路倒序快速冪正序快速冪代碼復雜度分析迭代實現快速冪 實現 pow(x, n) &#xff0c;即計算 x 的 n 次冪函數&#xff08;即&#xff0c;xn&#xff0…

備份MySQL數據庫的命令

備份MySQL數據庫的命令 mysqldump -hhostname -uusername -ppassword databasename > backupfile.sql 備份MySQL數據庫為帶刪除表的格式 備份MySQL數據庫為帶刪除表的格式&#xff0c;能夠讓該備份覆蓋已有數據庫而不需要手動刪除原有數據庫。 mysqldump -–add-drop-…

n位數的全排列(需要考慮大數的情況)

文章目錄題目思路代碼題目 輸入數字 n&#xff0c;按順序打印出從 1 到最大的 n 位十進制數。比如輸入 3&#xff0c;則打印出 1、2、3 一直到最大的 3 位數 999。 示例 1: 輸入: n 1 輸出: [1,2,3,4,5,6,7,8,9] 說明&#xff1a; 用返回一個整數列表來代替打印 n 為正整數 …

正則表達式匹配(動規)

文章目錄題目思路轉移方程特征再探 i 和 j代碼題目 請實現一個函數用來匹配包含 . 和 * 的正則表達式。模式中的字符 . 表示任意一個字符&#xff0c;而 * 表示它前面的字符可以出現任意次&#xff08;含0次&#xff09;。在本題中&#xff0c;匹配是指字符串的所有字符匹配整…

在循環遞增一次的數組中插入元素

文章目錄題目思路如何建立左右區間&#xff1f;如何查找最高點&#xff1f;那我們怎么判斷 num 到底處于什么樣的位置呢&#xff1f;如何確定插入位置&#xff1f;插入元素代碼題目 給一個只循環遞增一次的數組 res&#xff0c;res 滿足首元素大于等于尾元素&#xff0c;形如&…

Unable to find 'struts.multipart.saveDir' property setting.

Unable to find struts.multipart.saveDir property setting. 今天在做工作的時候遇到了這個問題&#xff0c;后來經過百度&#xff0c;終于知道了原因&#xff0c;現在記錄下來&#xff0c;以備以后查看。 1.struts.multipart.saveDir沒有配置 2.struts.multipart.saveDir用…

表示數值的字符串(有限狀態自動機與搜索)

文章目錄題目思路一代碼一思路二代碼二題目 思路一 考察有限狀態自動機&#xff08;參考jyd&#xff09;&#xff1a; 字符類型&#xff1a; 空格 「 」、數字「 0—9 」 、正負號 「 」 、小數點 「 . 」 、冪符號 「 eE 」 。 狀態定義&#xff1a; 按照字符串從左到右的…

樹的子結構

文章目錄題目深搜深搜代碼廣搜廣搜代碼題目 輸入兩棵二叉樹A和B&#xff0c;判斷B是不是A的子結構。(約定空樹不是任意一個樹的子結構) B是A的子結構&#xff0c; 即 A中有出現和B相同的結構和節點值。 例如: 給定的樹 A: 給定的樹 B&#xff1a; 返回 true&#xff0c;因為…

寫題過程中碰見的小問題

文章目錄和--vector二維vector的初始化數組中最大的數max_element()數組所有元素之和accumulate()vector數組去重對pair類型的vector排序對元素都為正整數的vector利用sort默認的升序排列進行降序排列一維數組轉二維數組size_t和int如何不用臨時變量交換兩個數?將類函數的形參…

LeetCode——二叉樹序列化與反序列化

文章目錄題目思路問題一問題二代碼實現題目 請實現兩個函數&#xff0c;分別用來序列化和反序列化二叉樹。 設計一個算法來實現二叉樹的序列化與反序列化。不限定序列 / 反序列化算法執行邏輯&#xff0c;你只需要保證一個二叉樹可以被序列化為一個字符串并且將這個字符串反序…