《编程珠玑》笔记:数组循环左移

问题描述:数组元素循环左移,将包含 num_elem 个元素的一维数组 arr[num_elem] 循环左移 rot_dist 位。能否仅使用数十个额外字节的存储空间,在正比于num_elem的时间内完成数组的旋转?

一:Bentley‘s Juggling Alogrithm

移动变量 arr[0] 到临时变量 tmp,移动 arr[rot_dist] 到 arr[0],arr[2rot_dist] 到 arr[rot_dist],依此类推,直到返回到取 arr[0] 中的元素,此时改为从 tmp 取值,程序结束。

这个方法需要保证:1. 能够遍历所有的数组元素;2.  arr[0] (即 tmp 的值)在最后一步赋给某个合适的数组元素。

当 num_elem 和 rot_dist 互素的时候上述条件自然满足,否则不满足。从代数的观点来看,当 num_elem 和 rot_dist 互素的时候上述遍历规则将 0, ... , num_elem 个元素进行了轮换,而当 num_elem 和 rot_dist 不互素时(记最大公约数为 common_divisor),上述遍历规则将构成 common_divisor 个不相交的轮换,对 num_elem/common_divisor 个元素进行轮换。

 1 unsigned gcd(unsigned m, unsigned n)
 2 {
 3     unsigned remainder;
 4     while(n > 0) {
 5         remainder = m % n;
 6         m = n;
 7         n = remainder;
 8     }
 9
10     return m;
11 }
12
13 // left rotate @arr containing @num_elem elements by @rot_dist positions
14 // Bentley‘s Juggling Algorithm from Programming Pearls
15 template<typename _Type>
16 void array_left_rotation_juggling(_Type *arr, int num_elem, int rot_dist)
17 {
18     if(rot_dist == 0 || rot_dist == num_elem)
19         return;
20
21     _Type tmp;
22     int i, j, k;
23
24     int common_divisor = gcd(num_elem, rot_dist);
25     for(i = 0; i < common_divisor; ++i) {
26         tmp = arr[i];
27         j = i, k = (j + rot_dist) % num_elem;
28         while(k != i) {
29             arr[j] = arr[k];
30             j = k;
31             k = (k + rot_dist) % num_elem;
32         }
33         arr[j] = tmp;
34     }
35 }

二:Gries and Mills Block Swapping

将包含 num_elem 个元素的数组循环左移 rot_dist 位,等价于交换两个数组块 arr[0, rot_dist-1] 和 arr[rot_dist, num_elem-1] 的位置(即 Block Swapping问题),用 X,Y 来表示这两个数组块。

  1. 当 X 和 Y 的长度相等时,直接交换两个数组块 XY -> YX 即可。
  2. 当 X 包含的元素个数较多时,将 X 分拆为两部分 X1 和 X2,其中 X1 的长度等于 Y 的长度,交换 X1 和 Y:X1X2Y -> YX2X1,此时 Y 位于循环左移之后(块交换之后)所应处于的位置。
  3. 当 Y 包含的元素个数较多时,将 Y 分拆为两部分 Y1 和 Y2,其中 Y2 的长度等于 X 的长度,交换 X 和 Y2: XY1Y2 -> Y2Y1X,此时 X 位于循环左移之后(块交换之后)所应处于的位置(后面成为最终位置)。

在第 2 种情况(第 3 种情况)操作完成之后,问题已经被约减,继续交换对X2X1 (第3种情况下是 Y1Y2),即可解决问题。这就是递归的解决思路了。这是《编程珠玑》上对这个方法的简单描述。看到这里就一头雾水,查阅《The Science of Programming》的18.1节还有这里算是搞明白了,在此整理下。

先看一个简单示例,用上面的思路将包含 7 个元素的数组循环左移 2 位,数组元素值依次为 0, 1, 2, 3, 4, 5, 6,图片来自这里

图中红色的表示较短的块。观察执行过程可以发现这样几个点:

  1. 将数组分成左右两块之后,短块与长块的一个子块进行交换之后,这个短块中的元素便位于最终位置,后续操作不再修改该部分。
  2. 与这个短块进行交换的子块位于长块(执行一次交换之后,长块相应缩短)中远离短块的一端。
  3. 当两个块长度相等,交换这两个等长的块后程序执行完毕。

首先需要能够交换两个等长的数组块的功能,函数实现如下。

1 // swap two blocks of equal length
2 // there must be no overlap between two blocks
3 template<typename _Type>
4 void swap_equal_blocks(_Type *arr, int beg1, int beg2, int num)
5 {
6     while(num-- > 0)
7         std::swap(arr[beg1++], arr[beg2++]);
8 }

