引言

STL中 rotate(first, middle, last) 函数的作用是原地把容器区间 [first, middle)(左半部分) 与 [middle, last) (右半部分)的元素互换。
它的实现充分利用了不同迭代器的特性进行算法优化,从而达到最优的性能。以下是libc++(该库用于clang的C++编译器中)中该函数的实现,为了可读性修改了部分变量名以及删除了类型检查:

代码全文

template <class _ForwardIterator>
_ForwardIteratorrotate_left(_ForwardIterator first, _ForwardIterator last)
{typedef typename iterator_traits<_ForwardIterator>::value_type value_type;value_type tmp = move(*first);_ForwardIterator lm1 = move(next(first), last, first);*lm1 = move(tmp);return lm1;
}template <class _BidirectionalIterator>
_BidirectionalIteratorrotate_right(_BidirectionalIterator first, _BidirectionalIterator last)
{typedef typename iterator_traits<_BidirectionalIterator>::value_type value_type;_BidirectionalIterator lm1 = prev(last);value_type tmp = move(*lm1);_BidirectionalIterator fp1 = move_backward(first, lm1, last);*first = move(tmp);return fp1;
}template <class _ForwardIterator>
_ForwardIteratorrotate_forward(_ForwardIterator first, _ForwardIterator middle, _ForwardIterator last)
{_ForwardIterator i = middle;while (true){swap(*first, *i);++first;if (++i == last)break;if (first == middle)middle = i;}_ForwardIterator r = first;if (first != middle){i = middle;while (true){swap(*first, *i);++first;if (++i == last){if (first == middle)break;i = middle;}else if (first == middle)middle = i;}}return r;
}template<typename _Integral>
_Integralalgo_gcd(_Integral x, _Integral y)
{do{_Integral t = x % y;x = y;y = t;} while (y);return x;
}template<typename _RandomAccessIterator>
_RandomAccessIteratorrotate_gcd(_RandomAccessIterator first, _RandomAccessIterator middle, _RandomAccessIterator last)
{typedef typename iterator_traits<_RandomAccessIterator>::difference_type difference_type;typedef typename iterator_traits<_RandomAccessIterator>::value_type value_type;const difference_type m1 = middle - first;const difference_type m2 = last - middle;if (m1 == m2){swap_ranges(first, middle, middle);return middle;}const difference_type g = algo_gcd(m1, m2);for (_RandomAccessIterator p = first + g; p != first;){value_type t(move(*--p));_RandomAccessIterator p1 = p;_RandomAccessIterator p2 = p1 + m1;do{*p1 = move(*p2);p1 = p2;const difference_type d = last - p2;if (m1 < d)p2 += m1;elsep2 = first + (m1 - d);} while (p2 != p);*p1 = move(t);}return first + m2;
}template <class _ForwardIterator>
_ForwardIterator__rotate(_ForwardIterator first, _ForwardIterator middle, _ForwardIterator last,forward_iterator_tag)
{if (next(first) == middle)return rotate_left(first, last);return rotate_forward(first, middle, last);
}template <class _BidirectionalIterator>
_BidirectionalIterator__rotate(_BidirectionalIterator first, _BidirectionalIterator middle, _BidirectionalIterator last,bidirectional_iterator_tag)
{if (next(first) == middle)return rotate_left(first, last);if (next(middle) == last)return rotate_right(first, last);return rotate_forward(first, middle, last);
}template <class _RandomAccessIterator>
_RandomAccessIterator__rotate(_RandomAccessIterator first, _RandomAccessIterator middle, _RandomAccessIterator last,random_access_iterator_tag)
{if (next(first) == middle)return rotate_left(first, last);if (next(middle) == last)return rotate_right(first, last);return rotate_gcd(first, middle, last);
}template <class _ForwardIterator>
_ForwardIteratorrotate(_ForwardIterator first, _ForwardIterator middle, _ForwardIterator last)
{if (first == middle)return last;if (middle == last)return first;return __rotate(first, middle, last,typename iterator_traits<_ForwardIterator>::iterator_category());
}

