STL::演算法::常見演算法

Inside_Zhang發表於2015-11-11

總述

定位

泛型程式設計(GP)走了一條與物件導向程式設計(OOP)完全不同的道路,各種容器類的設計與實現也沒有走嚴格意義的繼承、介面機制。在STL的設計與實現中,演算法並非是容器類的成員函式,而是一種搭配迭代器(架起溝通演算法和容器類的橋樑)使用的全域性函式

這樣做的一個重要優勢在於:所有演算法只需實現一份(而不是在每一個容器類的內部都實現一份),就可以對所有容器運作,不必為每一個容器類量身定製。因為演算法本身也是模板化的,演算法可以操作不同型別之容器內的元素,也可以將之與使用者自定義的容器搭配。

演算法標頭檔案(header file)

欲使用C++標準庫的演算法,首先必須包含標頭檔案<algorithm>:

#include <algorithm>

某些STL演算法用於數值處理,如accumulate,被定義在<numeric>標頭檔案中。

如下程式碼所示,在使用STL演算法時,經常需要用到function object(仿函式物件)或者function adapter,定義在<functional>中。

<algorithm>包含了若干輔助函式:min(), max(), minmax(),這裡不妨考察下並不常見的minmax()


template<typename T>
bool ptr_less(const T* x, const T* y)
{
    return *x < *y;
}

template<typename T>
struct lesser :public std::binary_function<T, T, T>
{
    bool operator()(const T* x, const T* y)const
    {
        return *x < *y;
    }
}

int main(int, char**)
{
    int x = 17, y = 42, z = 33;
    int *px = &x, *py = &y, *pz = &z;
    std::pair<int*, int*> extremes = std::minmax({px, py, pz}, ptr_less<int>);
                            // 這裡用作比較的function 
                            // 似乎不能進行型別推導,
                            // 在STL的環境下是不區分function和function object(仿函式)的
    //std::pair<int*, int*> extremes = std::minmax({px, py, pz}, lesser<int>());
                            // function object
    cout << *extremes.first << " " << *extrmes.second << endl;
    return 0;
}

演算法過載

STL所提供的各種演算法,往往有兩個版本(過載),其中一個版本表現為最常用(或最直觀)的某種運算,第二個版本表現為最泛化的演算法流程,允許使用者以template引數來指定所要採行的策略

下面我們分別以accumulatesort函式為例:

// version 1
template<typename InputIterator, typename T>
T accumulate(InputIterator first, InputIterator last, T init = T())     
            // 其實對第三個引數賦初值不僅沒有帶來形式上的簡潔,
            // 如果使用預設引數的話,就無法進行引數型別推導
            // 反而使形式變得複雜,就這樣將就看吧
{
    while (first != last)
    {
        init += *first;
        ++first;    
            // init += *first++;
            // 雖然這樣可能帶來形式上的簡單,
            // 然而字尾++有一個天然的劣勢,區域性變數的建立
    }
    return init;
}

// version 2
tempalte<typename InputIterator, typename T, typename BinaryOperation>
T accumulate(InputIterator first, InputIterator last, 
            BinaryOperation binary_op, T init = T())
{
    while (first != last)
    {
        init = binary_op(init, *first);
        ++first;
    }
    return init;
}

int main(int, char**)
{
    vector<int> ivec{0, 1, 2, 3, 4, 5};
    cout << accumulate<vector<int>::const_iterator, vector<int>::value_type>
            (ivec.begin(), ivec.end()) << endl;
    return 0;
}

演算法accumulate用來計算init[first, last)內所有元素的和。可見出於演算法健壯性的考慮,必須提供一個初值init,雖然這個值可能為0,這麼做的原因之一是[first, last)區間長度為0(也即first == last)時,仍能返回一個有著明確定義的值。

STL中的演算法都是有著嚴格的規範或者定義的。

accumulate的行為順序有明確的定義,先將init初始化(傳參),然後針對[first, last)區間內的每一個迭代器,依序執行init = init + *i(第一個版本)或init = f(init, *i)(第二個版本)。
我們來實踐一下將區間中的每一個元素與init相乘。

