数组左旋转k位 —— C++标准算法库中最悲剧的函数:rotate
2011-07-04 17:58
274 查看
要将一个数组的所有元素向左旋转k位,通常有三种算法:
算法1(分组交换):
若a长度大于b,将ab分成a0a1b,交换a0和b,得ba1a0,只需再交换a1 和a0。
若a长度小于b,将ab分成ab0b1,交换a和b0,得b0ab1,只需再交换a 和b1。
不断将数组划分和交换,直到不能再划分为止。分组过程与求最大公约数很相似。
读写内存各 n到2*n次
算法2 (三次反转)
利用ba=(br)r(ar)r=(arbr)r,先分别反转a、b,最后再对所有元素进行一次反转。
读写内存各约2*n次
算法3 (使用循环链)
假设 n、k的最大公约数为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函数),重新测试下,可得到下面结果:
(使用GCC的,请用版本号低于4.5的进行测试)
从结果可以看出,效率是:算法1 > 算法2 >>> 算法3。
从理论上讲,算法3只要读写内存各n次,应该是效率最高的算法。这在每次内存读写的开销相差不大时成立。但实际上,由于硬件限制,CPU对内存的访问采用分级缓存机制:一级缓存容量很小但访问速度最快,存放程序的指令和最常用的数据,而二、三级缓存容量较大但访问速度要慢很多。CPU是无法绕过缓存直接访问内存数据(某些特殊指令可以不用一二三级缓存,但它也要用到其它专用缓存),对不在缓存中的数据,必须先载入到缓存中,这个操作是相当昂贵的。对大数组来说,不可能将所有数据都存放在缓存中,而对内存的不连续访问,CPU对内存定位的开销(各级缓存间数据的调整,反复移入或移出数据到缓存)是巨大的,这就造成了算法3的性能在该情况下非常差。测试发现,k = 3时,该算法的效率就已经相当差了。对小数组,尽管该算法读写次数少,但由于各种算法所用时间都很小,这种优势很难体现出来。可以说,算法3在数学上是非常优美的,但是在实际应用中,是一种相当差的算法。
对算法的选择,不应该忽视内存因素。在对随机访问迭代器版本的roate实现上犯这个错误的,可不仅仅是VC,还有著名的STL Port、GCC(GCC从4.5开始libstdc++改用算法1,并做了些优化),以及新兴的libc++。(其它的编译器/库没用过,也就没有测试。)
另外,测试时发现VC 2010的一个bug:前向迭代器的实现版本,当k = 0时,程序直接挂了
算法1(分组交换):
若a长度大于b,将ab分成a0a1b,交换a0和b,得ba1a0,只需再交换a1 和a0。
若a长度小于b,将ab分成ab0b1,交换a和b0,得b0ab1,只需再交换a 和b1。
不断将数组划分和交换,直到不能再划分为止。分组过程与求最大公约数很相似。
读写内存各 n到2*n次
算法2 (三次反转)
利用ba=(br)r(ar)r=(arbr)r,先分别反转a、b,最后再对所有元素进行一次反转。
读写内存各约2*n次
算法3 (使用循环链)
假设 n、k的最大公约数为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 Port、GCC(GCC从4.5开始libstdc++改用算法1,并做了些优化),以及新兴的libc++。(其它的编译器/库没用过,也就没有测试。)
另外,测试时发现VC 2010的一个bug:前向迭代器的实现版本,当k = 0时,程序直接挂了
相关文章推荐
- 数组左旋转k位 —— C++标准算法库中最悲剧的函数:rotate
- 数组左旋转k位 —— C++标准算法库中最悲剧的函数:rotate
- 【LeetCode-面试算法经典-Java实现】【189-Rotate Array(旋转数组)】
- [C++]Rotate Array 旋转数组
- 简化以下程序,将函数对象 divide_by 转换为一个函数,并将 for 循环替换为用一个标准的 C++ 算法来输出数据
- C++ 算法 查找旋转数组中的最小值 允许重复元素
- 算法学习记录七(C++)--->二分法找有序旋转数组最小值
- 数组左旋转k位 std::rotate() POJ 1978
- sort函数的用法(C++排序库函数的调用) 对数组进行排序,在c++中有库函数帮我们实现,这们就不需要我们自己来编程进行排序了。 (一)为什么要用c++标准库里的排序函数 Sort()函数是c+
- 巧用标准c++中的算法函数,对数组进行操作
- C++ 数据结构之kmp算法中的求Next()函数的算法
- C++学习笔记1(结构体,命名空间,标准输入输出,引用,函数,构造函数)
- 在C/C++中如何使函数返回数组
- 标准c++中string类函数介绍
- C++ 字符数组函数与string函数
- c++中传递数组给函数时,为什么不能在子函数中求数组长度
- c++中如何给函数传递数组参数
- 编写一个函数,从标准输入读取一列整数,把这些值存储于一个动态分配的数组中并返回这个数组。函数通过观察EOF判断输入列表是否结束。数组的第一个数是数组包含的值的个数,他的后面就是这些整数值。
- C/C++——指向函数的指针和指向函数的指针的数组
- 阿里巴巴面试算法题:有一个函数int getNum(),每运行一次可以从一个数组V[N]里面取出一个数,N未知,当数取完的时候,函数返回NULL。现在要求写一个函数int get(),这个函数运行一次可以从V[N]里随机取出一个数,而这个数必须是符合1/N