代码结构

这个小小的功能代码就有一百五十多行,是不是有种太长不看的想法?没关系,我们先来捋一捋函数的调用关系。

首先,可以看到外部调用函数rotate()时,会根据传入的迭代器类型自动选择相应的函数版本。总共有三种迭代器的重载版本

  • 前向
  • 双向
  • 随机访问

rotate_left()函数作用是把容器所有元素向左(起始端)循环移动一位
rotate_right()则相反,把容器所有元素向右(末端)循环移动一位

如果不考虑对上述两个函数的调用,那么前向迭代版本和双向迭代版本是一样的,最终都是调用rotate_forward()
最关键的不同在于rotate_forward()rotate_gcd()的实现上。也就是专属于随机访问迭代版本的优化。

前向迭代版

先看看rotate_forward()的实现。

template <class _ForwardIterator>
_ForwardIteratorrotate_forward(_ForwardIterator first, _ForwardIterator middle, _ForwardIterator last)
{_ForwardIterator i = middle;while (true){swap(*first, *i);++first;if (++i == last)break;if (first == middle)middle = i;}_ForwardIterator r = first;if (first != middle){i = middle;while (true){swap(*first, *i);++first;if (++i == last){if (first == middle)break;i = middle;}else if (first == middle)middle = i;}}return r;
}

观察代码,发现代码分成两部分,而且两部分非常相似。第一部分代码的目的在于获取返回值,如果不需要返回值,完全可以写成下面这样子的:

template <class _ForwardIterator>
voidrotate_forward(_ForwardIterator first, _ForwardIterator middle, _ForwardIterator last)
{_ForwardIterator i = middle;while (true){swap(*first, *i);++first;if (++i == last){if (first == middle)break;i = middle;}else if (first == middle)middle = i;}
}

下面直接用图来说明吧。
假设容器内元素排布分为ABC三段。
总共分两种情况讨论:

  1. 图中middle指向B段第一个元素,A与B段等长,目标是把A段挪到最后,形成BCA的结构。这种情况下first首先到达middle,这时原来处于B段的元素已经被移动到正确的位置上,然后调整middle的位置到i的位置,然后继续对AC段进行rotate操作。

  2. middle指向C段第一个元素,A与C段等长,目标是把C段挪到最前面,形成CAB的结构。这种情况下i首先到达末尾,C段元素已经就位,调整i的位置到middle,继续对BA段进行操作。

很明显,这个算法虽然时间复杂度是线性的,但是有部分元素需要多次移动才能达到最终位置上。

随机访问版

接下来看看针对随机访问迭代器的优化版本

template<typename _RandomAccessIterator>
_RandomAccessIteratorrotate_gcd(_RandomAccessIterator first, _RandomAccessIterator middle, _RandomAccessIterator last)
{typedef typename iterator_traits<_RandomAccessIterator>::difference_type difference_type;typedef typename iterator_traits<_RandomAccessIterator>::value_type value_type;const difference_type m1 = middle - first;const difference_type m2 = last - middle;if (m1 == m2){swap_ranges(first, middle, middle);return middle;}const difference_type g = algo_gcd(m1, m2);for (_RandomAccessIterator p = first + g; p != first;){value_type t(move(*--p));_RandomAccessIterator p1 = p;_RandomAccessIterator p2 = p1 + m1;do{*p1 = move(*p2);p1 = p2;const difference_type d = last - p2;if (m1 < d)p2 += m1;elsep2 = first + (m1 - d);} while (p2 != p);*p1 = move(t);}return first + m2;
}

这个算法的理解就需要花点心思了。还是从示例讲起吧。下面是一个长度为10的数组a,假设我们对它调用rotate(a, a + 4, a + 10),把前四个和后六个元素调换位置。

也就是从这样:

位置 0 1 2 3 4 5 6 7 8 9
元素值 1 2 3 4 5 6 7 8 9 10

表1

变成这样:

位置 0 1 2 3 4 5 6 7 8 9
元素值 5 6 7 8 9 10 1 2 3 4

表2

