能用STL演算法,絕不自己實現!

2020-07-16 10:05:23
前面章節已經介紹了很多演算法函數,比如 find()、merge()、sort() 等。不知讀者有沒有發現,每個演算法函數都至少要用一對疊代器來指明作用區間,並且為了實現自己的功能,每個函數內部都勢必會對指定區域內的資料進行遍歷操作。

舉幾個例子,find() 函數會對指定區域的資料逐個進行遍歷,確認其是否為要查詢的目標元素;merge() 函數內部也會分別對 2 個有序序列做逐個遍歷,從而將它們合併為一個有序序列;sort() 函數在對指定區域內的元素進行排序時,其底層也會遍歷每個元素。

事實上,雖然這些演算法函數的內部實現我們不得而知,但無疑都會用到回圈結構。可以這麼說,STL 標準庫中幾乎所有的演算法函數,其底層都是藉助迴圈結構實現的。

在此基礎上,由於 STL 標準庫使用場景很廣,因此很多需要手動編寫迴圈結構實現的功能,用 STL 演算法函數就能完成。舉個例子:
#include <iostream>     // std::cout
#include <algorithm>    // std::for_each
#include <string>       // std::string
#include <vector>       // std::vector
#include <functional>
using namespace std;

class Address {
public:
    Address(string url) :url(url) {};
    void display() {
        cout << "url:" << this->url << endl;
    }
private:
    string url;
};

int main() {
    vector<Address>adds{ Address("http://c.biancheng.net/stl/"),
                         Address("http://c.biancheng.net/java/"),
                         Address("http://c.biancheng.net/python/") };
    //手動編寫迴圈結構
    cout << "first:n";
    for (auto it = adds.begin(); it != adds.end(); ++it) {
        (*it).display();
    }
    //呼叫 STL 標準庫中的演算法函數
    cout << "second:n";
    for_each(adds.begin(), adds.end(), mem_fun_ref(&Address::display));
    return 0;
}
程式執行結果為:

first:
url:http://c.biancheng.net/stl/
url:http://c.biancheng.net/java/
url:http://c.biancheng.net/python/
second:
url:http://c.biancheng.net/stl/
url:http://c.biancheng.net/java/
url:http://c.biancheng.net/python/

可以看到,對於輸出 adds 容器中儲存的元素,除了可以手動編寫迴圈結構實現,還可以使用 STL 標準庫提供的 for_each() 函數。

那麼,手動編寫迴圈結構和呼叫 STL 演算法函數相比,哪種實現方式更好呢?毫無疑問,直接呼叫演算法會更好,理由有以下幾個:
  1. 演算法函數通常比自己寫的迴圈結構效率更高;
  2. 自己寫迴圈比使用演算法函數更容易出錯;
  3. 相比自己編寫迴圈結構,直接呼叫演算法函數的程式碼更加簡潔明瞭。
  4. 使用演算法函數編寫的程式,可延伸性更強,更容易維護;

後面 3 個理由相信讀者很容易理解,接下來重點講一下“為什麼演算法函數的效率更高”。

為什麼STL演算法效率更高

仍以上面程式為例,如下是我們手動編寫的迴圈程式碼:
for (auto it = adds.begin(); it != adds.end(); ++it) {
    (*it).display();
}
此段程式碼中,每一次迴圈都要執行一次 end() 方法,事實上該方法並不需要多次呼叫,因為它的值自始至終都沒有發生改變。也就是說,end() 方法只需要呼叫一次就夠啦,for_each() 函數就對這一點進行了優化:
for_each(adds.begin(), adds.end(), mem_fun_ref(&Address::display));
可以看到,通過將 end() 方法作為引數傳入 for_each() 函數,該方法只執行了 1 次。當然,這也僅是眾多優化中的一處。事實上,STL 標準庫的開發者對每個演算法函數的底層實現程式碼都多了優化,使它們的執行效率達到最高。

有讀者可能會說,難道我們自己對回圈結構進行優化不行嗎?可以,但是其執行效率仍無法和演算法函數相提並論。

一方面,STL 開發者可以根據他們對容器底層的了解,對整個遍歷過程進行優化,而這是我們難以做到的。以 deque 容器為例,該容器底層會將資料儲存在多個大小固定的連續空間中。對於這些連續空間的遍歷,只有 STL 開發者才知道這些連續空間的大小,才知道如何控制指標逐個遍歷這些連續空間。

另一方面,某些 STL 函數的底層實現使用了複雜的科學計算方法,並不是普通 C++ 程式設計師能駕馭的。例如,在實現對某個序列進行排序時,我們很難編寫出比 sort() 函數更高效的程式碼。

總之,STL 開發者比使用者更了解內部的實現細節,他們會充分利用這些知識來對演算法進行優化。

當然,只有熟悉 STL 標準庫提供的函數,才能在實際程式設計時想到使用它們。作為一個專業的 C++ 程式設計師,我們必須熟悉 STL 標準庫中的每個演算法函數,並清楚它們各自的功能。

C++ STL 標準庫中包含 70 多個演算法函數,如果考慮到函數的過載,大約有 100 多個不同的函數模板。本章僅介紹一些常用的演算法函數,如果想了解全部的 STL 演算法,讀者可參考 C++ STL標準庫官網。