问题
设计一个算法,把一个含有N 个元素的数组循环右移K 位,要求时间复杂度为O(N)。
解法一
看到这个题目,第一个想法是:如果程序中真遇到这个需求,以其去移动具体元素,不如改变数组访问方式,这样能实现O(1)的复杂度。
设计一个类CircleArray<T>,其中:
List<T> items; //用来保存给定的数组;
int benchmark = 0; //访问基准
public T this[int i] //然后构造索引器,改变数组访问方式
{
get
{//省略边界检查
var j = (benchmark + i) % Count;
return items[j];
}
set
{//省略边界检查
var j = (benchmark + i) % Count;
items[j] = value;
}
}
public void LeftShift(int k) //循环左移
{
if (Count <= 0) return;
benchmark = (benchmark + k) % Count;
}
public void RightShift(int k) //循环右移
{
if (Count <= 0) return;
k = k % Count;
benchmark -= k;
if (benchmark < 0) benchmark += Count;
}
解法二
那么有读者可能会问,对于“非移动不可”的情况呢?《编程之美》中给出的解法可谓相当简练:
void RightShift(int* arr, int N, int k)
{
k %= N;
Reverse(arr, 0, N - k - 1);
Reverse(arr, N - k, N - 1);
Reverse(arr, 0, N - 1);
}
然而也存在浪费的移动,那么是否有算法能够实现所有元素“一次到位”?而又不用辅助空间?
假设有数组:1 2 3 4 5 6
左移2 位得:3 4 5 6 1 2
右移4 位得:3 4 5 6 1 2
由此可以看成,左移k 位 = 右移n-k 位
再继续分析:如果我们想“一步到位”,那么对于左移3 位:可以将4 和1 交换,5 和2 交换,6 和3 交换,得到:
4 5 6 1 2 3
仅交换了3 次就得到结果,相比Reverse 法少了2 次,但是不是所有情况都这么简单?
再继续分析:对于序列 1 2 3 4 5 6 7,我们需要左移3 位,按照前面的“一步到位”法可以得到:
4 5 6 1 2 3 7,而我们期望得到的是 4 5 6 7 1 2 3
由于7%3=1,所以最后还剩了一个7,所以出现了错误…那么是不是“一步到位”法行不通?
再仔细观察下,其实剩下的7 可以递归地用统一方法去解决:
将序列的最后3 位局部循环右移1 位:
4 5 6 1 2 3 7 -> 4 5 6 7 1 2 3
由此,我们可以写出一个循环左移、循环右移互相调用的递归算法,实现“一步到位”,并且还提供了局部循环移动的能力。另外,由于循环左移、循环右移可以相互等价,这个算法还能自动寻找短的路走。
利用C#中的扩展方法,我们可以对IList<T>接口进行扩展,将来所有实现此接口的集合(Array,List 等)的实例都可直接调用,非常方便。
public static class CircleShift
{
public static void RightShift<T>(this IList<T> target, int k)
{
if (target.Count == 0 || k <= 0) return;
k %= target.Count;
RightShift(target, k, 0, target.Count);
}
public static void RightShift<T>(this IList<T> target, int k, int s, int n)
{
if (target.Count == 0 || k <= 0) return;
var r = n % k;
for (int i = s + n - 1 - k; i >= s + r; i--) Swap(i, i + k, target);
if (r > 0) LeftShift(target, r, s, r + k);
}
public static void LeftShift<T>(this IList<T> target, int k)
{
if (target.Count == 0 || k <= 0) return;
k %= target.Count;
LeftShift(target, k, 0, target.Count);
}
public static void LeftShift<T>(this IList<T> target, int k, int s, int n)
{
if (target.Count == 0 || k <= 0) return;
var r = n % k;
for (int i = s + k; i < s + n - r; i++) Swap(i, i - k, target);
if (r > 0) RightShift(target, r, s + n - r - k, r + k);
}
}
其中:
k 是要移位的位数
对于局部循环位移的函数,s 是起始号,n 是长度。
每次“一步到位”交换以后,再将余数长度的“尾巴”反向循环移动即可。
讨论
在实际应用中的大多数情况下,解法一的O(1)算法是最好的选择。解法二O(n)可谓实现了“效率高、实用好、形式简洁”的统一。
作者:Silver,原文链接:http://gpww.blog.163.com/blog/static/11826816420099784356482/
其他文章:
连载1:卡特兰数(Catalan)
连载2:序列 ABAB对应字符串集合
连载3:最长公共子序列
连载4:计算字符串的相似度
连载5:寻找符合条件的整数