首先middle将数组分隔成两段,左段长度为4,右段长度为6,那么他们的最大公约数为g=2,那么我们要进行两次循环移位才做。
从位置p=g-1=1开始,每隔4个元素往前移4位。
设临时变量t,然后以t<-1<-5<-9<-3<-7<-t的赋值顺序循环移位。然后变成这样:

位置 0 1 2 3 4 5 6 7 8 9
元素值 1 6 3 8 5 10 7 2 9 4

表3

完成一次循环移位后,继续p=p-1=0,以下面的赋值顺序进行循环移位:
t<-0<-4<-8<-2<-6<-t
完成这次循环移位后,满足p==0,算法结束,得到表2的最终结果。

可以发现,每个元素都只被移动了一次,O(n)中的常数应该更小,那么理论上来说它应该比rotate_forward()更快。

测试

测试环境:
i5-3210M+8G RAM
Windows 10 64bit
Visual Studio 2015 with Update3
Build target: Release x64

测试代码如下:

auto get_duration(const system_clock::time_point &start, const system_clock::time_point &fin)
{auto duration = duration_cast<microseconds>(fin - start);return double(duration.count()) * microseconds::period::num / microseconds::period::den;
}int main()
{auto n = 10;auto data_len = 20000000;auto middle = 19;auto avg_a = 0.0, avg_b = 0.0;vector<int> data_a(data_len);vector<int> data_b(data_len);iota(begin(data_a), end(data_a), 0);iota(begin(data_b), end(data_b), 0);while (--n){auto start = system_clock::now();rotate_gcd(begin(data_b), begin(data_b) + middle, end(data_b));auto finish = system_clock::now();avg_a += get_duration(start, finish);cout << duration << " ";start = system_clock::now();rotate_forward(begin(data_a), begin(data_a) + middle, end(data_a));finish = system_clock::now();avg_b += get_duration(start, finish);cout << duration << "\n";}avg_a /= 10;avg_b /= 10;cout << avg_a << " " << avg_b << " " << avg_a / avg_b << "\n";return 0;
}

得到测试数据如下:
middle=19

次数 rotate_gcd()(单位:秒) rotate_forward()(单位:秒)
第1次 0.193052 0.034112
第2次 0.171875 0.0369
第3次 0.20115 0.039593
第4次 0.202003 0.043451
第5次 0.189696 0.030061
第6次 0.185225 0.028943
第7次 0.192203 0.025791
第8次 0.155145 0.021634
第9次 0.159483 0.022387
第10次 0.160719 0.022697
平均 0.181055 0.0305569

表4

rotate_gcd()耗时居然是rotate_forward()的6倍!
看上去更复杂巧妙的算法居然比简单的算法还慢。

进一步对耗时数据进行采样(数据规模从两千万缩小到两百万),可以得到下图(纵坐标是耗时,横坐标是middle的值,蓝线代表rotate_gcd(),绿线代表rotate_forward()):

可以看到rotate_gcd()耗时是rotate_forward()数倍以上,且性能曲线不平稳,实在跟它复杂的原理不匹配。而这个算法在MSVC内建的STL代码中已经移除掉了,大概就是因为这个原因吧。

总结

为什么会导致这样的结果呢?CPU缓存是很大的影响因素。

rotate_forward()里,可以看到它对元素的访问总是从位置1到位置2再到位置3这样依次访问,CPU可以提前将后面的元素一并加载到缓存中了,免去频繁读取内存的延迟。

rotate_gcd()对缓存就没那么友好了,它总是每隔一段距离再读取下一个元素,这种做法容易造成cache miss,必须从内存中读取元素,拖慢速度。

因此,在设计算法的时候,不仅仅要考虑算法自身的速度,也要考虑平台相关的优化,比如尽量做到缓存友好,必要时可以使用SIMD等。

参考文献

http://apprize.info/programming/mathematics/11.html

