1. 程式人生 > >泛型演算法----再探迭代器,泛型演算法結構,特定容器演算法

泛型演算法----再探迭代器,泛型演算法結構,特定容器演算法

 

一、再探迭代器

  除了為每個容器定義的迭代器之外,標準庫在標頭檔案iterator中還定義了額外幾種迭代器。這些迭代器包括以下幾種:

a、插入迭代器:這些迭代器被繫結到一個容器上,可用來向容器插入元素。

b、流迭代器:這些迭代器被繫結到輸入或輸出流上,課=可用來遍歷所關聯的IO流。

c、反向迭代器:這些迭代器向後而不是向前移動。除了forward_list之外的標準庫容器都有反向迭代器。

d、移動迭代器:這些專用的迭代器不是拷貝其中的元素,而是移動它們。

1、插入迭代器

  插入器是一種迭代器介面卡,它接受一個容器,生成一個迭代器,能實現向給定容器新增元素。當我們通過一個插入迭代器進行賦值時,該迭代器呼叫容器操作來向給定容器的指定位置插入一個元素。

  插入迭代器操作:

操作 說明
it = t 在it指定的當前位置插入值t。假定c是it繫結的元素,依賴於插入迭代器的不同種類,此賦值會分別呼叫c.push_back(t)、c.push_front(t)、c.insert(t, p),其中p為傳遞給insert的迭代器位置
*it, ++it, it++ 這些操作雖然存在,但不會對it做任何事情。每個操作都返回it

  插入迭代器有三種類型,差異在於元素插入的位置:

a、back_inserter建立一個使用push_back的迭代器。

b、front_inserter建立一個使用push_front的迭代器。

c、inserter建立一個使用insert的迭代器。此函式接受第二個引數,這個引數必須是一個指向給定容器的迭代器。元素將插入到給定迭代器所表示的元素之前。

  注意:只有在容器支援push_front的情況下,我們才可以使用front_inserter。類似的,只有在容器支援push_back的情況下,我們才能使用back_inserter。

1)inserter

  當呼叫inserter(c, iter)時,我們得到一個迭代器,接下來使用它時,會將元素插入到iter原來所指向的元素之前的位置。

  如果it是由inserter生成的迭代器,則下面的賦值語句:

  *it = val;

其效果與下面程式碼一樣:

  it = c.insert(it, val); // it指向新加入的元素

  ++it; // 遞增it使它指向原來的元素

  一個例子:

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <list>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::list<int> lst;
13     auto it = std::inserter(lst, lst.begin());
14     *it = 1;
15     *it = 2;
16     *it = 3;
17     for (auto iter = lst.begin(); iter != lst.end(); ++iter)
18     {
19         std::cout << *iter << " ";
20     }
21     std::cout << std::endl;
22     return 0;
23 }
View Code

2)front_inserter

  front_inserter生成的迭代器的行為與inserter生成的迭代器完全不一樣。當我們使用front_inserter時,元素總是插入到容器第一個元素之前。即使我們傳遞給inserter的位置原來指向的第一個元素,只要我們在此元素之前插入一個新元素,此元素就不是容器的首元素了。

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <list>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::list<int> lst = { 1, 2, 3, 4, 5 };
13     std::list<int> lst2, lst3, lst4;
14     
15     copy(lst.begin(), lst.end(), front_inserter(lst2));
16     copy(lst.begin(), lst.end(), inserter(lst3, lst3.begin()));
17     copy(lst.begin(), lst.end(), back_inserter(lst4));
18 
19     std::cout << "front_inserter lst2: ";
20     for (auto iter = lst2.begin(); iter != lst2.end(); ++iter)
21     {
22         std::cout << *iter << " ";
23     }
24     std::cout << std::endl;
25 
26     std::cout << "inserter lst3: ";
27     for (auto iter = lst3.begin(); iter != lst3.end(); ++iter)
28     {
29         std::cout << *iter << " ";
30     }
31     std::cout << std::endl;
32 
33     std::cout << "back_inserter lst4: ";
34     for (auto iter = lst4.begin(); iter != lst4.end(); ++iter)
35     {
36         std::cout << *iter << " ";
37     }
38     std::cout << std::endl;
39     return 0;
40 }
View Code

  當呼叫front_inserter(c)時,我們得到一個插入迭代器,接下來會呼叫push_front。當每個元素插入到容器c中時,它變為c的新的首元素。因此,front_inserter生成的迭代器會將插入的元素的序列的順序顛倒過來,而inserter和back_inserter則不會。

 

