循环移位问题是我面试技术人员的时候喜欢考的编码问题之一,对一个长度为n的数组,将其所有数据循环右移k位。对于编码能力要求不太高的岗位,比如部分测试,通常会让他做循环右移一位,大多数人员都可以成功的写出来;对于研发岗位,则要求写循环右移k位的算法,大部分人都不能正确的写出高效率的循环移位算法。
为简单起见,我们只考虑k在0和n之间的情况,如果k不在这个区间,很容易转换成这个区间内的等价问题。
《编程之美》的2.17节给出过一个很漂亮的算法,首先将前面n-k个数逆序,然后将后面k个数逆序,最后将所有n个数逆序。这个算法是非常简单而且非常精妙的,目前为止我只碰到过一个面试者能够正确的写出循环移位的代码,就是用了上面的算法。
但是从另外一个角度来说,非常精妙的算法很大程度上就变成了考核面试者是否曾经预先了解这个算法而非考核其基本的算法能力,这绝非我们的面试目标。这个问题,按照常规的算法思路也是可以求解的。
最简单的,我们可以将循环右移一位的算法调用k次,这样做的缺点是时间复杂度达到O(n*k),并非最优;另外一个方法是我们将末尾的k个数据暂存起来,完成移位之后再放到数组前面的k个空间,这样做的缺点是空间复杂度会达到O(k),同样并非最佳结果。
为了减少空间占用,我们不能保存k个数据,同样的,为了减少时间开销,我们需要将移动的数据一次移动到位,按照这个思路,我们可以设计出这样的算法:设数组为a,首先取第一个元素a[0],它的目标位置应该是a[k],于是我们把a[k]暂存起来,把a[0]放到a[k]的位置;而a[k]的目标位置应该是a[2k],于是我们把a[2k]暂存起来,把a[k]保存到a[2k]的位置,依次处理,直到所有的数据都移动到了目标位置,则移位完成。
需要特别关注两个问题,a[mk]的下标可能大于等于n了,这个容易处理,直接赋给a[mk%n]就可以;另外一个问题则比较难以关注到,假设n和k都是偶数,则a数组的下标为奇数的元素比如a[1],用上面的方法是永远无法访问到的,推广到更加普遍的场合,如果(n,k)的最大公约数不是1,则不能通过一次移位循环访问所有的数据,我们需要考虑改动下标的偏移量。
最终我们可以得到如下所示的循环移位算法:
def shift(a, k):
for i in range(gcd(len(a), k)):
cur = i
tmp = a[cur]
for j in range(len(a)//gcd(len(a), k)):
tgt = (cur+k)%len(a)
a[tgt], tmp = tmp, a[tgt];
cur = tgt
虽然我们使用了两层循环,其实际的复杂度是O(n)。
实际上,很难期待面试者能够一次写出上面的算法,我通常更希望能够通过交流和算法改进达到上面的目标,但是,如果连上面的内层循环代码也不能正确的写出来,甚至对移位的实现完全无感,则就没有了后续交流的基础了。