数组入门练习:一维数组循环移动的两种解法

NC110 旋转数组

题目大意

给出一个长度为 n n n 的一维数组 a r r arr arr,将 a r r arr arr 中的每个元素循环向右移动 m m m 个位置。更严格一点的定义是:将原本在位置 i i i 处的元素移动至 ( i + m )   m o d   n (i+m)\bmod n (i+m)modn 处。

比如,输入为 ( 1 , 2 , 3 , 4 , 5 , 6 ) (1,2,3,4,5,6) (1,2,3,4,5,6) m = 2 m=2 m=2,操作结果如下图示:

方法一

首先明确一件事情,根据取模运算的分配律可得
( i + m )   m o d   n = ( i   m o d   n + m   m o d   n )   m o d   n = ( i + m   m o d   n )   m o d   n \begin{aligned} (i+m)\bmod n &= (i\bmod n + m\bmod n)\bmod n \\ &= (i + m\bmod n)\bmod n \end{aligned} (i+m)modn=(imodn+mmodn)modn=(i+mmodn)modn

从操作上来讲,上述式子也很好理解——循环向右移动 n n n 个位置,必然回到起始位置。

因此,在移动之前先进行 m = m   m o d   n m=m\bmod n m=mmodn,避免做无用功。现在 m ∈ [ 0 , n ) m∈[0,n) m[0,n) 了。

通过观察不难发现,操作完成后:

  • m m m 个元素被移到了前 m m m 个位置。
  • n − m n-m nm个元素被移到了后 n − m n-m nm 个位置。

于是,先将整个数组翻转一次。

如上图所示,翻转之后,原先的最后 m m m 个元素的确移到 前 n − m n-m nm 个元素之前了,不过次序是反的。为了调整次序,可将区间 [ 0 , m ) [0,m) [0,m) [ m , n ) [m,n) [m,n) 再各进行一次翻转。

整个翻转过程如下图所示:
在这里插入图片描述
《数组入门指北》中提到过翻转的套路:将前 n 2 \frac{n}{2} 2n 个元素与后 n 2 \frac{n}{2} 2n 元素交换位置。

因为本题中需要进行三次翻转:

  • 整个数组翻转一次
  • m m m 个元素翻转一次
  • n − m n-m nm 个元素翻转一次

所以我们可以将翻转的代码封装为一个函数,这样代码更加整洁(说不定你的面试官有代码洁癖呢)。代码如下:

class Solution {
public:
    void reverse(vector<int> &a, int L, int R) {
        for (int i = (L+R)/2; i >= L ; i--) {
            int tmp = a[i];
            a[i] = a[R+L-i];
            a[R+L-i] = tmp;
        }
    }
    vector<int> solve(int n, int m, vector<int>& a) {
        m %= n;
        if (m == 0) {
            return a;
        }
        reverse(a, 0, n-1);
        reverse(a, 0, m-1);
        reverse(a, m, n-1);
        return a;
    }
};

方法二

方法一算是比较取巧的做法了,不是很直观。接下来讲解一下模拟循环右移的做法。

考虑有 x x x 为满足 x ∗ m   m o d   n = 0 x*m \bmod n = 0 xmmodn=0 的最小正整数,不难发现 1 ≤ x ≤ n 1\le x \le n 1xn。可将 n n n 个元素拆分为 n x \frac{n}{x} xn 个环,每个环有 x x x 个元素。

上面这段看不懂也没关系,后面有机会讲数学部分的时候,我们再讨论。

上面这段的目的是想说,可将 n n n 个元素划分为若干个环,属于同一个环的相邻的下标 p p p q q q 满足 ( p + m )   m o d   n = q (p+m)\bmod n = q (p+m)modn=q,换言之, p p p 处的数据会移动到 q q q 处。

于是,我们可将整个数组视作若干个独立的等长的子序列,只要完成所有子序列的位移,就完成了整个数组的位移。

如下图所示,要对一个长度为 8 的数组,向右移动两次。首先,将数组分成两个子序列。相同颜色的数据属于同一个子序列。移动过程,如下图所示:

整个过程,可总结为:

  • 首先,找到 x x x
  • 枚举前 n x \frac{n}{x} xn 个位置,相当于枚举每个环中的最小的位置。
  • 移动每个环中的数据。

代码如下:

class Solution {
public:

    vector<int> solve(int n, int m, vector<int>& a) {
        m %= n;
        if (m == 0) {
            return a;
        }
        int x = 1;
        while (x*m%n != 0) {
            x++;
        }
        for (int i = 0; i < n/x; i++) {
            if (a[i] < 0) { continue; }
            for (int j = (i+m)%n; j != i; j = (j+m)%n) {
                int tmp = a[j];
                a[j] = a[i];
                a[i] = tmp;
            }
        }
        return a;
    }
};
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值