2、iostream迭代器

  雖然iostream型別不是容器,但標準庫型別定義了可以用於這些IO型別物件的迭代器。istream_iterator讀取輸入流,ostream_iterator向一個輸出流寫資料。這些迭代器將它們對應的流當作一個特定型別的元素序列來處理。通過使用流迭代器,我們可以用泛型演算法從流物件讀取資料以及向其寫入資料。

1)istream_iterator操作

  當建立一個流迭代器時,必須指定迭代器將要讀寫的物件型別。一個istream_iterator使用>>來讀取流。因此,istream_iterator要讀取的型別必須定義了輸入運算子。當建立一個istream_iterator時,我們可以將它繫結到一個流。當然,我們還可以預設初始化迭代器,這樣就建立了一個可以當作尾後值使用的迭代器。

  對於一個繫結到流的迭代器,一旦其關聯的流遇到檔案尾或遇到IO錯誤,迭代器的值就與尾後迭代器相等。

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <list>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::istream_iterator<int> in_iter(std::cin); // 從cin讀取int
13     std::istream_iterator<int> eof; // 尾後迭代器
14     while (in_iter != eof) // 當有資料可供讀取時
15     {
16         std::cout << *in_iter++ << " ";
17     }
18     std::cout << std::endl;
19     return 0;
20 }
View Code

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::istream_iterator<int> in_iter(std::cin); // 從cin讀取int
13     std::istream_iterator<int> eof; // 尾後迭代器
14     std::vector<int> vec(in_iter, eof);
15     for (auto iter = vec.begin(); iter != vec.end(); ++iter)
16     {
17         std::cout << *iter << " ";
18     }
19     std::cout << std::endl;
20     return 0;
21 }
View Code

 

  istream_iterator操作:

操作 說明
std::istream_iterator<T> in(is) in從輸入流is讀取型別為T的值
std::istream_iterator<T> end 讀取型別為T的值的istream_iterator迭代器,表示尾後位置
in1 == in2 in1和in2必須讀取相同型別。如果它們都是尾後迭代器,或繫結到相同的輸入,則兩者相等
in1 != in2  
*in 返回從流中讀取的值
in->mem 與(*in).mem的含義相同
++in, in++ 使用元素型別所定義的>>運算子從輸入流中讀取下一個值。與以往一樣,前置版本返回一個指向遞增後迭代器的引用,後置版本返回舊值

2)使用演算法操作流迭代器

  由於演算法使用迭代器操作來處理資料,而流迭代器又至少支援某些迭代器操作,因此我們至少可以用某些演算法來操作流迭代器。

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::istream_iterator<int> in_iter(std::cin); // 從cin讀取int
13     std::istream_iterator<int> eof; // 尾後迭代器
14     std::cout << accumulate(in_iter, eof, 0) << std::endl;
15     return 0;
16 }
View Code

3)istream_iterator允許使用懶惰求值

  當我們將一個istream_iterator繫結到一個流時,標準庫並不保證迭代器立即從流讀取資料。具體實現可以推遲從流中讀取資料,直到我們使用迭代器時才真正讀取。標準庫中的實現所保證的是。在我們第一次解引用迭代器之前,從流中讀取資料的操作已經完成了。對於大多數程式來說,立即讀取還是推遲讀取沒什麼差別。但是,如果我們建立了一個istream_iterator,沒有使用就銷燬了,或者我們正在從兩個不同的物件同步讀取同一個流,那麼何時讀取可能就很重要了。

4)ostream_iterator操作

  我們可以對任何具有輸出運算子(<<運算子)的型別定義ostream_iterator。當建立一個ostream_iterator時,我們可以提供(可選的)第二個引數,它是一個字串,在輸出每個元素後都會列印此字串。此字串必須是一個C風格字串(即,一個字串字面常量或者一個指向以空字元結尾的字元陣列的指標)。必須將ostream_iterator繫結到一個指定的流,不允許空的或表示尾後位置的ostream_iterator。

  ostream_iterator操作:

操作 說明
ostream_iterator<T> out(os) out將型別為T的值寫到輸出流os中
ostream_iterator<T> out(os, d) out將型別為T的值寫到輸出流os中,每個值後面都輸出一個d。d指向一個空字元結尾的字元陣列
out = val 用<<運算子將val寫入到out所繫結的ostream中。val的型別必須與out可寫的型別相容
*out, ++out, out++ 這些運算是存在的,但不對out做任何事情。每個運算子都返回out
 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::vector<int> vec = { 1, 2, 3, 4, 5 };
