数组循环移位

问题

设计一个算法,把一个含有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)可谓实现了“效率高、实用好、形式简洁”的统一。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值