template<typename Arg1, typename Arg2, typename Result>
struct binary_function
{
    typename Arg1 first_argument_type;
    typename Arg2 second_argument_type;
    typename Result result_type;
}
template<typename T>
struct multiply :public ::binary_function<T, T, T> 
            // 使用::域作用符避免與庫中的binary_function 相沖突
{
    T operator()(const T& x, const T& y) const      
            // 過載括號運算子,實現對傳遞進來的兩個引數的相乘操作
    {
        return x * y;
    }
}

int main(int, char**)
{
    vector<int> ivec {1, 2, 3, 4, 5};
    cout << accumulate<vector<int>::const_iterator, vector<int>::value_type>
            (ivec.begin(), ivec.end(), mulitply<vector<int>::value_type>(), 1) << endl;
    // 1*2*3*4*5 = 120
    return 0;
}

find 與 find_if

根據equality操作符,循序(++first)查詢[first, last)內的所有元素,找出第一個匹配等同(equality)條件者。如果找到,就返回一個InputIterator指向該元素,否則返回迭代器last

template<typename InputIterator, typename T>
InputIterator find(InputIterator first, InputIterator last, 
        const T& value)
{
    while (first != last && *first != value)
    {   
        ++first;
    }
    return first;       
        // 如果找到符合條件的值,while退出,返回指向元素的InputIterator
        // 如果沒有找到,此時first 會遍歷到last,
        // 並返回最終的last
}

我們放寬相等性equality約束,根據指定的pred(predicate:斷言)運算條件(function object),循序查詢[first, last)內的所有元素,找出第一個滿足斷言pred者(斷言的返回值是bool型別),也就是把pred(*pos)為真者,如果找到就返回一個InputIterator指向該元素,否則返回迭代器last

template<typename InputIterator, typename Predicate>
InputIteraor find_if(InputIterator first, InputIterator last, Predicate pred)
{
    while (first != last && !pred(*first))
    {
        ++first;
    }
    return first;
}   

我們以一個找到容器內第一個奇數值為例使用上述的algorithm介面:

template<typename T>
struct is_odd :public unary_function<T, bool>
{
    bool operator()(const T& x) const   
                        // 過載括號運算子,即位仿函式
    {
        return x % 2 != 0;
    }
}

int main(int, char**)
{
    vector<int> ivec {0, 2, 4, 3, 5};
    cout << *::find_if(ivec.begin(), ivec.end(), is_odd<int>()) << endl;
            // 使用::域作用符是為了避免與庫中的find_if發生混淆。
    return 0;
}

find_if給我們提供了一種對相等的定義的機會,也就是提供了一種比較的自由,而不是find那樣強加的一種對相等的判斷,尤其當牽涉到元素不只有一個元素時或者對涉及堆物件的比較時,就只能選擇更為靈活的find_if:

class Item
{
private:
    std::string _name;
    float _price;
public:
    Item(const std::string& name, float price):_name(name), _price(price){}
    std::string getName() const { return _name;}
    float getPrice() const { return _price;}
}

int main(int, char**)
{
    vector<Item*> books{new Item("C++", 10.), new Item("Python", 20.), new Item("Machine Learning", 30.)};
        // 當然更好的設計是使用shard_ptr智慧指標
    std::find(books.begin(), books.end(), new Item("C++", 10.));
        // 因為是比較的是堆物件,遍歷整個序列都不會相等,故find返回的迭代器等於books.end()
std::cout << boolalpha << find(books.begin(), books.end(), new Item("C++", 10.)) == books.end() << std::endl;
        // true
    // 我們來看find_if的用法
    find_if(books.begin(), books.end(), [](Item* book){return book->getName()=="C++"; })
    // 使用find_if,也即使用了自定義的相等性定義
    // 自然對多屬性值進行判斷時,可以更加自如地進行判斷
    return 0;
}

transform