13     std::ostream_iterator<int> out_iter(std::cout, "-*-");
14     for (auto e: vec)
15     {
16         *out_iter++ = e; // 賦值語句實際將元素寫到cout
17     }
18     std::cout << std::endl;
19     return 0;
20 }
View Code

5)使用流迭代器處理類型別

  我們可以為任何定義了輸入運算子(>>)的型別建立istream_iterator物件。類似的,只要型別由輸出運算子(<<),我們就可以為其定義ostream_iterator。

 

3、反向迭代器

   反向迭代器就是在容器中從尾元素向首元素反向移動的迭代器。對於反向迭代器,遞增(以及遞減)操作的含義會顛倒過來。遞增一個反向迭代器(++it)會移動到前一個元素;遞減一個迭代器(--it)會移動到下一個元素。

  除了forward_list之外,其他容器都支援反向迭代器。我們可以呼叫rbegin、rend、crbegin和crend成員函式來獲得反向迭代器。這些成員函式返回指向容器尾元素和首元素之前一個位置的迭代器。與普通迭代器一樣,反向迭代器也有const和非const版本。

1)反向迭代器需要遞減運算子

  我們只能從既支援++也支援--的迭代器來定義反向迭代器。畢竟反向迭代器的目的是在序列中反向移動。除了forward_list之外,標準容器上的其他迭代器都既支援遞增運算又支援遞減運算。但是,流迭代器不支援遞減運算,因為不可能在一個流中反向移動。因此,不可能從一個forward_list或一個流迭代器建立反向迭代器

2)反向迭代器和其他迭代器之間的關係

  我們通過呼叫反向迭代器的base成員函式完成到普通迭代器的轉換,此成員函式會返回其對應的普通迭代器。

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::string line = "first,middle,last";
13     auto rcomma = find(line.crbegin(), line.crend(), ','); // 查詢最後一個逗號的位置
14     std::cout << std::string(line.crbegin(), rcomma) << std::endl; // 列印最後一個單詞
15     return 0;
16 }
View Code

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::string line = "first,middle,last";
13     auto rcomma = find(line.crbegin(), line.crend(), ','); // 查詢最後一個逗號的位置
14     std::cout << std::string(rcomma.base(), line.cend()) << std::endl; // 列印最後一個單詞
15     return 0;
16 }
View Code

  注意:反向迭代器的目的是表示元素範圍,而這些範圍是不對稱的,這導致一個重要的結果:當我們從一個普通迭代器初始化一個反向迭代器,或是給一個反向迭代器賦值時,結果迭代器與原迭代器指向的並不是相同的元素

 

二、泛型演算法結構

   任何演算法的最基本的特性是它要求其迭代器提供哪些操作。演算法所要求的迭代器操作可以分為5個迭代器類別:

迭代器類別 說明
輸入迭代器 只讀,不寫;單遍掃描,只能遞增
輸出迭代器 只寫,不讀;單遍掃描,只能遞增
前向迭代器 可讀寫;多遍掃描,只能遞增
雙向迭代器 可讀寫;多遍掃描,可遞增遞減
隨機訪問迭代器 可讀寫,多遍掃描,支援全部迭代器運算

1、5類迭代器

  類似容器,迭代器也定義了一組公共操作。一些操作所有迭代器都支援,另外一些只有特定類別的迭代器才支援。迭代器是按它們所提供的操作來分類的,而這種分類形成了一種層次。除了輸出迭代器之外,一個高層類別的迭代器支援低層類別迭代器的所有操作。

  C++標準指明瞭泛型和數值演算法的每個迭代器引數的最小類別。向演算法傳遞一個能力更差的迭代器會產生錯誤。

1)輸入迭代器

  可以讀取序列中的元素。一個輸入迭代器必須支援:

a、用於比較兩個迭代器的相等和不相等運算子(==、!=)。

b、用於推進迭代器的前置和後置遞增運算(++)。

c、用於讀取元素的解引用運算子(*);解引用只會出現在賦值運算子的右側。

d、箭頭運算子(->),等價於(*it).member,即,解引用迭代器,並提取物件的成員。

  輸入迭代器只用於順序訪問。對於一個輸入迭代器,*it++保證是有效的,但遞增它可能導致所有其他指向流的迭代器失效。其結果就是,不能保證輸入迭代器的狀態可以儲存下來並用來訪問元素。因此,輸入迭代器只能用於單遍掃描演算法。