因为需要考虑两个块的长度,分别记左右两个块的长度位 i 和 j,即左侧有 i 个元素仍待处理,右侧有 j 个元素待处理,i + j 表示此时还没有位于最终位置的元素个数。下图画出了初始状态以及 i > j 和 i < j 两种情况下的执行一次交换之后的状态。图中用 r 指代 rot_dist,用 n 指代 num_elem,灰色部分表示已经位于最终位置。

根据上图可以归纳出如下几个关系:

  1. i > j 时,交换的两部分是从下标 r-i 和下标 r 开始的 j 个元素。
  2. i < j 时,交换的两部分时从下标 r-i 和下标 r+j-i 开始的 i 个元素。
  3. arr[0 : r-i-1] 以及 arr[r+j : n-1] 已经位于最终位置。
  4. 左侧待处理的 i 个元素总是 arr[r-i : r-1],右侧待处理的 j 个元素总是 arr[r : r+j-1]。

据此,则有如下实现。

 1 template<typename _Type>
 2 void array_left_rotation_blockswapping(_Type *arr, int num_elem, int rot_dist)
 3 {
 4     if(rot_dist == 0 || rot_dist == num_elem)
 5         return;
 6
 7     int i = rot_dist, j = num_elem - rot_dist;
 8     while(i != j) { // could be dead loop when rot_dist equals to 0 or num_elem
 9         // Invariant:
10         // arr[0 : rot_dist-i-1] is in final position
11         // arr[rot_dist-i : rot_dist-1] is the left part, length i
12         // arr[rot_dist : rot_dist+j-1] is the right part, length j
13         // arr[rot_dist+j : num_elem-1] is in final position
14         if(i > j) {
15             swap_equal_blocks(arr, rot_dist-i, rot_dist, j);
16             i -= j;
17         }
18         else {
19             swap_equal_blocks(arr, rot_dist-i, rot_dist+j-i, i);
20             j -= i;
21         }
22     }
23     swap_equal_blocks(arr, rot_dist-i, rot_dist, i);
24 }

三:Reversal Algorithm

这个方法的最好说明就是 Doug Mcllroy 用双手给出的示例了,见下图。图中将数组循环左移了 5 位。

对应到这里的表示就是用三次反转实现循环左移。实现代码如下:

// reverse the elements @arr[@low : @high]
void reverse(char *arr, int low, int high)
{
    while(low < high)
        std::swap(arr[low++], arr[high--]);
}

template<typename _Type>
void array_left_rotation_reversal(_Type *arr, int num_elem, int rot_dist)
{
    if(rot_dist == 0 || rot_dist == num_elem)
        return;

    reverse(arr, 0, rot_dist-1);
    reverse(arr, rot_dist, num_elem-1);
    reverse(arr, 0, num_elem-1);
}

