STL::演算法::常見演算法
總述
定位
泛型程式設計(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引數來指定所要採行的策略
。
下面我們分別以accumulate
和sort
函式為例:
// 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的數學含義在於:
將輸入序列
UnaryOperation
的實現。如 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;
}
版本二的數學含義:
例如本例的:
+
是一種二元操作符,即必須有左操作符和右操作符。
客戶端程式碼:
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:
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);
相關文章
- 常見加密演算法及常見加密演算法簡述加密演算法
- 常見排序演算法排序演算法
- 常見限流演算法演算法
- js常見演算法題JS演算法
- Javascript常見演算法整理JavaScript演算法
- JavaScript常見演算法集合JavaScript演算法
- 演算法題常見模板演算法
- JS常見演算法題目JS演算法
- 常見的排序演算法 (下)排序演算法
- 常見排序演算法總結排序演算法
- C++STL常見面試題C++面試題
- 常見的JavaScript面試演算法JavaScript面試演算法
- 常見國密演算法簡介演算法
- 常見壓縮演算法總結演算法
- 常見的排序演算法分析(一)排序演算法
- 常見演算法 PHP 實現 -- 堆排序演算法PHP排序
- 幾種常見排序演算法總結排序演算法
- 10種python常見的排序演算法!Python排序演算法
- JavaScript實現常見查詢演算法JavaScript演算法
- 人工智慧常見演算法簡介人工智慧演算法
- (新)app逆向四(常見加密演算法)APP加密演算法
- 常見一致性演算法演算法
- 常見排序演算法及複雜度排序演算法複雜度
- Javascript常見排序演算法的筆記JavaScript排序演算法筆記
- 讓前端面試不再難(常見演算法)前端面試演算法
- 幾種常見的排序演算法總結排序演算法
- C# 面試常見遞迴演算法C#面試遞迴演算法
- 幾種常見的JS遞迴演算法JS遞迴演算法
- 常見演算法技巧之——雙指標思想演算法指標
- 35.幾種常見的排序演算法排序演算法
- 負載均衡常見的演算法有哪些?負載演算法
- Java常見排序演算法之插入排序Java排序演算法
- 演算法之常見排序演算法-氣泡排序、歸併排序、快速排序演算法排序
- python實現常見的五種排序演算法Python排序演算法
- 常見的排序演算法:冒泡、快排、歸併排序演算法
- 一些常見的字串匹配演算法字串匹配演算法
- 棧的模擬實現及常見演算法演算法
- C++常見函式的基礎演算法C++函式演算法
- 常見的PID的演算法及程式碼示例演算法