2)輸出迭代器

  可以看作輸入迭代器功能上的補集----只寫而不讀元素。輸出迭代器必須支援:

a、用於推進迭代器的前置和後置遞增運算子(++)。

b、解引用運算子(*),只出現在賦值運算子的左側(向一個已經解引用的輸出迭代器賦值,就是將值寫入它所指向的元素)。

  我們只能向一個輸出迭代器賦值一次。類似輸入迭代器,輸出迭代器只能用於單遍掃描演算法。用作目的位置的迭代器通常都是輸出迭代器。

3)前向迭代器

  可以讀寫元素。這類迭代器只能在序列中沿一個方向移動。前向迭代器支援所有輸入和輸出迭代器的操作,而且可以多次讀寫同一個元素。因此,我們可以儲存前向迭代器的狀態,使用前向迭代器的演算法可以對序列進行多遍掃描。

4)雙向迭代器

  可以正向/反向讀寫序列中的元素。除了支援所有前向迭代器的操作之外,雙向迭代器還支援前置和後置遞減運算子(--)。除了forward_list之外,其他標準庫都提供符合雙向迭代器要求的迭代器。

5)隨機訪問迭代器

  提供在常量時間內訪問序列中任意元素的能力。此類迭代器支援雙向迭代器的所有功能,還支援:

a、用於比較兩個迭代器相對位置的關係運算符(<、<=、>、>=)。

b、迭代器和一個整數值的加減運算(+、+=、-、-=),計算結果是迭代器在序列中前進(或後退)給定整數個元素後的位置。

c、用於兩個迭代器上的減法運算(--),得到兩個迭代器的距離。

d、下標運算(iter[n]),與*(iter[n])等價。

  演算法sort要求隨機訪問迭代器。array、deque、vector、string的迭代器都是隨機訪問迭代器,用於訪問內建陣列元素的指標也是。

 

2、演算法形參模式

  在任何其他演算法分類上,還有一組引數規範。大多數演算法具有如下4種形式之一:

  alg(beg, end, other args);

  alg(beg, end, dest, other args);

  alg(beg, end, beg2, other args);

  alg(beg, end, beg2, end2, other args);

其中alg是演算法的名字,beg和end表示演算法所操作的輸入範圍。幾乎所有演算法都接受一個輸入範圍,是否有其他引數依賴於要執行的操作。這裡列出了常見的一種----dest、beg2和end2,都是迭代器引數。顧名思義,如果用到了這些迭代器引數,它們分別承擔指定目的位置和第二個範圍的角色。除了這些迭代器引數,一些演算法還接受額外的、非迭代器的特定引數。

1)接受單個目標迭代器的演算法

  dest引數是一個表示演算法可以寫入的目的位置的迭代器。演算法假定:按其需要寫入資料,不管寫入多少個元素都是安全的。

  注意:向輸出迭代器寫入資料的演算法都假定目標空間足夠容納寫入的資料。

  如果dest是一個直接指向容器的迭代器,那麼演算法將輸出資料寫到容器中已存在的元素內。更常見的情況是,dest被繫結到一個插入迭代器或是一個ostream_iterator。插入迭代器會將新元素新增到容器中,因而保證空間是足夠的。ostream_iterator會將資料寫入到一個輸出流,同樣不管要寫入多少個元素都沒有問題。

2)接受第二個輸入序列的演算法

  接受單獨的beg2或是接受beg2和end2的演算法用這些迭代器表示第二個輸入範圍。這些演算法通常使用第二個範圍中的元素與第一個輸入範圍結合來進行一些運算。

  如果一個演算法接受beg2和end2,這兩個迭代器表示第二個範圍。這類演算法接受兩個完整指定的範圍:[beg, end)表示的範圍和[beg2, end2)表示的第二個範圍。

  只接受單獨的beg2(不接受end2)的演算法將beg2作為第二個輸入範圍中的首元素。此範圍的結束位置未指定,這些演算法假定從beg2開始的範圍與beg和end所表示的範圍至少一樣大。

 

3、演算法命名規範

  除了引數規範,演算法還遵循一套命名和過載規範。這些規範處理諸如:如何提供一個操作代替預設的<或==運算子以及演算法是將輸出資料寫入輸入序列還是一個分離的目的位置等問題。

1)一些演算法使用過載形式傳遞一個謂詞

  接受謂詞引數來代替<或==運算子的演算法,以及那些不接受額外引數的演算法,通常都是過載的函式。函式的一個版本用元素型別的運算子來比較元素;另一個版本接受一個額外謂詞引數,來代替<或==:

  unique(beg, end); // 使用==運算子比較元素

  unique(beg, end, comp); // 使用comp比較元素

