前言
本节就来分析一下 rotate
的实现。为什么前言就一句话!
rotate
rotate
的作用为将 [first,middle)
内的元素和 [middle,last)
内的元素互换。middle 所指的元素会成为容器的第个元素。如果有个数字序列 { 1,2,3,4,5,6,7}
,对元素 3 做旋转操作,会形成 {3,4,5,6,7,1,2}
。rotate()
可以交换两个长度不同的区间,如下所示。
做过力扣189. 轮转数组 的人都知道,实现这种效果最简单的方法就是分别在前半段,后半段以及整个区间上各翻转一次,这里的前后段以 middle
分界,这样就可以达到 rotate 的效果,并且代码也最好理解。
当然,STL 中的 rotate 函数仍然不是简单地这样实现的,按照《STL 源码剖析》,rotate
函数的全貌如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 template <class ForwardIterator , class Distance >ForwardIterator rotate_dsipatch (ForwardIterator first, ForwardIterator middle, ForwardIterator last, Distance*, forward_iterator_tag) { for (ForwardIterator i = middle;;) { tinystl::iter_swap (first, i); ++first; ++i; if (first == middle) { if (i == last) return ; else middle = i; } else if (i == last) { i == middle; } } } template <class BidirectionalIterator , class Distance >void rotate_dsipatch (BidirectionalIterator first, BidirectionalIterator middle, BidirectionalIterator last, Distance*, bidirectional_iterator_tag) { tinystl::reverse (first, middle); tinystl::reverse (middle, last); tinystl::reverse (first, last); } template <class EuclideanRingElement >EuclideanRingElement gcd (EuclideanRingElement m, EuclideanRingElement n) { while (n != 0 ) { auto t = m % n; m = n; n = t; } return m; } template <class RandomAccessIterator , class Distance , class T >void rotate_cycle (RandomAccessIterator first, RandomAccessIterator last, RandomAccessIterator initial, Distance shift, T*) { T value = *initial; RandomAccessIterator ptr1 = initial; RandomAccessIterator ptr2 = ptr1 + shift; while (ptr2 != initial) { *ptr1 = *ptr2; ptr1 = ptr2; if (last - ptr2 > shift) ptr2 += shift; else ptr2 = first + (shift - (last - ptr2)); } *ptr1 = value; } template <class RandomAccessIterator , class Distance >void rotate_dispatch (RandomAccessIterator first, RandomAccessIterator middle, RandomAccessIterator last, Distance*, random_access_iterator_tag) { Distance n = gcd (last - first, middle - first); while (n--) { tinystl::rotate_cycle (first, last, first + n, middle - first, value_type (first)); } } template <class ForwardIterator >void rotate (ForwardIterator first, ForwardIterator middle, ForwardIterator last) { if (first == middle || middle == last) return ; tinystl::rotate_dispatch (first, middle, last, distance_type (first), iterator_category (first)); }
总体来说还是根据不同的迭代器类型都实现了各自的版本,其中 BidirectionalIterator
的版本就是反转三次的实现,直接调用 reverse
就行,十分方便。ForwardIterator
版本的实现比较神奇,会辗转移位直到前后区间长度相同,主要适用于与链表类似的结构。RandomAccessIterator
的实现就和 力扣题解 中的环状替换思路相同了,先计算需要循环移位的次数,再逐次调用 rotate_cycle
函数,该函数的作用就是以 initial
为起点,在区间中进行 shift
个单位的移位(实际上是左移)。
就结论而言,STL 中针对每种迭代器实现的 rotate
都是该类型迭代器最高效的实现方式,时间复杂度都是 O ( n ) O(n) O ( n ) 并且不需要使用额外空间,这也体现了 STL 对于性能极致的优化。
性能测试
来做一下测试,看看我们实现的版本与 STL 的差距,这里只测试了 RandomAccessIterator
的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #define TEST_ROTATE(mode, len, count) do { \ srand((int)time(0)); \ clock_t start, end; \ mode::vector<int> v(len); \ mode::iota(v.begin(), v.end(), 1); \ start = clock(); \ for (int i = 0; i < count; ++i) { \ int middle = rand() % len + 1; \ mode::rotate(v.begin(), v.begin() + middle, v.end()); \ } \ end = clock(); \ int n = static_cast<int> (static_cast<double> (end - start) \ / CLOCKS_PER_SEC * 1000); \ std::cout << "Total time: " << n << " ms\n" ; \ } while (0) TEST (rotate_pk) { std::cout << "| std |" << std::endl; TEST_ROTATE (std, 10000 , 10000 ); std::cout << "| tinystl |" << std::endl; TEST_ROTATE (tinystl, 10000 , 10000 ); }
结果十分出人意料,按照上述实现的算法和 STL 中的运行效率差别巨大。慢了整整六倍,令人百思不得其解。博主简单查看了一下 stl_algo.h
,发现 STL 中真实的实现和《STL源码剖析》中所介绍的差别还是挺大的,比如 RandomAccessIterator
其实并没有调用类似于 rotate_cycle()
的函数,甚至并没有调用 gcd()
,而是在函数内部自己实现了类似的功能,同时还使用了在别处定义的大量宏,或许就是这些导致的性能差距吧。
值得一提的是,最初博主并没有按照《STL源码剖析》中的实现来写 rotate
,而是简单地对所有类型迭代器均翻转三次,同样进行测试居然发现这样还稍微快些。
可以参考《编程珠玑》2.3节的内容,翻转三次的方式无论是空间还是时间上都比较优秀,算是既高效又不容易出错的一种实现了。
1 2 3 4 5 6 7 8 9 10 template <class ForwardIterator >ForwardIterator rotate (ForwardIterator first, ForwardIterator middle, ForwardIterator last) { auto new_middle = first; tinystl::advance (new_middle, last - middle); tinystl::reverse (first, last); tinystl::reverse (first, new_middle); tinystl::reverse (new_middle, last); return new_middle; }
仔细想想的话,上述对 vector
的测试最终其实会调用 RandomAccessIterator
版本的 rotate
,翻转三次的做法实际上相当于对整个区间处理了两遍,但理想状态下的 STL 中 RandomAccessIterator
版本的 rotate
做法,只会处理一遍整个区间,因此几乎会快一倍;但实际运行来看并不能达到一倍,这和中间的其他计算有关,如最大公因数的计算等。
总结
这一节分析了 rotate
函数的实现,可以看出 STL 仍然是针对每种迭代器类型都做了优化,使得运行时的效率最佳,但实现的版本与 STL 还是有很大差距,可能与 STL 内部的其他优化有关,留个坑等有空了再填吧咕咕 。