对于顺序容器的操作,我们前面介绍了添加,删除,大小操作,但是我们还有比如查找,排序等操作,这些操作是基于算法的,C++给我们提供了很好的算法库,这些算法大部分在algorithm头文件中,我们使用这些算法的时候需要我们包含进头文件。
一般情况下,算法是不直接操作容器本身的,而是依赖于迭代器,通过迭代器进行容器的遍历,所以通过算法不能添加,删除元素等能够改变容器大小的操作。
在algorithm中,我们有一个find查找算法,基本算法是
f i n d ( i t e r 1 , i t e r 2 , v a l ) find(iter1,iter2,val) find(iter1,iter2,val)
意思是传入两个迭代器,表示查找的范围,最后一个val表示需要查找的值,如果找到,返回找到该元素的迭代器,如果没找到,返回find第二个参数。
vector<string> s{ "a","b","c","d","e","f","g","h" };
auto l = find(s.cbegin(), s.cend(), "d");
cout << *l << endl;
find查找算法是通过遍历容器,逐一跟val值进行匹配,如果匹配到了就返回指向该元素的迭代器,如果直到范围尾部都没有匹配,则返回范围尾部迭代器,当然这些操作都是通过迭代器进行操作的。
注意
迭代器让算法不依赖于容器,但是算法依赖于元素的类型
如同我们的find算法,它的底层使用的是 ==的运算符,所以我们的比较的类型需要能够使用该运算符才能够使用该算法。
[注意]:算法永远都不会操作于容器
泛型算法有超过100多个,此篇只举几个。泛型算法的基本结构大部分是相同的,如果对其他算法感兴趣可以百度查找或者阅读C++11文档
对于只读算法,find算法也是一种只读算法。
其他的只读算法还有accumulate,他是定义在numeric头文件中的。accumulate它能够让容器中的元素进行相加,它接收3个参数,前两个是元素的范围跟find一样,后面一个是相加的初始值。
accumulate的返回结果是根据最后一个参数类型而定的
vector<int> i{ 1,2,3,4,5,6 };
auto sum = accumulate(i.cbegin(), i.cend(), 0); // 把i容器相加,初始值为0
cout << sum << endl;
// 输出结果21
如果我们的初始值任然为0,但是把容器的类型改成double,并且将1改写成1.5,输出结果与预期的不符。
vector<double> i{1.5,2,3,4,5,6 };
auto sum = accumulate(i.cbegin(), i.cend(), 0); // 把i容器相加,初始值为0
cout << sum << endl;
// 原本想要的输出结果 21.5
// 实际输出结果 21
如果我们把初值改写成0.0,输出结果就为21.5.这就证明了accumulate的返回结果是根据初值来决定的。
我们可以拼接一个容器中的字符串,使用“” 的方式来拼接
vector<string> i{"hello","word","c++" };
auto sum = accumulate(i.cbegin(), i.cend(), string("")); // 把i容器相加,初始值为0
cout << sum << endl;
// 输出结果:hellowordc++
切记一点是,我们要把初始值用strin连接,如果不用string就变成了const char*类型,该类型没有+运算符。
对于只读的操作,建议选定元素范围的时候使用cbegin和cend,这样可以增加执行的效率
还有一种只读的比较两个容器大小的算法,equal
equal接收三个参数,和前面一样,前两个参数是第一个容器的输入范围,第三个是第二个容器比较序列的首元素
如果两个容器的对应的元素都一致,则返回true,如果不相等返回false。
由于算法是建立在迭代器而不是容器的基础上的,所以equal允许比较两个数据类型不同的容器,但是这两个容器的数据类型可以使用==运算符即可
还有一点就是,euqal的第二个序列必须是大于等于第一个序列的,否则报错。
写容器算法依然不是对容器进行操作,在执行写容器操作的时候要求写入元素的大小小于等于容器的大小。
算法并不检查写操作,只对迭代器操作,所以如果我们传进去的元素超出了容器大小,那这是违规的。因为迭代器访问了一个未定义的空间
写操作-- fill算法
fill算法接收三个参数,前两个参数传入元素的范围,第三个参数传入写入的值。
vector<int> i{ 1,2,3,4,5,6,7,8,9,10 };
fill(i.begin(), i.end(), 0); // 将i容器中的代码全部置0
for (auto a : i)
{
cout << a << " ";
}
// 输出结果:0 0 0 0 0 0 0 0 0 0
还有另外一个写入算法:fill_n
fill_n同样接收3个参数,第一个参数是写入是首元素的位置,第二个参数是写入元素的个数,第三个是写入的值
vector<int> i{ 1,2,3,4,5,6,7,8,9,10 };
fill_n(i.begin(), 5, 0); // 将前五个置0
for (auto a : i)
{
cout << a << " ";
}
// 输出结果:0 0 0 0 0 6 7 8 9 10
前面两个算法都是不能扩大容器的大小,因为受到迭代器的缘故,该迭代器只能在begin和end之间修改元素。所以下面要介绍另外一种算法能够在容器中的尾部添加元素-- back_inserter算法。
当然该算法依然不直接操作容器,而是迭代器的类型与前面的不一样,他操作的是一种插入迭代器,关于迭代器的知识将在另外一篇文章中讲到,这里不多扩展。
back_inserter是定义在iterator头文件中的函数。接收一个容器的引用,并且返回一个尾后迭代器。
vector<int> i;
auto s = back_inserter(i); // 返回一个尾后迭代器
*s = 12; // 向容器中添加元素
for (auto a : i)
{
cout << a << endl;
}
// 输出结果:12
我们常常使用back_inserter来创建一个迭代器来作为插入的初始位置。
vector<int> i;
fill_n(back_inserter(i), 10, 5); // 在i的末尾插入10个5
for (auto s : i)
cout << s << endl;
拷贝算法是将选中的容器范围拷贝到另外一个容器中,使用的是copy进行拷贝。
copy拷贝接收三个参数,前面两个接收的是输入的元素的范围,第三个是写入的第一个位置。copy返回的是拷贝尾元素之后位置的迭代器。
vector<int> i{1,2,3,4,5,6,7,9,};
vector<int> u;
copy(i.begin(), i.end(), back_inserter(u)); // 将i容器拷贝到u容器中
for (auto a : u)
cout << a << endl;
重排容器的元素比较经典的算法是sort排序算法,这个算法使用<运算符实现的,排序规则也是按照容器的排序规则实现。
vector<string> s{ "hello","word", "C++","slow","word" };
sort(s.begin(),s.end());
for (auto a : s)
cout << a << " ";
// 输出结果:C++ hello slow word word
可以发现容器中出现了相同的元素,我们可以消除重复元素。
消除重复元素使用的是unique函数,该函数接收两个参数,就是元素范围,虽然是消除,但也不是真正意义上的消除,他返回不重复元素的最后一个位置的迭代器,其余的重复元素依然在容器中,在该迭代器之后。所以还需要我们使用erase来进行删除。
vector<string> s{ "hello","word", "C++","slow","word" };
auto t = unique(s.begin(), s.end());
for (; t != s.end(); t++)
s.erase(t);
for (auto a : s)
cout << a << " ";
// 输出结果:hello word C++ slow word
对于sort操作,默认情况下我们只能够使用从小到大的顺序排列,当然我们也可以自定义大小。
谓词是一个可调用的表达式,返回结果是一个能够调用的值。标准库算法使用谓词分类是两类:一元谓词(只接收一个参数和二元谓词(只接收两个参数)。接收谓词的算法会调用谓词,并且传入相应的参数。
bool isbig(const int &i,const int &s)
{
return i > s;
}
int main()
{
vector<int> s{ 1,4,5,3,7,6,8,2,9,10 };
sort(s.begin(), s.end(),isbig); // sort传入一个谓词
for (auto a : s)
cout << a << " ";
// 输出结果:10 9 8 7 6 5 4 3 2 1
}
注意:传入的是isbig函数名不是函数,如果传入的是函数,则报错
我们使用如果要查找一个字符串中大于6的长度,我们可以使用find_if进行查找,find_if接收3个参数前两个是元素的范围,后面一个是查找的条件,如果我们传入一个谓词,那么这个谓词只能是一元谓词
bool elimdups(const string &i)
{
return i.size() > 6; // 查找关于大于6的字符串
}
int main()
{
vector<string> s{ "word","hello","aaaaaaa"};
auto d = find_if(s.begin(), s.end(), elimdups);
cout <<*d<< endl;
//输出结果:aaaaaaa
}
这段代码的缺陷在于,我们没办法指定需要传入的比较长度,大大降低了程序的灵活性。但是我们在isbig参数中不能传入两个参数。这个时候想要实现这个功能就要使用lambda表达式
我们可以向参数中传入可调用对象,我们所知道的可调用对象是函数和指针,接下来要讲解的lambda表达式也是可调用对象
lambda格式:[引用变量] (传入的参数)->返回类型 { 功能 }
auto f = [](string s)->string // 传入一个string参数,并且返回string类型
{
cout << s;
return s;
};
f("hello word");
// 输出结果:hello word
lambda表达式与普通的函数不相同,不能有自己的默认参数,也就是说实参的数量与形参的数量是相同的
auto f = [](int s,int i)->int // 传入两个int参数,并且返回int类型
{
cout << s+i; // 传入两个参数,让他们相加
return s + i;
};
f(1, 2);
在前面的代码中我们一直都没有用到捕获列表。捕获列表在很多情况下是空的,但是有的时候我们也是需要传入的。
上例我们使用find_if算法的时候,由于find_if只能够接收一个一元谓词,所以我们没办法传入一个比较的字符大小。但是我们可以使用lambda表达式进行比较。
int main()
{
string::size_type t = 6;
vector<string> s{ "word","hello","aaaaaaa" };
auto d = find_if(s.begin(), s.end(),
[t](const string &s)
{
return s.size() > t;
});
cout << *d << endl;
//输出结果:aaaaaaa
}
lambda表达式只有在捕获列表中捕获局部变量才能够调用该变量
捕获列表只能捕获非static的变量,其他的static变量和函数外部变量可以直接在lambda表达式中调用
与传参一样,lambda表达式的传参也分为值和引用。
我们之前使用的一直都是值传参,也就是说松门传入的变量要是能够进行拷贝的。
int s = 10;
auto f =
[s]
{
return s;
};
s = 1;
auto t = f();
cout << t << endl;
// 输出结果:10
因为f在初始化的时候传入的是s的拷贝,所以以后不管s怎么变f返回的都是创建时s的拷贝
使用引用传值则不一样,他存放的是捕获变量的地址,如果变量改变,那么值也随着改变。
int s = 10;
auto f =
[&s]
{
return s;
};
s = 1;
auto t = f();
cout << t << endl;
// 输出结果:1
在使用引用的时候必须确保引用变量的存在,当然,就像我们函数不能够返回函数的引用一样,lambda表达式也不能返回变量的引用。
除了显示捕获以外,我们也可以使用隐式捕获。在捕获列表中加上&或=来告诉编译器是使用引用不捕获还是值捕获
int s = 10;
auto f =
[=]
{
return s;
};
s = 1;
auto t = f();
cout << t << endl;
// 输出结果:10
在该例中,捕获列表中传入=表示值捕获,s通过隐式捕获拷贝进lambda。
当然,我们也可以使用&和=一起使用表示一部分是引用捕获,一部分是值捕获。
int s = 10;
int i = 100;
auto f =
[=,&i] // s是值捕获,i是引用捕获
{
return s;
};
注意:不能使用值捕获后在添加值捕获变量
以下错误释放
int s = 10; int i = 100; auto f = [=,i] { return s; };
在使用的=值捕获后面不能在添加值捕获变量i,可以添加&i
注意点二: 如果使用值和引用捕获的混合,捕获列表的开头必须是=或&,这样子就指定了默认捕获方式
前面引用后面值捕获也是一样的原理。
对于值捕获我们不能够修改捕获后拷贝的值的,如果想要修改那么可以使用mutable来修饰
int s = 10;
auto f =
[s]() mutable
{
return ++s;
};
s = 1;
auto t = f();
cout << t << endl;
// 输出结果:11
目前为止,我们写的lambda都是单一的return语句,在lambda表达式中,除了return语句以外的其他语句编译器都默认返回void类型。再有的情况下我们需要指定返回类型。
auto s = [](int i,int t )
{
return i >t?i:t;
};
如果我们将上面代码改写为以下代码则发生错误
auto s = [](int i,int t )
{
if(i>t)
return i;
else return t;
};
因为此时系统默认返回void但是我们返回了一个整数型,这个时候我们就需要指定返回类型
指定返回类型是在形参后面使用-> 返回类型 的方式进行返回。
auto s = [](int i,int t ) ->int
{
if(i>t)
return i;
else return t;
};