2)_if版本的演算法

  接受一個元素值的演算法通常有另一個不同名的版本,該版本接受一個謂詞代替元素值。接受謂詞引數的演算法都有附加的_if字尾:

  find(beg, end, val); // 查詢輸入範圍中val第一次出現的位置

  find_if(beg, end, pred); // 查詢第一個令pred為真的元素

3)區分拷貝元素的版本和不拷貝的版本

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

  reverse(beg, end); // 反轉輸入範圍中元素的順序

  reverse_copy(beg, end, dest); // 將元素逆序拷貝到dest

  一些演算法同時提供_copy和_if版本。這些版本接受一個目的位置迭代器和一個謂詞:

  remove_if(v1.begin(), v1.end(), [](int i) { return i%2; }); // 從v1中刪除奇數元素

  remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) { return i%2; }); // v1不變,將v1的偶數元素拷貝到v2中

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::vector<int> v1 = { 1, 2, 3, 4, 5 };
13     auto end_it = remove_if(v1.begin(), v1.end(), [](int i) { return i % 2; });
14     for (auto iter = v1.begin(); iter != end_it; ++iter)
15     {
16         std::cout << *iter << " ";
17     }
18     std::cout << std::endl;
19     return 0;
20 }
View Code

 1 #include <iostream>
 2 #include <string>
 3 #include <algorithm>
 4 #include <numeric>
 5 #include <vector>
 6 #include <iterator>
 7 #include <functional>
 8 
 9 
10 int main()
11 {
12     std::vector<int> v1 = { 1, 2, 3, 4, 5 };
13     std::vector<int> v2;
14     remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int i) { return i % 2; });
15     for (auto iter = v1.begin(); iter != v1.end(); ++iter)
16     {
17         std::cout << *iter << " ";
18     }
19     std::cout << std::endl;
20     for (auto iter = v2.begin(); iter != v2.end(); ++iter)
21     {
22         std::cout << *iter << " ";
23     }
24     std::cout << std::endl;
25     return 0;
26 }
View Code

 

三、特定容器演算法

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

  連結串列型別定義的其他演算法的通用版本可以用於連結串列,但代價太高。這些演算法需要交換序列中的元素。一個連結串列可以通過改變元素減的連結而不是真正的交換它們的值來快速“交換”元素。因此,連結串列版本的演算法的效能比對應的通用版本好得多

  list和forward_list成員函式版本的演算法:

  這些操作都返回void。

操作 說明
lst.merge(lst2) 將來自lst2的元素合併入lst。lst和lst2都必須是有序的
lst.merge(lst2, comp) 元素將從lst2刪除。在合併之後,lst2變為空。第一個版本使用<運算子;第二個版本使用給定的比較操作
lst.remove(val) 呼叫erase刪除掉與給定值相等(==)或令一元謂詞為真的每個元素
lst.remove_if(pred)  
lst.reverse() 反轉lst中元素的順序
lst.sort() 使用sort或給定比較操作排序元素
lst.sort(comp)  
lst.unique() 呼叫erase刪除同一個值的連續拷貝。第一個版本使用==;第二個版本使用給定的二元謂詞
lst.unique(pred)  

  連結串列型別還定義了splice演算法,此演算法是連結串列資料結構所特有的。

  list和forward_list的splice成員函式的引數:

引數 說明
lst.splice(args) 或 flst.splice_after(args)

 

(p, lst2)

p是一個指向lst中元素的迭代器,或一個指向flst首前位置的迭代器。函式將lst2的所有位置移動到lst中p之前的位置或是flst中p之後的位置。

將元素從lst2中刪除。lst2的型別必須與lst或flst相同,且不能是同一個連結串列

(p, lst2, p2) p2是一個指向lst2中位置的有效的迭代器。將p2指向的元素移動到lst中,或將p2之後的元素移動到flst中。lst2可以是與lst獲flst相同的連結串列
(p, lst2, b, e) b和e必須表示lst2中的合法範圍。將給定範圍中的元素從lst2中移動到lst或flst。lst2與lst(或flst)可以是相同的連結串列,但p不能指向給定範圍中元素

  多數連結串列特有的演算法都與其通用版本很相似,但不完全相同。連結串列特有版本與通用版本間的一個至關重要的區別是連結串列版本會改變底層的容器