[关闭]
@fcxxzux 2017-02-02T17:21:34.000000Z 字数 14274 阅读 1372

Learning a Part of C++(for ACM/ICPC) (6) STL算法

我们先来讲解STL的算法们。
先讲的原因比较简单,你可以在数组上直接使用,同时,实在是很省功夫啊。

1、最大和最小

在开始这个话题前,我们先讲一个关键点:

Tips 01

在STL中,任何涉及到大小比较的地方,都会使用<(小于号)以及其等效的,进行小于比较的函数来进行。

那么,我们写代码的时候,总要取最大值/最小值,有时要整个数组的最大值/最小值,欢迎来偷懒使用max()/min()/max_element()/min_element()

  1. int a=5,b=3;
  2. int max_ab=std::max(a,b); //5
  3. int min_ab=std::min(a,b); //3
  4. int arr[]={-5,8,3,4,9,2,-10};
  5. int max_of_arr=*(std::max_element(arr, arr + 7)); //9
  6. int min_of_arr=*(std::min_element(arr, arr + 7)); //-10

当然,我们也可以像用sort时一样,指定一个比较函数,比如说,呃,我们来求绝对值最小的:

  1. bool abs_cmp(int a,int b){
  2. return abs(a)<abs(b);
  3. }
  4. int arr[]={-5,8,3,4,9,2,-10};
  5. int max_of_arr=*(std::max_element(arr, arr + 7, abs_cmp)); //-10
  6. int min_of_arr=*(std::min_element(arr, arr + 7, abs_cmp)); //2

顺带一提:

Tips 02

在STL中,涉及到一个(元素存储的)地址范围的,永远采取 左闭右开 的形式。
也就是说,开始迭代器指向第一个有效元素,结束迭代器指向最后一个有效元素之后。

2、有关排序

