C++ 泛型算法


 《C++ Primer 4th》讀書筆記

標准容器(the standard container)定義了很少的操作。標准庫並沒有為每種容器類型都定義實現這些操作的成員函數,而是定義了一組泛型算法:因為它們實現共同的操作,所以稱之為“算法”;而“泛型”指的是它們可以操作在多種容器類型上——不但可作用於 vector 或 list 這些標准庫類型,還可用在內置數組類型、甚至其他類型的序列上

 

標准算法固有地獨立於類型,與容器的類型無關:在前面的描述中,沒有任何內容依賴於容器類型。這種算法只在一點上隱式地依賴元素類型:必須能夠對元素做比較運算。該算法的明確要求如下:

1. 需要某種遍歷集合的方式:能夠從一個元素向前移到下一個元素。

2. 必須能夠知道是否到達了集合的末尾。

3. 必須能夠對容器中的每一個元素與被查找的元素進行比較。

4. 需要一個類型指出元素在容器中的位置,或者表示找不到該元素。

 

標准庫提供了超過 100 種算法。與容器一樣,算法有着一致的結構。

 

關鍵概念:算法永不執行容器提供的操作

泛型算法本身從不執行容器操作,只是單獨依賴迭代器和迭代器操作實現。算法基於迭代器及其操作實現,而並非基於容器操作。這個事實也許比較意外,但本質上暗示了:使用“普通”的迭代器時,算法從不修改基礎容器的大小。正如我們所看到的,算法也許會改變存儲在容器中的元素的值,也許會在容器內移動元素,但是,算法從不直接添加或刪除元素。

 

使用泛型算法必須包含 algorithm 頭文件:

#include <algorithm>

標准庫還定義了一組泛化的算術算法(generalized numeric algorithm),其命名習慣與泛型算法相同。使用這些算法則必須包含 numeric 頭文件:

#include <numeric>

除了少數例外情況,所有算法都在一段范圍內的元素上操作,我們將這段范圍稱為“輸出范圍(input range)”。

 

accumulate 帶有三個形參。頭兩個形參指定要累加的元素范圍。第三個形參則是累加的初值。

find_first_of 函。這個算法帶有兩對迭代器參數來標記兩段元素范圍,在第一段范圍內查找與第二段范圍中任意元素匹配的元素,然后返回一個迭代器,指向第一個匹配的元素。如果找不到元素,則返回第一個范圍的 end 迭代器。

寫入到輸入序列的一個簡單算法是 fill 函數帶有一對迭代器形參,用於指定要寫入的范圍,而所寫的值是它的第三個形參的副本。

fill_n 函數帶有的參數包括:一個迭代器、一個計數器以及一個值。該函數從迭代器指向的元素開始,將指定數量的元素設置為給定的值。fill_n 函數假定對指定數量的元素做寫操作是安全的。常犯的錯誤的是:在沒有元素的空容器上調用 fill_n 函數

 

確保算法有足夠的元素存儲輸出數據的一種方法是使用插入迭代器插入迭代器是可以給基礎容器添加元素的迭代器。通常,用迭代器給容器元素賦值時,被賦值的是迭代器所指向的元素。而使用插入迭代器賦值時,則會在容器中添加一個新元素,其值等於賦值運算的右操作數的值。

back_inserter 的程序必須包含 iterator 頭文件。

back_inserter 函數是迭代器適配器。與容器適配器一樣,迭代器適配器使用一個對象作為實參,並生成一個適應其實參行為的新對象。

vector<int> vec; // empty vector

// ok: back_inserter creates an insert iterator that adds elements to vec

fill_n (back_inserter(vec), 10, 0); // appends 10 elements to vec

copy函數帶有三個迭代器參數:頭兩個指定輸入范圍,第三個則指向目標序列的一個元素。傳遞給 copy 的目標序列必須至少要與輸入范圍一樣大

vector<int> ivec; // empty vector

// copy elements from ilst into ivec

copy (ilst.begin(), ilst.end(), back_inserter(ivec));

 

 

replace函數帶有四個形參:一對指定輸入范圍的迭代器和兩個值。

// replace any element with value of 0 by 42

replace(ilst.begin(), ilst.end(), 0, 42);

 

 

replace_copy算法接受第三個迭代器實參,指定保存調整后序列的目標位置。

// create empty vector to hold the replacement

vector<int> ivec;

// use back_inserter to grow destination as needed

replace_copy (ilst.begin(), ilst.end(),back_inserter(ivec), 0, 42);

 

 

sort 算法帶有兩個迭代器實參,指出要排序的元素范圍。這個算法使用小於(<)操作符比較元素。

 

