题目描述

一个数组A中存有N(N&gt0)个整数,在不允许使用另外数组的前提下,将每个整数循环向右移M(M>=0)个位置,即将A中的数据由(A0 A1 ……AN-1 )变换为(AN-M …… AN-1 A0 A1 ……AN-M-1 )(最后M个数循环移至最前面的M个位置)。如果需要考虑程序移动数据的次数尽量少,要如何设计移动的方法?

示例1

输入:

6,2,[1,2,3,4,5,6]

返回值:

[5,6,1,2,3,4]

方法一:模拟

解题思路

首先一个很简单明了的想法,因为题目里说的是整体向右移动 M 个位置,那么我们可以先写好每次只向右移动 1 位的操作,再循环 M 次这个操作就可以得到最后的结果。

这里可以做一个小优化,就是如果传入的 M 参数,比数组的长度还要长,我们可以将 M 这个数字缩小来减少循环的次数,也就是减少移动的次数。

如果 M 的值和数组的长度是相等的,那么数组向右整体移动 M 个位置得到的结果是没有变的,之前的数字在什么位置,移动后这个数字仍然在原位置。所以这里可以将 M 对 数组的长度取余,只需要移动的次数比数组的长度小即可。

想法很简单,思路也很简单、代码也很简单,那么下面就看看图示和代码示例吧:

图片说明

代码示例

import java.util.*;


public class Solution {
    /**
     * 旋转数组
     * @param n int整型 数组长度
     * @param m int整型 右移距离
     * @param nums int整型一维数组 给定数组
     * @return int整型一维数组
     */
    public int[] solve (int n, int m, int[] nums) {
        if (nums == null || nums.length == 0) {
            return null;
        }

        // 传入的 m 值,可能比数组的长度还大;
        // 此时做一个取模运算即可,减少一些不必要的移动
        int shift = m % nums.length;

        // 数字向右移动一位,这个操作需要进行 shift 次
        for (int i = 0; i < shift; i++) {
            rightShiftOne(nums);
        }

        return nums;
    }


    /**
     * 将 nums 数组的所有数字,往右移动 1 位
     */
    public void rightShiftOne(int[] nums) {
        int len = nums.length;
        // 记录下最后1位的数字
        int temp = nums[len - 1];
        // 除最后1位数字外,所有数字往右移动1位
        for (int i = len - 1; i > 0; i--) {
            nums[i] = nums[i - 1];
        }
        // 将最后1位的数字移动到数组第1位
        nums[0] = temp;
    }
}

复杂度分析

时间复杂度:O( (M%N) * N )

M 为向右移动的位置个数;N 为数组的长度。

首先将数组中所有的数字向右移动 1 位,这里需要遍历完整个数组元素,时间复杂度为 O(N)。

其次这样的操作需要 M % N 次,所以这是一个双重循环遍历,

最后总的时间复杂度为 : O( (M%N) * N )

空间复杂度:O(1)

代码里只是声明了有限的几个变量来方便操作,并没有开辟和数据量 N 有关的空间。

所以,空间复杂度为 : O(1)。

方法二:反转

解题思路

为了尽可能的减少移动数据的次数,可以采取另外的思路。

可以先自己找几个数组右移 M 个位置,看最后的结果和初始状态有什么区别。可以发现最后的结果可以分为2个部分,这个2个部分一起看的话是没有顺序的,但是分开看,分别集中在这2个部分的各自范围看,可以发现内部是有序的。

这里说的有序无序不是相对于自然数的状态来说的,是相对于原数组的状态来说的,也就是说和原数组的位置顺序区别有没有变化。

就好像一个有序的数组,把他从某一点分割开,再把这2部分调换位置一样。

所以,我们可以通过反转数组来达到同样的结果,因为反转 2 次就还是和原来一样,等于没有反转。

首先我们将整个数组进行反转,然后再看数组中那2个部分的元素分别是多少个,然后再分别反转这些个元素,让这些个元素是有序的,就好像这些个元素是没有变化的。但这样最后的结果就是2个部分一起看是没有顺序的,但分开集中在这2个部分的各自范围看,是有序的,也就达到了我们想要的状态。

另外,为尽可能的减少数据移动的次数,减少空间的开销,在反转的过程中,可以采用双指针的思想,让头部尾部分别进行,一次就交换到位,不用另外开辟一块同样大小的空间。

另外,在反转的时候,如果传入的参数 M 比数组的长度还要大的话,就要做一步取余操作,原因和方法一里阐述的是一样的。

具体的流程及实现请参考下面的图示和代码示例:

图片说明

代码示例

import java.util.*;


public class Solution {
    /**
     * 旋转数组
     * @param n int整型 数组长度
     * @param m int整型 右移距离
     * @param nums int整型一维数组 给定数组
     * @return int整型一维数组
     */
    public int[] solve (int n, int m, int[] nums) {
        if (nums == null || nums.length == 0) {
            return null;
        }

        // 数组的长度
        int len = nums.length;
        // 右移的位数可能会比长度还要大,取个模就好了
        int shift = m % len;

        // 反转整个数组
        reverse(nums, 0, len - 1);
        // 反转数组的前 shift 个数
        reverse(nums, 0, shift - 1);
        // 反转数组剩余数字
        reverse(nums, shift, len - 1);

        return nums;
    }


    /**
     * 将数字 nums[left...right] 范围上的数字反转
     */
    public void reverse(int[] nums, int left, int right) {
        if (left >= right) {
            return;
        }

        // 用左右指针,相互交换即可
        int times = (right - left + 1) / 2;
        for (int i = 0; i < times; i++) {
            swap(nums, left++, right--);
        }
    }

    /**
     * 将 nums[i] , nums[j] 这两个数字交换
     */
    public void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

复杂度分析

时间复杂度:O(N)

整个代码流程分为 3 个 reverse 操作,在 reverse 里面,有一个用双指针的思想进行遍历数组的操作,总共需要遍历 N / 2 次,即这里的时间复杂度为 O(N),外面有 3 次这样的重复操作,对于不固定的数据量 N,常熟级别的消耗是可以忽略的。

所以最后总的时间复杂度为 O(N)。

空间复杂度:O(1)

整个代码流程没有开辟和数据量 N 有关的空间,只是用了有限几个变量。

所以,最后总的空间复杂度为 : O(1)。