排序的sort()其实我们在前面的运算符重载一章中提到过了(https://www.zybuluo.com/fcxxzux/note/292911),这里不会对讲过的内容再次重复。但是还有一些东西我们要在这里提一下:

1 . 请一定要定义严格的小于号!请一定要定义严格的小于号!请一定要定义严格的小于号!
不然真的会出现包括超时、运行时错误(爆栈) 等问题。
具体解释的话:
正常情况下,只有一个小于号,怎么知道2个数的关系呢?
通过2次小于比较:a < b和b < a

a < b b < a 结论
True False a 小于 b
False True a 大于 b
False False a 等于 b
True True ??????

两者都为真,算什么东西啊?然后这个排序函数自己就混乱了……
所以,请不要:

  1. bool cmp(int a,int b){
  2. return a<=b;
  3. }

请:

  1. bool cmp(int a,int b){
  2. return a<b;
  3. }

2 . sort()的时候,自定义函数,如果是结构体,请一定要传常引用,请一定要传常引用,请一定要传常引用
不然网络赛的时候超时了我不负责,我已经尽了告知义务了。

3 . 有关 stable_sort() 与 排序稳定性:

有同学自己上网找,发现了stable_sort()这个函数,然后问为什么不用。
这个问题的话,主要问题是,stable_sort()在大部分情况下跑得没有sort()快(除非数据基本有序,两者运行时间可能stable_sort()好一点)。
那为什么STL要提供stable_sort()呢?
注意到,前面有个单词,stable,意思是,稳定的。
稳定的排序?
这就涉及到了排序稳定性的问题。

排序稳定性,不是说执行时间的确定性,而是指:
有多个同关键词的数据,在排序后,保证他们先后顺序不变的特性。

这么说有点抽象,不妨来做实验:

Exp 01

请执行以下代码,观察结果:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <algorithm>
  4. using namespace std;
  5. const int n=100;
  6. struct Person{
  7. int id;
  8. int score;
  9. Person(){}
  10. Person(int a,int b):id(a),score(b){}
  11. bool operator<(const Person& b) const{
  12. return score>b.score;
  13. }
  14. }lista[n],listb[n];
  15. int main()
  16. {
  17. for(int i=0;i<n;i++){
  18. listb[i]=lista[i]=Person(i+1,rand()%10);
  19. }
  20. sort(lista,lista+n);
  21. stable_sort(listb,listb+n);
  22. for(int i=0;i<n;i++){
  23. printf("%d %d",lista[i].id,lista[i].score);
  24. printf("--%d %d\n",listb[i].id,listb[i].score);
  25. }
  26. return 0;
  27. }

可以发现:
同一个score的情况下,sort排序出来的结果,id的顺序已经完全打乱,
stable_sort得到的结果,id的顺序保持和原始给定的顺序一样。
这就是排序稳定性的意义。这个排序稳定性有时候做题进行排序的时候,是有必要保证的,这个要靠大家自己去甄别了。

4 .(扩展知识)使用C++11的lambda表达式来作为自定义排序规则。

如果你说:诶呀,我这个特定的比较规则可能只用一次,不想再回到前面专门写个函数,怎么办啊?

不用担心,C++11引入了一个新东西:lambda表达式
我们可以这样写:

  1. struct Person{
  2. int id;
  3. int score;
  4. Person(){}
  5. Person(int a,int b):id(a),score(b){}
  6. }lista[n];
  7. sort(a,a+n,[](const Person& a, const Person& b){
  8. return a.score>b.score; // 按分数从高到低排序
  9. });
  10. sort(a,a+n,[](const Person& a, const Person& b){
  11. return a.id<b.id; // 按id从小到大排序
  12. })

注意到,我们的[](const Person& a, const Person& b){return a.id<b.id;}部分,就是所谓的lambda表达式了。
[]表示捕获变量,()中的为参数,sort需要2个参数(小于关系是二元谓词嘛,二元,就2个参数),{}中的为具体的代码了。
总体来讲,就是个不用取名、可以不用写返回值类型的函数(返回值类型在编译期间自动推导)。

注意:lambda 表达式是从C++11开始支持的,如果想在比赛中使用,请确定比赛平台的编译器支持C++11/你自己选择的提交语言是对应开启C++11支持的。

竞赛中所需的lambda表达式基本就是这样,如果想要了解更多,请参阅:
http://zh.cppreference.com/w/cpp/language/lambda
https://msdn.microsoft.com/zh-cn/library/dd293608.aspx

3、排序之后(1) —— 二分查找

一般我们排序之后要干嘛?
利用有序性,O(n)处理过去以外,
有时候我们需要通过二分查找,确定有没有某个数值/最小的大于特定值的数值等等等等。
STL贴心的考虑了我们的需求,提供了二分查找的函数们:lower_boundupper_boundbinary_searchequal_range

这三个函数的参数格式一模一样:
xxxxxx(开始位置, 结束位置, 待查找数值 [,自定义比较函数])
主要差别在返回值上了。

首先,binary_search:返回bool,表示待查找数值在这个查找范围内有没有存在,有就返回true,否则返回false

其次:lower_boundupper_bound返回一个迭代器
lower_bound指向第一个大于等于这个待查找数值的迭代器
upper_bound指向第一个大于这个待查找数值的迭代器
……好难记啊。

没事,结合equal_range反而好记:
equal_range返回一个pair<迭代器, 迭代器>
其中.first指向第一个大于等于待查找数值的地方,
.second指向最后一个等于待查找数值的地方之后一个位置,
或者说,假设equal_range(array, array + n, 10)的返回值为x那么[x.first, x.second)这一段之内的东西的值都为10。(你看,还是左闭右开)
而且,x.first就是由lower_bound得到的,x.second就是由upper_bound得到的

注意:lower_boundupper_boundequal_range 他们的返回值都是迭代器(或者一对迭代器),你要知道下标?和数组的开始位置做个减法。

Tips 03

大家应该都知道,二分查找要基于随机访问(通过下标直接跳转)。
那么,请不要把非随机访问迭代器给这些二分查找函数,不然会使用O(n)的算法去确定要查找的对象的,这样就慢飞了。
或者说,除了:数组指针、vector的迭代器、deque的迭代器、自定义的随机访问迭代器,其他的迭代器不要给这些二分查找函数

还是很抽象?那就举例子吧:

Exp 02

请执行以下代码,观察结果:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <algorithm>
  4. using namespace std;
  5. int main()
  6. {
  7. int a[6]={0,1,2,2,2,3};
  8. printf("lower_bound: %d\n",lower_bound(a,a+6,2)-a); // 2
  9. printf("upper_bound: %d\n",upper_bound(a,a+6,2)-a); // 5
  10. return 0;
  11. }

Tips 04

lower_boundupper_boundequal_range的返回值均是迭代器。
要得到下标,请把返回结果和首指针/首迭代器相减。
(如果只是想取值,直接用*解引用/->通过迭代器解引用+访问成员就行了)


Bonus 01
对自定义函数进行二分查找

一般情况下,我们都认为,这种lower_bound / upper_bound是没法用于函数的二分查找的。
比如下面这个题:

现在一种饮料在促销,3个瓶盖换1瓶新的饮料。(当然,每瓶只有1个瓶盖)
你现在想喝至少n瓶(),问在不能和别人借瓶盖的情况下,你最少要花钱买多少瓶饮料。

显然,很容易写出一个函数,表示买x瓶饮料,最后最多能喝到多少瓶。
然后我们用二分查找。好吧,手写一个

二分查找写多了,你会发现,其实这个东西很容易写错导致死循环……
然后要不死记硬背,要不偷懒:把数量缩小到很小的规模,然后就3~5个东西暴力枚举一下。
那么,我们有办法利用STL里的这些二分查找函数来找到解吗?

当然可以!就是,非常需要技巧……
(我目前只找到下面这个解决方案,如果你找到其他的,请告诉我)

大体思路:我们把这个需要二分查找的函数,包装成一个随机访问迭代器,然后我们可以喂给STL的二分查找函数了!
我的实现如下:

  1. #include <stdio.h>
  2. #include <algorithm>
  3. using namespace std;
  4. typedef long long ll;
  5. struct MyPtr:public std::iterator<std::random_access_iterator_tag, int/*这里是解引用后的值类型,或者说,迭代器指向的值类型,根据需要换掉*/>{
  6. MyPtr(int x=0):val(x){}
  7. int val;
  8. //这里是你自定义函数的地方
  9. int f(){
  10. int x=val,ans=val,left=0;
  11. while(x>=3){
  12. left=x%3;
  13. ans+=x/3;
  14. x/=3;x+=left;
  15. }
  16. return ans;
  17. }
  18. int operator*(){
  19. return f();
  20. }
  21. MyPtr operator+(const int x){
  22. return MyPtr(val+x);
  23. }
  24. MyPtr& operator++(){
  25. ++val;
  26. return *this;
  27. }
  28. int operator-(const MyPtr& x)const{
  29. return val-x.val;
  30. }
  31. MyPtr& operator+=(const int x){
  32. val+=x;
  33. return *this;
  34. }
  35. };
  36. int main(){
  37. int n;
  38. while(~scanf("%d",&n)){
  39. printf("%d\n",lower_bound(MyPtr(0),MyPtr(n+1),n).val);
  40. }
  41. return 0;
  42. }

(参考数据: 输入 15084,输出 10047 )

可以看到,我们继承了std::iterator<>,还声明自己是随机访问迭代器,返回值类型是int,然后实现了很多的运算符:+ 整数、 += 整数 、 前缀++ 、 - 自身 、 * 解引用(计算结果)
……说实话,写完以后发现,好像太长了……
反正大家当看个热闹吧,如果觉得有用的同学可以学去。
(不过真的,和抄一个二分查找的函数相比,性价比太低了)

4、排序之后(2) —— 去重与离散化

当然,还有一类很重要、很常用的情况:

这没问题,STL里提供了一个函数,unique,能满足你的需要。

unique的参数为:unique(开始位置, 结束位置[, 自定义比较函数])
请注意,unique因为只关心2个元素是否一样,所以unique需要你实现

这一点和sortlower_bound等函数还是很不一样的。

还有就是,unique是有返回值,而且一般情况下都需要用到的。
unique的返回值是个迭代器,表示 去重后 剩下的元素的区间的结束位置。

举例:

  1. #include <stdio.h>
  2. #include <algorithm>
  3. using namespace std;
  4. typedef long long ll;
  5. struct People{
  6. People(int a=0,int b=0):id(a),val(b){}
  7. int val;
  8. int id;
  9. bool operator==(const People&b)const{
  10. return val==b.val;
  11. }
  12. };
  13. int main(){
  14. People a[8]={People(0,5),People(1,5),
  15. People(2,6),People(3,6),People(4,6),
  16. People(5,5),People(6,5),
  17. People(7,4)};
  18. printf("unique item=%d\n",unique(a,a+8)-a);
  19. for_each(a,a+8,[](const People & x){printf("%d %d\n",x.id,x.val);});
  20. puts("");
  21. return 0;
  22. }
  23. /*
  24. 可能的输出:
  25. unique item=4
  26. 0 5
  27. 2 6
  28. 5 5
  29. 7 4
  30. 4 6
  31. 5 5
  32. 6 5
  33. 7 4
  34. */

注意到:

所以,unique的具体实现类似:

  1. template <class ForwardIterator>
  2. ForwardIterator unique (ForwardIterator first, ForwardIterator last)
  3. {
  4. if (first==last) return last;
  5. ForwardIterator result = first;
  6. while (++first != last)
  7. {
  8. if (!(*result == *first)) // or: if (!pred(*result,*first))
  9. *(++result)=*first;
  10. }
  11. return ++result;
  12. }

所以,用unique的时候:


来2个例题吧:

例题A、CodeForces 631B Print Check

2种操作:整行涂某个颜色,或者整列涂某个颜色。
输出涂色完以后的图形情况。
其中:操作个数<=100000,行数、列数<=5000,行数*列数<=100000

暴力做法:直接开个数组,然后把所有操作,按顺序做下去,再输出——铁定超时。

小小的优化:
显然,对某行/某列,只有最后一次操作是有效的。
而行数+列数,最多也就不超过10000个(其实最多是5000+20个),那就意味着,我不需要都做。我只要找到每行/每列上的最后一次操作,然后按顺序执行那些操作就行了。这样需要的计算量10000*5000,5千万,CF上没问题。

在具体实现这个思路的时候:
搞一个结构体,记录
执行的顺序、操作类型、操作的行号/列号,还有要涂的颜色

读入所有操作后,按
1、操作类型
2、操作的行号/列号
3、执行顺序的倒序
依次排序。
(这样,相同的操作在一起了,然后按执行顺序倒序排,这样最后的操作在最前面)
然后用unique去重,最后再次排序,对剩下的操作,恢复正确的操作顺序,并依次执行,即可。

这题的正解是
开2个数组,记录某一行/某一列最后一次涂色是什么时候,涂了什么颜色。
之后在输出的时候,每个位置上的颜色,就是相应的行和列上最迟涂的颜色了。
当然,这里是强行展示unique的用处了。

  1. #include <stdio.h>
  2. #include <ctype.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. #include <limits.h>
  6. #include <math.h>
  7. #include <algorithm>
  8. using namespace std;
  9. typedef long long ll;
  10. struct OP{
  11. int idx,o,x,c;
  12. void get(){
  13. scanf("%d%d%d",&o,&x,&c);
  14. }
  15. bool operator==(const OP &b)const{
  16. return o==b.o&&x==b.x;
  17. }
  18. bool operator<(const OP &b)const{
  19. if(o!=b.o)return o<b.o;
  20. if(x!=b.x)return x<b.x;
  21. return idx>b.idx;
  22. }
  23. }op[100005];
  24. bool cmp(const OP &a,const OP &b){
  25. return a.idx<b.idx;
  26. }
  27. int n,m,k;
  28. int arr[5005][5005];
  29. int main(){
  30. scanf("%d%d%d",&n,&m,&k);
  31. for(int i=0;i<k;i++){
  32. op[i].idx=i;
  33. op[i].get();
  34. }
  35. sort(op,op+k);
  36. int last_idx=unique(op,op+k)-op;
  37. sort(op,op+last_idx,cmp);
  38. for(int pid=0;pid<last_idx;pid++){
  39. if(op[pid].o==1){
  40. int x=op[pid].x;
  41. int c=op[pid].c;
  42. for(int j=1;j<=m;j++)arr[x][j]=c;
  43. }else{
  44. int x=op[pid].x;
  45. int c=op[pid].c;
  46. for(int j=1;j<=n;j++)arr[j][x]=c;
  47. }
  48. }
  49. for(int i=1;i<=n;i++){
  50. for(int j=1;j<=m;j++){
  51. printf(j==m?"%d\n":"%d ",arr[i][j]);
  52. }
  53. }
  54. return 0;
  55. }

例题B、hihocoder #1368 积水的城市2

地址:(https://hihocoder.com/problemset/problem/1368)或者(http://blog.csdn.net/fcxxzux/article/details/52435342

具体思路就不在这里展开了,看我的博客(http://blog.csdn.net/fcxxzux/article/details/52435342),有具体的解释。

主要说明一下:如何用STL的那些算法函数(sort、unique、lower_bound)来实现这个离散化。

这种2维的网格图,x轴和y轴分开离散化即可。
我们下面以x轴的离散化为例。

1、把所有要参与离散化的数值,放入数组。
2、把这个数组用sort进行排序,用unique进行去重,并记录下最后不重复元素的数量

  1. //第74~75行
  2. sort(axis,axis+cnt);
  3. cnt=unique(axis,axis+cnt)-axis;

这样其实我们就完成了离散化的预处理操作。

至于查询的时候:
某个元素的新的坐标,就是这个元素,在这个数组里的下标。
如果我们想知道某个老坐标值的新的坐标,那使用lower_bound进行二分查找即可。

  1. //第85行
  2. lower_bound(axis,axis+cnt,val)-axis

如果我们想知道某个新坐标值对应的原来的老坐标值,更简单了,直接数组下标访问即可。

  1. //第80行
  2. length[i+1]=orilength[axis[i]]-orilength[axis[i-1]];
  3. //其中axis[i]和axis[i-1]取到的均是原来的坐标值。

使用STL算法函数sortuniquelower_bound实现的离散化处理的本题,参考代码如下:

  1. #include <stdio.h>
  2. #include <ctype.h>
  3. #include <string.h>
  4. #include <stdlib.h>
  5. #include <limits.h>
  6. #include <math.h>
  7. #include <algorithm>
  8. #include <queue>
  9. using namespace std;
  10. typedef long long ll;
  11. const int di[4][2]={{-1,0},{1,0},{0,1},{0,-1}};
  12. struct Point{
  13. int x,y;
  14. Point(){}
  15. Point(int a,int b):x(a),y(b){}
  16. Point go(int i){
  17. return Point(x+di[i][0],y+di[i][1]);
  18. }
  19. bool check(int n,int m){
  20. return x>=1&&y>=1&&x<=n&&y<=m;
  21. }
  22. };
  23. int Alength[1005];
  24. int Blength[1005];
  25. int K;
  26. int cross[35][2];
  27. int xaxis[105],xcnt=0;
  28. int yaxis[105],ycnt=0;
  29. ll dis[105][105];
  30. bool walk[105][105];
  31. int colLength[105];
  32. int rowLength[105];
  33. ll solve(int xa,int ya,int xb,int yb){
  34. int n=xcnt,m=ycnt;
  35. dis[xa][ya]=0;
  36. queue<Point> q;
  37. q.push(Point(xa,ya));
  38. while(!q.empty()){
  39. Point x=q.front();q.pop();
  40. ll disx=dis[x.x][x.y],disy;
  41. for(int i=0;i<4;++i){
  42. Point y=x.go(i);
  43. if(y.check(n,m)&&walk[y.x][y.y]){
  44. switch(i){
  45. case 0:disy=disx+colLength[y.x+1];break;
  46. case 1:disy=disx+colLength[y.x];break;
  47. case 2:disy=disx+rowLength[y.y];break;
  48. case 3:disy=disx+rowLength[y.y+1];break;
  49. }
  50. if(disy<dis[y.x][y.y]){
  51. dis[y.x][y.y]=disy;
  52. q.push(y);
  53. }
  54. }
  55. }
  56. }
  57. return dis[xb][yb]==(ll)INT_MAX*100000?-1:dis[xb][yb];
  58. }
  59. void addPoint(int v,int* axis,int& cnt){
  60. axis[cnt++]=v;
  61. axis[cnt++]=v+1;
  62. }
  63. void fix(int* axis,int& cnt,int n,int *orilength,int *length){
  64. axis[cnt++]=1;
  65. sort(axis,axis+cnt);
  66. cnt=unique(axis,axis+cnt)-axis;
  67. while(axis[cnt-1]>n) --cnt;
  68. fill(length,length+105,0);
  69. for(int i=1;i<cnt;i++){
  70. length[i+1]=orilength[axis[i]]-orilength[axis[i-1]];
  71. }
  72. }
  73. int getIndex(int* axis,int cnt,int val){
  74. return lower_bound(axis,axis+cnt,val)-axis+1;
  75. }
  76. inline void out(ll x) {
  77. if(x<0){
  78. putchar('-');
  79. out(-x);
  80. return;
  81. }
  82. if(x>9) out(x/10);
  83. putchar(x%10+'0');
  84. }
  85. int main(){
  86. int n,m;
  87. scanf("%d%d",&n,&m);
  88. for(int i=2;i<=n;++i) scanf("%d",&Alength[i]),Alength[i]+=Alength[i-1];
  89. for(int i=2;i<=m;++i) scanf("%d",&Blength[i]),Blength[i]+=Blength[i-1];
  90. scanf("%d",&K);
  91. for(int i=0;i<K;++i) scanf("%d%d",&cross[i][0],&cross[i][1]);
  92. int Q,xa,ya,xb,yb;
  93. for(scanf("%d",&Q);Q--;){
  94. scanf("%d%d%d%d",&xa,&ya,&xb,&yb);
  95. xcnt=ycnt=0;
  96. addPoint(xa,xaxis,xcnt);
  97. addPoint(xb,xaxis,xcnt);
  98. addPoint(ya,yaxis,ycnt);
  99. addPoint(yb,yaxis,ycnt);
  100. for(int i=0;i<K;++i){
  101. addPoint(cross[i][0],xaxis,xcnt);
  102. addPoint(cross[i][1],yaxis,ycnt);
  103. }
  104. fix(xaxis,xcnt,n,Alength,colLength);
  105. fix(yaxis,ycnt,m,Blength,rowLength);
  106. fill(dis[0],dis[100]+101,(ll)INT_MAX*100000);
  107. memset(walk,1,sizeof(walk));
  108. for(int i=0;i<K;++i){
  109. int xid=getIndex(xaxis,xcnt,cross[i][0]);
  110. int yid=getIndex(yaxis,ycnt,cross[i][1]);
  111. walk[xid][yid]=0;
  112. }
  113. out(solve(getIndex(xaxis,xcnt,xa),getIndex(yaxis,ycnt,ya),
  114. getIndex(xaxis,xcnt,xb),getIndex(yaxis,ycnt,yb)));
  115. puts("");
  116. }
  117. return 0;
  118. }

5、还有那些随手就用的STL算法函数

下面还有一些我平时随手就用的STL算法函数,就都整理在这一节了:

  1. #include <limits.h> //定义了INT_MAX和INT_MIN,int的最大值和最小值
  2. int distance[100005];
  3. //distance[0]到distance[n+1]之间的,全部用INT_MAX/2填充
  4. fill(distance, distance+n+1, INT_MAX/2);

这样,你的初始值想要什么随你高兴!(最短路的时候,初始化每个点的距离,全部填上你需要的INF很有用!)

  1. int a=5,b=6;
  2. printf("%d %d\n",a,b); // 5 6
  3. swap(a,b);
  4. printf("%d %d\n",a,b); // 6 5
  1. int a[3]={1,2,3};
  2. printf("%d %d %d\n",a[0],a[1],a[2]);
  3. reverse(a,a+3);
  4. printf("%d %d %d\n",a[0],a[1],a[2]); // 3 2 1
  1. //需要C++11编译来执行输出语句
  2. //当然你可以自己写这个输出a数组的语句
  3. int a[5]={1,2,3,4,5};
  4. for_each(a,a+5,[](int x){printf("%d ",x);});
  5. puts("");
  6. rotate(a,a+3,a+5);
  7. for_each(a,a+5,[](int x){printf("%d ",x);}); // 4 5 1 2 3
  8. puts("");
  1. int a[10];
  2. for (int i=1; i<10; ++i)a[i]=i;
  3. random_shuffle(a, a+10);
  4. //然后会被打乱,变成随机排列的数组,比如:
  5. //3 4 1 6 0 8 9 2 7 5
  1. #include <iostream> // std::cout
  2. #include <algorithm> // std::next_permutation, std::sort
  3. int main () {
  4. int myints[] = {1,2,3};
  5. std::sort (myints,myints+3);
  6. std::cout << "The 3! possible permutations with 3 elements:\n";
  7. do {
  8. std::cout << myints[0] << ' ' << myints[1] << ' ' << myints[2] << '\n';
  9. } while ( std::next_permutation(myints,myints+3) );
  10. std::cout << "After loop: " << myints[0] << ' ' << myints[1] << ' ' << myints[2] << '\n';
  11. return 0;
  12. }
  13. /*
  14. 输出:
  15. The 3! possible permutations with 3 elements:
  16. 1 2 3
  17. 1 3 2
  18. 2 1 3
  19. 2 3 1
  20. 3 1 2
  21. 3 2 1
  22. After loop: 1 2 3
  23. */
  1. #include <stdio.h>
  2. #include <algorithm>
  3. using namespace std;
  4. typedef long long ll;
  5. int main(){
  6. int a[9]={4,2,5,7,6,9,1,3,8};
  7. nth_element(a,a+2,a+9);
  8. for_each(a,a+9,[](int x){printf("%d ",x);});
  9. puts("");
  10. return 0;
  11. }
  12. /*
  13. 可能的输出:
  14. 1 2 3 6 4 5 9 7 8
  15. */

原理:

这实际上是快速排序中的划分阶段拎出来的一个应用,叫 快速选择算法
一趟的快速排序(一层递归,或者叫,一次划分)中,我们会:
- 选择一个参考值
- 比参考值小的放前面,比参考值大的放后面

那么,在这里,我们随便选个参考值,完成一次划分,确定参考值的位置:
- 恰好是第n,不继续了,直接返回
- 小于预期的n,那么那个待查找的值在后面部分,只对后面部分继续处理
- 大于预期的n,那么那个待查找的值在前面部分,只对前面部分继续处理

所以其期望时间复杂度(一般认为,每次排除一半)为

Bonus 02
如何正确的手写一个rotaterandom_shuffle

不用STL,实现这两个函数的功能,是经典的企业面试题了。这里简单介绍一下这个解法。

rotate
这里只介绍我自己唯一记得住的方法:

字符串旋转一定的位数,等价于:
把串A和串B组成的串AB变成串BA
定义符号:,表示A的反串,比如,那么
先把AB整个反转,得到
——显然只需要再把B的部分和A的部分各自做一次翻转即可。
所以,这种方法是,三次翻转法

有关rotate的更详细的讨论(包括其他的实现方法,以及各种实现方法的实际效率问题),请参见:http://www.cnblogs.com/flyinghearts/archive/2011/05/27/2060265.html




random_shuffle
有一种非常有名的算法:Fisher–Yates shuffle
大致的实现思想:从前到后每一个位置枚举过去,这张牌随机和之后未确定位置的牌(包括这张牌自己)中,随机选择一张,放到这个位置上。
伪代码如下:

  1. -- To shuffle an array a of n elements (indices 0..n-1):
  2. for i from 0 to n2 do
  3. j random integer such that i j < n
  4. exchange a[i] and a[j]

更多有关随机洗牌的讨论,参见:
http://coolshell.cn/articles/8593.html
https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注