unique 算法帶有兩個指定元素范圍的迭代器參數。該算法刪除相鄰的重復元素,然后重新排列輸入范圍內的元素,並且返回一個迭代器,表示無重復的值范圍的結束。unique 實際上並沒有刪除任何元素,而是將無重復的元素復制到序列的前端,從而覆蓋相鄰的重復元素。unique 返回的迭代器指向超出無重復的元素范圍末端的下一位置。

 

算法不直接修改容器的大小。如果需要添加或刪除元素,則必須使用容器操作。

 

謂詞是做某些檢測的函數,返回用於條件判斷的類型,指出條件是否成立。

 

stable_sort 算保留相等元素的原始相對位置。

 

count_if 算法返回使謂詞函數返回條件成立的元素個數

 

插入迭代器

標准庫所定義的迭代器不依賴於特定的容器。事實上,C++ 語言還提供了另外三種迭代器:

插入迭代器:這類迭代器與容器綁定在一起,實現在容器中插入元素的功能。

iostream 迭代器:這類迭代器可與輸入或輸出流綁定在一起,用於迭代遍歷所關聯的 IO 流。

反向迭代器:這類迭代器實現向后遍歷,而不是向前遍歷。所有容器類型都定義了自己的 reverse_iterator 類型,由 rbegin 和 rend 成員函數返回。

 

插入器是一種迭代器適配器帶有一個容器參數,並生成一個迭代器,用於在指定容器中插入元素。通過插入

迭代器賦值時,迭代器將會插入一個新的元素。C++ 語言提供了三種插入器,其差別在於插入元素的位置不同。

• back_inserter,創建使用 push_back 實現插入的迭代器。

• front_inserter,使用 push_front 實現插入。

• inserter,使用 insert 實現插入操作。除了所關聯的容器外,inserter還帶有第二實參:指向插入起始位置的迭代器。

 

 

list<int> ilst, ilst2, ilst3; // empty lists

// after this loop ilst contains: 3 2 1 0

for (list<int>::size_type i = 0; i != 4; ++i)

ilst.push_front(i);

// after copy ilst2 contains: 0 1 2 3

copy (ilst.begin(), ilst.end(), front_inserter(ilst2));

// after copy, ilst3 contains: 3 2 1 0

copy (ilst.begin(), ilst.end(), inserter (ilst3, ilst3.begin()));

 

 

 

 

front_inserter 的使用將導致元素以相反的次序出現在目標對象中,這點非常重要。

 

iostream 迭代器

標准庫提供了在 iostream 對象上使用的迭代器:istream_iterator 用於讀取輸入流,而 ostream_iterator 則用

於寫輸出流

iostream

迭代器的構造函數

istream_iterator<T> in(strm);

創建從輸入流   strm 中讀取 T 類型對象的istream_iterator 對象

istream_iterator<T> in;

istream_iterator   對象的超出末端迭代器

ostream_iterator<T> in(strm);

創建將 T   類型的對象寫到輸出流 strm 的ostream_iterator 對象

ostream_iterator<T>   in(strm, delim);

創建將 T   類型的對象寫到輸出流 strm 的ostream_iterator 對象,在寫入過程中使用 delim作為元素的分隔符。delim   是以空字符結束的字符數組

 

istream_iterator

的操作

it1 ==   it2

it1 != it2

比較兩上   istream_iterator 對象是否相等(不等)。迭代器讀取的必須是相同的類型。如果兩個迭代器都是 end   值,則它們相等。對於兩個都不指向流結束位置的迭代器,如果它們使用同一個輸入流構造,則它們也相等

*it

返回從流中讀取的值

it->mem

是   (*it).mem 的同義詡。返回從流中讀取的對象的 mem 成員

++it

it++

通過使用元素類型提供的   >> 操作從輸入流中讀取下一個元素值,使迭代器向前移動。通常,前綴版本使用迭代器在流中向前移動,並返回對加 1   后的迭代器的引用。而后綴版本使迭代器在流中向前移動后,返回原值

 

流迭代器都是類模板:任何已定義輸入操作符(>> 操作符)的類型都可以定義 istream_iterator。類似地,任何已定義輸出操作符(<< 操作符)的類型也可定義 ostream_iterator。

在創建流迭代器時,必須指定迭代器所讀寫的對象類型:

istream_iterator<int> cin_it(cin); // reads ints1 from cin

istream_iterator<int> end_of_stream; // end iterator value

// writes Sales_items from the ofstream named outfile

// each element is followed by a space

ofstream outfile;

ostream_iterator<Sales_item> output(outfile, " ");

 

ostream_iterator 對象必須與特定的流綁定在一起。在創建istream_iterator 時,可直接將它綁定到一個流上。

 

istream_iterator<int> in_iter(cin); // read ints from cin

istream_iterator<int> eof; // istream "end" iterator

// read until end of file, storing what was read in vec

