STL泛型算法
泛型算法概述
针对一段范围内的元素序列执行操作的一系列函数。
- 大多数算法定义在
algorithm
中,此外还有一组数值型泛型算法定义在numeric
中 - 叫
泛型
,是因为他们可以用于不同类型的元素和多种容器类型(不仅包括标准库类型,还包括内置的数组类型)。 - 叫
算法
,是因为他们实现了一些经典算法公共接口,如排序和搜索。
迭代器、容器、算法之间的关系
迭代器独立于特定容器
利用迭代器解引用运算符可以实现元素访问,甚至通过迭代器访问的元素可以不是容器元素,比如数据来自数组。
int a[] = {1, 2, 3, 4, 5, 6};
int val = 3;
int* result = find(std::begin(a), std::end(a), val); // 通过begin(a)和end(a)分别表示首元素和尾元素之后位置的指针,并传递给find
// 在子序列范围里面访问
auto result = find(a + 1, a + 3, val); // 在a[1], a[2], a[3]中查找元素
算法只执行迭代器操作
永远不会执行容器的操作,但会运行在迭代器之上,执行迭代器的操作。
即:算法不会改变容器的大小,但可能改变容器中保存的元素的值,通过迭代器操作可以实现在容器内移动元素,但不会执行添加或删除元素。
可能会依赖元素特性操作
某一种元素类型需要定义+
或者-
, 亦或是==
操作类型, 比如:
Find算法
:
-
三个参数,前两个入参为迭代器,表示一个范围,第三个参数是目标值
-
逐个遍历迭代器中的元素和定值相比较
-
返回指向第一个匹配的元素的迭代器,否则返回
第二个入参
-
依赖元素
==
比较操作,如果第三个参数类型不匹配,编译报错 -
vector
int val = 31;
vector<int> vec = {10, 23, 32};
auto result = find(vec.cbegin(), vec.cend(), val);
-
list
int num = 0;
string val = "a value";
list<string> list = {"a value", "xxx", "yyy"};
auto result = find(str.cbegin(), str.cend(), num);
报错: num类型与list容器类元素类型不匹配,不支持find里面 == 运算符操作
-
array
int num = 63;
int a[] = {32, 63, 55};
auto *result = std::find(std::cbegin(a), std::cend(a), num);
// result类型是 const int*
算法分类
只读容器算法
访问一个范围内的元素
只读取输入范围内的元素,不改变元素,迭代器区间
[a, b)
find
例子见前文
std::find
template <class InputIterator, class T> InputIterator find (InputIterator first, InputIterator last, const T& val);
Find value in range
Returns an iterator to the first element in the range [first,last) that compares equal to val. If no such element is found, the function returns last.
The function uses operator==
to compare the individual elements to val.
The behavior of this function template is equivalent to:
template<class InputIterator, class T>
InputIterator find (InputIterator first, InputIterator last, const T& val)
{
while (first!=last) {
if (*first==val) return first;
++first;
}
return last;
}
std::count
与find
类似, 只不过返回的范围内目标值的数量。
std::accumulate
- 定义在
numeric
头文件 - 第三个参数为初始值,最终返回范围内元素值累加的结果,决定了函数中使用哪个
+
运算符,以及返回结果的类型 - 要求元素定义
+
运算,因此string
也适用
总结:
- 对于只读操作,建议使用
cbegin()
和cend()
迭代器 - 对于读写操作,建议使用
begin()
和end()
迭代器
双序列的只读容器算法
equal
The behavior of this function template is equivalent to:
template <class InputIterator1, class InputIterator2>
bool equal ( InputIterator1 first1, InputIterator1 last1, InputIterator2 first2 )
{
while (first1!=last1) {
if (!(*first1 == *first2)) // or: if (!pred(*first1,*first2)), for version 2
return false;
++first1; ++first2;
}
return true;
}
从这里可以看出序列比较范围是由
first
和last
确定的。
- 说明:
-
将前两个参数表示第一序列范围,第三个参数表示第二序列迭代器开始位置
-
在第一序列范围内与第二序列对应元素都相等返回
true
, 否则返回false
-
第二序列元素数量少于第一个序列会返回
false
,不会报错; 多余则没有影响
-
测试代码:
std::list<string> list = {"a value", "from a", "to b"};
std::list<string> lst_less = {"a value", "from a"};
std::list<string> lst_more = {"a value", "from a", "find c"};
auto res1 = std::equal(list.cbegin(), list.cend(), list.cbegin()); // true 和自己比较
auto res2 = std::equal(list.cbegin(), list.cend(), lst_less.cbegin()); // false
auto res3 = std::equal(list.cbegin(), list.cend(), lst_more.cbegin()); // false
写容器算法
将新值赋给序列中的元素
std::fill
将指定的值填充到指定的序列
Fill range with value
Assigns val to all the elements in the range [first,last).
The behavior of this function template is equivalent to:
template <class ForwardIterator, class T>
void fill (ForwardIterator first, ForwardIterator last, const T& val)
{
while (first != last) {
*first = val;
++first;
}
}
测试代码:
std::list<string> list = {"a value", "from a", "to b"};
std::fill(list.begin(), list.end(), "1"); // {"1", "1", "1"}
std::fill_n
-
- 将新值赋给指定序列中的指定个数的元素
- 第一个参数迭代器,表示序列;第二个参数为数量; 第三个参为数值,填充所用的值
测试代码:
vector<int> vec;
std::fill_n(vec.begin(), vec.size(), 0);
std::fill_n(vec.begin(), 10, 0); // 报错,vec中没有足够的元素,改语句的结果未定义
报错如图所示:
说明:
向目迭代器所指向的位置写入数据的位置空间要足够大,能容纳要写入的元素。
std::back_inserter
插入迭代器(back_inserter,inserter, front_inserter)
创建一个使用push_back的迭代器。一种用于为容器添加元素的迭代器,其设计目的是避免容器中的原元素被覆盖,在容器的末尾自动插入新元素。
std::back_inserter (Container& x);
x:Container in which new elements will
be inserted at the end.
return:A back_insert_iterator that inserts
elements at the end of container x.
- 说明
- 接收指向容器的引用,返回一个插入迭代器
- 通过插入迭代器赋值,赋值运算符会调用
push_back
将元素添加到容器中 - 通过该迭代器再调用
fill_n
就会添加n
个元素
测试代码:
// Declaring second container for
// copying values
vector<int> v2 = { 4, 5, 6 };
// Using std::back_inserter inside std::copy
std::copy(v1.begin(), v1.end(), std::back_inserter(v2));
// v2 now contains 4 5 6 1 2 3
vector<int> vec;
auto it = back_inserter(vec);
*it = 42;
std::copy
Copy range of elements
Copies the elements in the range [first,last)
into the range beginning at result.
The function returns an iterator to the end of the destination range (which points to the element following the last element copied).
The ranges shall not overlap in such a way that result points to an element in the range [first,last)
. For such cases, see copy_backward.
The behavior of this function template is equivalent to:
template<class InputIterator, class OutputIterator>
OutputIterator copy (InputIterator first, InputIterator last, OutputIterator result)
{
while (first!=last) {
*result = *first;
++result; ++first;
}
return result;
}
- 向目的序列的元素写入数据
- 第一、二个参数代表范围,第三个参数代表目的序列的起始位(目的序列至少要包含与输入序列一样多的元素)
- 返回目的位置的迭代器(递增后)的值,示例中
ret
指向a2
尾元素之后的位置
测试代码:
int a1[] = (0, 2, ,3 ,4 ,5 ,6 ,7 ,8, 9);
int a2[sizeof(a1)/sizeof(*a1)]; // a2与a1大小相同
fill/auto ret = std::copy(begin(a1), end(a1), a2); // ret指向拷贝到a2的末尾元素之后的位置
std::replace & std::replace_copy
指定范围内元素替换
手册定义:
Replace value in range
Assigns new_value to all the elements in the range [first,last) that compare equal to old_value.
The function uses operator== to compare the individual elements to old_value.
The behavior of this function template is equivalent to:
template <class ForwardIterator, class T>
void replace (ForwardIterator first, ForwardIterator last,
const T& old_value, const T& new_value)
{
while (first!=last) {
if (*first == old_value) *first=new_value;
++first;
}
}
例子:
int myints[] = { 10, 20, 30, 30, 20, 10, 10, 20 };
std::vector<int> myvector (myints, myints+8); // 10 20 30 30 20 10 10 20
std::replace (myvector.begin(), myvector.end(), 20, 99); // 10 99 30 30 99 10 10 99
删除容器算法
std::remove
Transforms the range [first,last) into a range with all the elements that compare equal to val removed, and returns an iterator to the new end of that range.
template <class ForwardIterator, class T>
ForwardIterator remove (ForwardIterator first, ForwardIterator last, const T& val)
{
ForwardIterator result = first;
while (first!=last) {
if (!(*first == val)) {
if (result!=first)
*result = move(*first);
++result;
}
++first;
}
return result;
}
remove
也需要一对迭代器来指定所要进行操作的元素区间。因为它并没有容器作为参数,所以remove
本身并不知道这些元素被存放到哪个容器中。并且,remove
并不能从迭代器获知对应的容器以及容器类型。
结论:如果要从容器中删除元素需要调用人哦年起成员函数erase
(除了list)。
vector<int> c = {0,1,2,3,4,5,6,7,8,9,1};
cout << "size : " << c.size() << endl;
remove(c.begin(), c.end(), 1);
cout << "size : " << c.size() << endl;
vector<int>::iterator newEnd(remove(c.begin(), c.end(), 1));
vector<int>::iterator iter = c.begin();
while(iter + 1 != newEnd) {
cout << *iter << " ";
iter++;
}
cout << endl;
执行结果:
size : 11
size : 11
0 2 3 4 5 6 7 8 9
可以看到remove
并没有真正从容器里删除元素;remove
移动了区间中的元素。其结果是,“需要被删除”的元素被移到了区间的尾部。remove
返回一个新的迭代器,指向第一个“需要被删除”的元素。
-
remove
之前
-
remove
之后
vector<int>::iterator newEnd(remove(c.begin(), c.end(), 1));
可以看到最后两个元素的值没有发生变化。在内部,remove
遍历整个区间,用需要保留的元素的值覆盖掉那些要被删除的元素的值,这种覆盖是通过对那些需要被覆盖的元素的赋值来完成的。
元素的移动对应关系如下图所示:
因此,要想删除这些元素,必须调用区间形式的 erase
。
还用两个类似的算法:remove_if
和 unique
。都需要在调用 remove_if
和 unique
后调用 erase
。
其中 list 的调用是不一致的,list::remove
会真正删除元素(并且比使用 erase-remove
习惯用法更加高效),list::unique 也会真正删除元素(并且比使用 erase-unique
更加高效)
注意:当容器中存放的是指向动态分配的对象的指针时,应该避免使用 remove
和类似的算法(remove_if
和 unique
)。
可以使用智能指针。
参考资料
- 《C++ Primer 第5版》 第10章 泛型算法
- https://cplusplus.com/reference/algorithm/find/
- https://cplusplus.com/reference/algorithm/equal/
- https://cplusplus.com/reference/algorithm/fill/
- https://cplusplus.com/reference/algorithm/fill_n/