关于性能:这三种方法的时间复杂度均为 O(n)。在这里,Victor J. Duvanenko 用Intel C++ Composer XE 2011在 Intel i7 860 (2.8GHz, with TurboBoost up to 3.46GHz) 对三种算法进行的实际的性能测试,结果显示 Gries-Mills 算法运行时间最短,Reversal 算法居第二位,但与 Gries-Mills 算法在运行时间上相差很少。但是 Reversal 有一个优势是在多次的测试种,Reversal 算法的运行时间非常稳定(即多次所测时间的标准差很小),而 Juggling 算法在运行时间和性能稳定性方面均教差。Duvanenko 具体分析了导致这一结果的原因:Gries-Mills 算法和 Reversal 算法的良好表现是由于它们的缓存友好的内存读取模式,而 Juggling 算法的内存读取模式不是缓存友好的("The Gries-Mills and Reversal algorithms preformed well due to their cache-friendly memeory access patterns. The Juggling algorithm preformed the fewest memory accesses, but came in 5x slower dut to its cache-unfriendly memory access pattern.)。总之,Juggling 算法较其他两个算法的执行效率很差,而 Reversal 算法和 Gries-Mills 算法相比,具有基本相同的运行时间,但是 Reversal 算法运行效率更为稳定,算法原理更容易理解,代码实现也更为简洁。

时间: 2024-10-10 08:46:29

《编程珠玑》笔记:数组循环左移的相关文章

从数组循环左移问题中浅谈考研算法设计的规范代码

问题:设将n(n>1)个整数存放到一维数组R中.设计一个算法,将R中的序列循环左移p(0<p<n)个位置,即将R中的数据由{X0,X1,...,Xn-1}变换为{Xp,Xp+1,...,Xn-1,X0,X1,...,Xp-1}.要求:写出本题的算法描述. 分析: 本题不难,要实现R中序列循环左移p个位置,只需先将R中前p个元素逆置,再将剩下的元素逆置,最后整体逆置操作即可.本题算法描述如下: 1 #include <iostream> 2 using namespace st

数组循环左移p位

逆置前p个元素,再逆置剩下的元素,再逆置全部 测试数组:1 2 3 4 5 6 如:循环左移3位 3 2 1 6 5 4 4 5 6 1 2 3 法2:从0开始,下标-p,如果小于0,再加数组长度n,将0的值放入该下标 测试数组:1 2 3 4 5 如:循环左移3位 下标0:0-3+6=3 原文地址:https://www.cnblogs.com/dubuyunjie/p/12184364.html

&lt;编程珠玑&gt;笔记(三) 四条原则

第三章作者重在阐述一种编程观念, 即 “data does indeed strcture programs” 这一章貌似没什么干货,只好把作者的几个例子贴出来,反复看看了. 1  A survey program   Total US Citi Perm Visa Temp Visa Male Female African American 1289 1239 17 2 684 593 Mexican American 675 577 80 11 448 219 Native American

shell脚本编程学习笔记-while循环

1.当型循环和直到型循环 While使用的不多,一般守护进程程序或始终循环执行会用,其他循环运算都用for代替. 1.1 当型和直到型循环语法 (1)while条件语句 语法: While 条件 do 指令-. done 手机充值:发短信扣费,充值100,每次扣1角5,当费用低于1角5分就不能发了. (2)until条件语句 语法: until 条件 do 指令- Done 提示:只循环一次,应用场景不多,了解就好. 1.2 当型和直到型循环基本范例 休息命令:sleep休息1秒,usleep1

&lt;编程珠玑&gt;笔记(二) 程序验证

在芯片设计(IC)领域有专门的职位叫做芯片验证工程师,其中的一种方法叫形式验证(Formal Verification),具体包括等价性检查,模型检查和定理证明. 本章所讲的程序验证方法(不要与软件测试混为一谈),与芯片行业的形式验证法非常相似.参考芯片行业,随着分工的细化,软件也会有专门的验证工程师. 1  Binary search determine whether the sorted array x[0..n-1] contains the target element t mustb

&lt;编程珠玑&gt;笔记(二) 三个算法

在第二章里,作者提出了三个问题,然后慢慢引出对应的算法实现. 1  Binary search 二分查找 Given a sequential file that contain at most 4x109 32-bit integers in random order, find a 32-bit integer that is not in the file. How would you solve this problem with ample main memory? How would

《编程珠玑》---笔记。浏览此文,一窥此书。

第一章: 磁盘排序:对于一个提出的问题,不要未经思考就直接给出答案.要先深入研究问题,搞清楚这个问题的特点,根据这个特点,可能有更好的解决方案. 比如:文中:最初的需求只是"我如何对磁盘文件排序". 我们首先想到了经典的归并排序. 但,进一步了解到排序的内容是10000000个记录,每条记录都是一个7位整数,且只有1M可用的内存.每条记录不相同. [位示图法,详见我的关于排序的博文] 第二章: 三个问题: 1.给定一个包含32位整数的顺序文件,它至多只能包含40亿个这样的整数,并且整数

读书笔记第一周《编程珠玑》

--<编程珠玑>读后感 因为时间原因,现在只读了书的前四章以及关于代码优化的第九章,虽然感觉因为语言文化的差异或者翻译的问题,行文感觉十分别扭,但是仍是收益良多. 开篇提出的问题的解决方式令人印象深刻,巧妙地利用位图的方式解决了一个电话号码的排序问题,并且大大降低了时间与空间复杂度.其实引人深思的不仅是这样一种算法,而是文中告诫大家,我们不应当看到问题的第一时间就在脑中搜索我们学习的相关知识,这样很容易搜寻到不是最适合此问题的解决办法.作者在获取更多关于问题的信息后,得到了这个问题数据独有的特

数组的循环左移

描述 设将n(n>1)个整数存放到一维数组R中.试设计一个在时间和空间两方面都尽可能高效的算法,将R中保存的序列循环左移p(0<p<n)个位置,即将R中的数据由(x0, x1-, xn-1)变换为(xp,xp+1,-,xn-1,x0,x1,-,xp-1). 输入 多组数据,每组数据有三行.第一行为一个整数n,代表数组R中有n个元素.第二行为数组R中的n个元素(元素之间用空格分隔).第三行为一个整数p,代表将R中的序列循环左移p个位置.当n等于0时,输入结束. 输出 每组数据输出一行,为移