STL实现细节之rotate()相关推荐

  1. STL 之reverse,reverse_copy,rotate,rotate_copy

    作用:倒置或调换区间元素的位置 声明: #include <algorithm> template <class biDirectionalItr> void reverse( ...

  2. STL源码分析-rotate

    http://note.youdao.com/noteshare?id=4ba8ff81aa96373ba11f1b82597ec73a 转载于:https://www.cnblogs.com/tai ...

  3. 三十分钟掌握STL(Using STL)

    STL概述 STL的一个重要特点是数据结构和算法的分离.尽管这是个简单的概念,但这种分离确实使得STL变得非常通用.例如,由于STL的sort()函数是完全通用的,你可以用它来操作几乎任何数据集合,包 ...

  4. 短时间让大家对C++ STL有所学习

    STL概述 STL的一个重要特点是数据结构和算法的分离.尽管这是个简单的概念,但这种分离确实使得STL变得非常通用.例如,由于STL的sort()函数是完全通用的,你可以用它来操作几乎任何数据集合,包 ...

  5. 切换效果:coverflow 封面轮播图

    文章目录 前言 一.最终效果 二.swiper库的引入 三.代码复现 1. html主体 2. css样式 3. js脚本 总结 前言 coverflow封面轮播图主要常用在一些音乐播放网站,或者视频 ...

  6. c++stl和std_std :: rotate()函数以及C ++ STL中的示例

    c++stl和std C ++ STL std :: rotate()函数 (C++ STL std::rotate() function) rotate() function is a librar ...

  7. c++ string类_C++|细说STL string类概貌及底层细节

    C语言中的字符串称为C风格字符串,是一个以'0'结尾的字符数组,string.h库只提供了有限.不甚安全的字符串操作函数.char str[]只能定义编译期确定大小的字符串,而保存在堆内存的动态字符数 ...

  8. STL的rotate函数分析

    STL的rotate其实就可以看做是一个循环移位的函数. 首先对于循环移位操作有以下的几种思路: 1)最简单的想法就是每次移动一位进行循环遍历整个容器,算法复杂度为O(n*m). 2)运用分组交换(尽 ...

  9. C++ STL 逆转旋转 reverse reverse_copy rotate

    #include <iostream> #include <algorithm> #include <vector> #include <iterator&g ...

最新文章

  1. 6-5-树的双亲表示法-树和二叉树-第6章-《数据结构》课本源码-严蔚敏吴伟民版...
  2. 区块链将带来怎样的应用?
  3. hadoop 全分布式部署
  4. Java 类中可以覆盖静态方法吗?
  5. 联想创投子公司国民认证,助力农行全面升级FIDO移动生物识别安全认证
  6. js的栈堆与浅拷贝、深拷贝的理解
  7. 宝塔 windows 2012 配置 ftp 允许 fileZilla 连接
  8. redis-cli 常用命令
  9. 数据采集标注、模型开发、部署落地,百度大脑全栈 AI 能力详解
  10. java异常处理思考题_java异常处理试题及答案
  11. 最近围绕生鲜社区团购的一些事
  12. [转载] LeetCode题解(面试16.22):兰顿蚂蚁(Python)
  13. LAMP组合之服务分离部署
  14. 【Java TCP/IP Socket】UDP Socket(含代码)
  15. c语言 程序段 数据段,C程序段(代码段、数据段、BSS段以及堆栈)的详解
  16. java中怎么读取txt文件_Java读取TXT文件
  17. JAVA使用Gson解析json数据,实例
  18. echart饼状图上显示百分比
  19. Android Studio sdk emulator directory is missing
  20. 谁能和乔布斯比勤奋?乔布斯的睡眠时间

热门文章

  1. 正经人谁写博客啊!下贱!
  2. SQL2005 还原备份数据
  3. 数学建模更新12(非线性规划)
  4. 【WLAN】【软件】NXP芯片方案用户态和内核态通讯方式小结
  5. “搜狗拼音输入法”软件评价
  6. js中怎么添加图片以及在图片上添加链接
  7. 中国5G无人仓现场运行披露
  8. BZOJ P2819 Nim
  9. matlab虚拟现实工具箱,Matlab虚拟现实工具箱简单使用
  10. 谷歌浏览器驱动下载地址