在STL的演算法(<algorithm>)實現中,一般針對某一函式(也即某一特定演算法)都有兩個版本(或者叫函式過載),一種是基礎版本(表現為最常用或最直觀的那種版本),一種是泛化版本。

tranform()的第一個版本以一元仿函式op作用於輸入序列[first, last)中的每一個元素身上,並以其結果返回一個新序列。第二個版本以二元仿函式binary_op作用於一對元素身上,並以其結果產生一個新序列。如果第二個輸入序列的長度小於第一個序列,屬於undefined behavior
關於undefined behavior更詳細的討論請見<矯情的C++——不明確行為(undefined behavior)>

template<typename InputIterator, typename OutputIterator, typename UnaryOperation>
OutputIterator transform(InputIterator first, InputIterator last, 
        OutputIterator result, UnaryOperation op)
{
    for (; first != last; ++first, ++result)
        *result = op(*first);
    return result;
}

聽其函式名(transform),便可知其極具數學意義。

版本1的數學含義在於:

y=f(x)
y=f(x)

將輸入序列x
x
對映為輸出序列y
y
,當然這一變換(transformation)的性質取決於UnaryOperation的實現。如 y=x+5
y = x+5
,表示的就是線性變換(linear transformation)。


這裡寫圖片描述

template<typename InputIterator, typename OutputIterator, typename BinaryOperation>
OutputIterator transform(InputIterator first1, InputIterator last1, 
        InputIterator first2, OutputIterator result, 
        BinaryOperation binary_op)
{
    for (; first1 != last1; ++first1, ++first2, ++result)
        *result = op(*first1, *first2);
    return result;
}

版本二的數學含義:

y=f(x,y)
y=f(x, y)

例如本例的:y=x+y

y=x+y
,表示的就是一種二元關係,+是一種二元操作符,即必須有左操作符和右操作符。

客戶端程式碼:

template<typename T>
struct add_five :public std::unary_function < T, T >
{
    T operator()(const T& x) const
    {
        return x + 5;        // y=x+5;
    }
};

int main(int, char**)
{
    std::vector<int> ivec{ 0, 1, 2, 3 };
    std::vector<int> ivec2(ivec);

    std::vector<int> dst(ivec.size()/*-1*/);
    std::vector<int> dst2(ivec.size());

    std::transform(ivec.begin(), ivec.end(), dst.begin(), ::add_five<int>());
    std::copy(dst.begin(), dst.end(), std::ostream_iterator<int>(std::cout, " "));
    std::cout << std::endl;

    std::transform(ivec.begin(), ivec.end(), ivec2.begin(), dst2.begin(), ::plus<int>());
    std::copy(dst2.begin(), dst2.end(), std::ostream_iterator<int>(std::cout, " "));
    std::cout << std::endl;
    return 0;
}

無論是版本1還是版本,遍歷元素時,都是以第一個輸入序列為基準的,只要滿足第二個輸入序列的有效長度不低於第一個輸入序列的有效長度,以及輸出序列的有效長度不低於輸入序列的有效長度即可,自然在函式介面設計時,不需考慮第二個輸入學列的尾端迭代器及輸出序列的尾端迭代器,只一點由客戶端保證。


這裡寫圖片描述

transform拾遺

transform:y=f(x)

y=f(x)
,的本質是建立起一種輸入序列和輸出序列的對映(map)關係,通過上述程式碼我們可以發現,除非異常發生,輸入和輸出的序列長度是一致的,也即輸入與輸出一一對應,給定一個輸入獲得一個輸出,這不正是函式的定義嗎。transform的第二個版本也是如此,只不過從一元函式換成了二元函式z=f(x,y)
z=f(x, y)

transform函式輸出區間長度的獲得:

// 方式1,預先分配空間
vector<int> v1{...};
vector<int> v2(v1.size());
std::transform(v1.begin(), v1.end(), v2.begin(), func);

// 方式2,使用iterator進行尾插
vector<int> v1{...}; 
vector<int> v2;
std::transform(v1.begin(), v1.end(), std::back_inserter(v2), func);

相關文章