while (in_iter != eof)

// increment advances the stream to the next value dereference reads next value from the istream

vec.push_back(*in_iter++);

 

eof 迭代器定義為空的istream_iterator 對象,用作結束迭代器。綁在流上的迭代器在遇到文件結束或某個錯誤時,將等於結束迭代器的值。

 

istream_iterator<int> in_iter(cin); // read ints from cin

istream_iterator<int> eof; // istream "end" iterator

vector<int> vec(in_iter, eof); // construct vec from an iteratorrange

 

 

可使用 ostream_iterator 對象將一個值序列寫入流中,其操作的過程與使用迭代器將一組值逐個賦給容器中的元素相同:

// write one string per line to the standard output

ostream_iterator<string> out_iter(cout, "\n");

// read strings from standard input and the end iterator

istream_iterator<string> in_iter(cin), eof;

// read until eof and write what was read to the standard output

while (in_iter != eof)

// write value of in_iter to standard output and then increment the iterator to get the next value from cin

*out_iter++ = *in_iter++;

 

這個程序讀 cin,並將每個讀入的值依次寫到 cout 中不同的行中。

 

流迭代器有下面幾個重要的限制:

• 不可能從 ostream_iterator 對象讀入,也不可能寫到istream_iterator 對象中。

• 一旦給 ostream_iterator 對象賦了一個值,寫入就提交了。賦值后,沒有辦法再改變這個值。此外,ostream_iterator 對象中每個不同的值都只能正好輸出一次。

• ostream_iterator 沒有 -> 操作符。

 

在一些泛型算法上使用流類迭代器。

istream_iterator<int> cin_it(cin); // reads ints from cin

istream_iterator<int> end_of_stream; // end iterator value

// initialize vec from the standard input:

vector<int> vec(cin_it, end_of_stream);

sort(vec.begin(), vec.end());

// writes ints to cout using " " as the delimiter

ostream_iterator<int> output(cout, " ");

// write only the unique elements in vec to the standard output

unique_copy(vec.begin(), vec.end(), output);

 

 

反向迭代器

反向迭代器是一種反向遍歷容器的迭代器。也就是,從最后一個元素到第一個元素遍歷容器。反向迭代器將自增(和自減)的含義反過來了:對於反向迭代器,++ 運算將訪問前一個元素,而 -- 運算則訪問下一個元素。

 

// reverse iterator of vector from back to front

vector<int>::reverse_iterator r_iter;

for (r_iter = vec.rbegin(); // binds r_iter to last element

r_iter != vec.rend(); // rend refers 1 before 1st element

++r_iter) // decrements iterator one element

cout << *r_iter << endl; // prints 9,8,7,...0

 

 

從一個既支持 -- 也支持 ++ 的迭代器就可以定義反向迭代器,流迭代器不能創建反向迭代器。

反向迭代器與其他迭代器之間的關系

// find first element in a comma-separated list

string::iterator comma = find(line.begin(), line.end(), ',');

cout << string(line.begin(), comma) << endl;

// find last element in a comma-separated list

string::reverse_iterator rcomma = find(line.rbegin(), line.rend(), ',');

// wrong: will generate the word in reverse order

cout << string(line.rbegin(), rcomma) << endl;

// ok: get a forward iterator and read to end of line

cout << string(rcomma.base(), line.end()) << endl;

 

圖顯示的對象直觀地解釋了普通迭代器與反向迭代器之間的關系。例如,正如 line_rbegin() 和 line.end() 一樣,rcomma 和 rcomma.base() 也指向不同的元素。為了確保正向和反向處理元素的范圍相同,這些區別必要的。從技術上來說,設計普通迭代器與反向迭代器之間的關系是為了適應左閉合范圍這個性質的,所以,[line.rbegin(), rcomma) 和[rcomma.base(), line.end()) 標記的是 line 中的相同元素。

 

法要求的迭代器操作分為五個類別,分別對應表列出的五種迭代器。

 

迭代器種類

Input   iterator(輸入迭代器)

讀,不能寫;只支持自增運算

Output   iterator(輸出迭代器)

寫,不能讀;只支持自增運算

Forward   iterator(前向迭代器)

讀和寫;只支持自增運算

Bidirectional   iterator(雙向迭代器)

讀和寫;支持自增和自減運算

Random   access iterator(隨機訪問迭代器)

讀和寫;支持完整的迭代器算術運算

 

輸入迭代器可用於讀取容器中的元素,但是不保證能支持容器的寫入操作。輸入迭代器必須至少提供下列支持。

o 相等和不等操作符(==,!=),比較兩個迭代器。

o 前置和后置的自增運算(++),使迭代器向前遞進指向下一個元素。

o 用於讀取元素的解引用操作符(*),此操作符只能出現在賦值運算的右操作數上。

