数组左旋转k位 —— C++标准算法库中最悲剧的函数:rotate

 

要将一个数组的所有元素向左旋转k位,通常有三种算法:

 

算法1分组交换):

a长度大于b,将ab分成a0a1b,交换a0b,得ba1a0,只需再交换a1 a0

a长度小于b,将ab分成ab0b1,交换ab0,得b0ab1,只需再交换a b1

不断将数组划分和交换,直到不能再划分为止。分组过程与求最大公约数很相似。

 读写内存各 n2*n

 

算法2 (三次反转)

利用ba=(br)r(ar)r=(arbr)r,先分别反转ab,最后再对所有元素进行一次反转。

读写内存各约2*n

 

算法3 (使用循环链)

假设 nk的最大公约数为M,则所有序号为 (i + j*k) % n (0<= i < M, 0 <= j < n/M)的元素,构成M个循环链(i值相同的在同一个循环链上), 每个循环链上的元素移动到前一个元素的元素,就可以交换到最终结果上的位置,因而总共只要读写内存各n次。(比如: 1 2 3 4 5 6,左移2位, 1 3 5 2 4 6分别构成两个循环链。)

 

事实上C++标准算法库提供了现成的函数:rotate函数。按理说,几种算法都比较简单,编译器的库函数又是经过时间检验的,效率即使比手写的差,也不会差太多。但如果对rotate函数进行测试的话,可能会发现标准库的版本慢得可不是一点点。

VC 2010,运行后面的测试程序,自定义函数(采用算法2)要用99ms,而std::rotate却要1656ms。是库的实现者不懂得用这个简单的算法吗?检查下库的源代码,就会发现:标准算法库中,对C++的三种迭代器(前向迭代器、双向迭代器,随机访问迭代器),分别采用了上面三种算法。直接调用其内部的实现(std::_Rotat函数),重新测试下,可得到下面结果:

 

迭代器

前向(算法1

双向(算法2

随机访问(算法3

时间(ms

46

99

1651

                                                               (使用GCC的,请用版本号低于4.5的进行测试)

 

    从结果可以看出,效率是:算法1 > 算法2 >>> 算法3 

 

从理论上讲,算法3只要读写内存各n次,应该是效率最高的算法。这在每次内存读写的开销相差不大时成立。但实际上,由于硬件限制,CPU对内存的访问采用分级缓存机制:一级缓存容量很小但访问速度最快,存放程序的指令和最常用的数据,而二、三级缓存容量较大但访问速度要慢很多。CPU是无法绕过缓存直接访问内存数据(某些特殊指令可以不用一二三级缓存,但它也要用到其它专用缓存),对不在缓存中的数据,必须先载入到缓存中,这个操作是相当昂贵的。对大数组来说,不可能将所有数据都存放在缓存中,而对内存的不连续访问,CPU对内存定位的开销(各级缓存间数据的调整,反复移入或移出数据到缓存)是巨大的,这就造成了算法3的性能在该情况下非常差。测试发现,k = 3时,该算法的效率就已经相当差了。对小数组,尽管该算法读写次数少,但由于各种算法所用时间都很小,这种优势很难体现出来。可以说,算法3在数学上是非常优美的,但是在实际应用中,是一种相当差的算法。

对算法的选择,不应该忽视内存因素。在对随机访问迭代器版本的roate实现上犯这个错误的,可不仅仅是VC,还有著名的STL PortGCCGCC4.5开始libstdc++改用算法1,并做了些优化),以及新兴的libc++。(其它的编译器/库没用过,也就没有测试。)

 

另外,测试时发现VC 2010的一个bug:前向迭代器的实现版本,当k = 0时,程序直接挂了。

 

 

测试代码:

 

rotate
 1 // www.cnblogs.com/flyinghearts
 2 
 3 #include <vector>
 4 #include <algorithm>
 5 #include <iterator>
 6 #include <ctime>
 7 
 8 #if __GNUC__
 9   #define ROTATE std::__rotate
10 #elif _MSC_VER
11   #define ROTATE std::_Rotate
12 #else
13   #error "You should use GCC or VC"
14 #endif
15 
16 
17 
18 template<unsigned Count, bool Show, typename T, typename Iterator_tag>
19 void test(T beg, T mid, T end, const Iterator_tag& iterator_tag, const char *str = "")
20 {
21   unsigned sum = 0;
22   for (unsigned i = 0; i != Count; ++i) {
23     unsigned ta = clock();
24     ROTATE(beg, mid, end, iterator_tag);
25     ta = clock() - ta;
26     sum += ta;
27     if (Show) printf("%s  %u ms\n", str, ta);
28   }
29   if (Show) printf("aveg: %u ms\n\n", sum / Count);
30   else printf(" %s  total: %u ms\n", str, sum);
31 }
32 
33 
34 template<unsigned Count, bool is_std, typename T>
35 void test2(T beg, T mid, T end,const char *str = "")
36 {
37   unsigned sum = 0;
38   for (unsigned i = 0; i != Count; ++i) {
39     unsigned ta = clock();
40     if (is_std) std::rotate(beg, mid, end);
41     else {
42       std::reverse(beg, mid);
43       std::reverse(mid, end);
44       std::reverse(beg, end);
45     }
46     ta = clock() - ta;
47     sum += ta;
48     printf("%s  %u ms\n", str, ta);
49   }
50   printf("aveg: %u ms\n\n", sum / Count);
51 }
52 
53 template<unsigned Count, bool Show, typename T>
54 inline void test3(T beg, T mid, T end)
55 {
56   test<Count, Show>(beg, mid, end, std::forward_iterator_tag(), "forward");
57   test<Count, Show>(beg, mid, end, std::bidirectional_iterator_tag(), "bidirectional");
58   test<Count, Show>(beg, mid, end, std::random_access_iterator_tag(), "random");
59 }
60 
61 int main()
62 {
63   const int N = 1e7;
64   const int M = 1024;
65   //const int M = 777;
66   std::vector<int> vec(N);
67   std::vector<int>::iterator beg(vec.begin()), mid(beg + M), end(vec.end());
68 
69   printf("------\n");
70   test2<3,false>(beg, mid, end, " 3_reverse");
71   test2<3true>(beg, mid, end, " std::rotate");
72   
73   test3<3true>(beg, mid, end);
74   
75   for (int i = 1; i < 5++i) {
76     printf ("\n%d\n", i);
77     test3<3false>(beg, beg + i, end);
78   }
79   
80 }

 

 

posted @ 2011-05-27 21:00  flyinghearts  阅读(6379)  评论(5编辑  收藏  举报