o 箭頭操作符(->),這是 (*it).member 的同義語,也就是說,對迭代器進行解引用來獲取其所關聯的對象的成員。

 

輸出迭代器可視為與輸入迭代器功能互補的迭代器;輸出迭代器可用於向容器寫入元素,但是不保證能支持讀取容器內容。輸出迭代器一般用作算法的第三個實參,標記起始寫入的位置。輸出迭代器要求:

o 前置和后置的自增運算(++),使迭代器向前遞進指向下一個元素。

o 解引用操作符(*),引操作符只能出現在賦值運算的左操作數上。給解引用的輸出迭代器賦值,將對該迭代器所指向的元素做寫入操作。

 

前向迭代器 用於讀寫指定的容器。這類迭代器只會以一個方向遍歷序列。前向迭代器支持輸入迭代器和輸出迭代器提供的所有操作,除此之外,還支持對同一個元素的多次讀寫。

 

雙向迭代器 從兩個方向讀寫容器。除了提供前向迭代器的全部操作之外,雙向迭代器還提供前置和后置的自減運算(--)。

 

隨機訪問迭代器 提供在常量時間內訪問容器任意位置的功能。這種迭代器除了支持雙向迭代器的所有功能之外,還支持下面的操作:

o 關系操作符 <、<=、> 和 >=,比較兩個迭代器的相對位置。

o 迭代器與整型數值 n 之間的加法和減法操作符 +、+=、- 和 -=,結果是迭代器在容器中向前(或退回)n 個元素。

o 兩個迭代器之間的減法操作符(--),得到兩個迭代器間的距離。

o 下標操作符 iter[n],這是 *(iter + n) 的同義詞。

 

除了輸出迭代器,其他類別的迭代器形成了一個層次結構:需要低級類別迭代器的地方,可使用任意一種更高級的迭代器。對於需要輸入迭代器的算法,可傳遞前向、雙向或隨機訪問迭代器調用該算法。調用需要隨機訪問迭代器的算法時,必須傳遞隨機訪問迭代器。

map、set 和 list 類型提供雙向迭代器,而 string、vector 和 deque 容器上定義的迭代器都是隨機訪問迭代器都是隨機訪問迭代器,用作訪問內置數組元素的指針也是隨機訪問迭代器。istream_iterator 是輸入迭代器,而

ostream_iterator 則是輸出迭代器。

 

泛型算法的結構

算法最基本的性質是需要使用的迭代器種類。所有算法都指定了它的每個迭代器形參可使用的迭代器類型。

另一種算法分類的方法,根據對元素的操作將算法分為下面幾種:

• 只讀算法,不改變元素的值順序。

• 給指定元素賦新值的算法。

• 將一個元素的值移給另一個元素的算法。

 

算法的形參模式。大多數算法采用下面四種形式之一:

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 迭代器形參,或者同時帶有 beg2 和 end2 迭代器形參,來指定它的第二個輸入范圍。帶有 beg2 而不帶 end2 的算法將 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

 

檢查指定值的算法默認使用 == 操作符。系統為這類算法提供另外命名的(而非重載的)版本,帶有謂詞函數形參。帶有謂詞函數形參的算法,其名字帶有后綴 _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

 

區別是否實現復制的算法版本

標准庫也為這些算法提供另外命名的版本,將元素寫到指定的輸出目標。此版本的算法在名字中添加了_copy 后綴:

reverse(beg, end);

reverse_copy(beg, end, dest);

 

標准庫為 list 容器定義了更精細的操作集合,使它不必只依賴於泛型操作。對於 list 對象,應該優先使用 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)lst.splice(iter, lst2, iter2)lst.splice(iter,   beg, end)

將 lst2   的元素移到 lst 中迭代器 iter 指向的元素前面。在 lst2 中刪除移出的元素。第一個版本將 lst2 的所有元素移到 lst   中;合並后,lst2 為空。lst 和 lst2 不能是同一個 list 對象。第二個版本只移動 iter2 所指向的元素,這個元素必須是 lst2   中的元素。在這種情況中,lst 和lst2 可以是同一個 list 對象。也就是說,可在一個 list對象中使用 splice   運算移動一個元素。第三個版本移動迭代器 beg 和 end 標記的范圍內的元素。beg 和 end 照例必須指定一個有效的范圍。這兩個迭代器可標記任意   list 對象內的范圍,包括 lst。當它們指定 lst 的一段范圍時,如果 iter 也指向這個范圍的一個元素,則該運算未定義。

lst.unique()   lst.unique(binaryPred)

調用 erase   刪除同一個值的團結副本。第一個版本使用 ==操作符判斷元素是否相等;第二個版本則使用指定的謂詞函數實現判斷

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM