zoukankan      html  css  js  c++  java
  • C++旋转数组(三种解法详解)

    题目描述

    给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

    附加要求

    • 尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
    • 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗?

    样例输入与输出

    输入: nums = [1,2,3,4,5,6,7], k = 3
    输出: [5,6,7,1,2,3,4]
    解释:
    向右旋转 1 步: [7,1,2,3,4,5,6]
    向右旋转 2 步: [6,7,1,2,3,4,5]
    向右旋转 3 步: [5,6,7,1,2,3,4]
    
    输入:nums = [-1,-100,3,99], k = 2
    输出:[3,99,-1,-100]
    解释: 
    向右旋转 1 步: [99,-1,-100,3]
    向右旋转 2 步: [3,99,-1,-100]
    

    提示

    • 1 <= nums.length <= 2 * 104
    • -231 <= nums[i] <= 231 - 1
    • 0 <= k <= 105

    解法1(直接新开数组赋值)

    思路

    创建一个一模一样大小的数组,按照题目描述的规则进行赋值,最后再把值赋给原本的数组

    代码

    class Solution {
    public:
        void rotate(vector<int>& nums, int k) {
            int n = nums.size();
            int temp[n];
            memset(temp,0,sizeof(int)*n);
            k %= n;
            for(int i = 0;i < k;i++){
            	temp[i] = nums[n-k+i];
            }
            for(int i = k;i < n;i++){
            	temp[i] = nums[i-k];
            }
            for(int i = 0;i < n;i++)
            	nums[i] = temp[i];
        }
    };
    

    复杂度分析

    时间复杂度O(n)

    空间复杂度O(n)

    解法2(环状替换)

    思路

    对于一个数组,如a[7] = [1, 2, 3, 4, 5, 6, 7],当k = 1时,数组最终会变成 a'[7] = [7, 1, 2, 3, 4, 5, 6],对于每一个数组元素而言,新的位置下标x2与原位置下标x1的关系为x2 = (x1+1) % n,例如数组元素 7 的下标 a[6] -> a[0],(6+1) % 7 = 0。经过观察,可以较为容易的发现每个数组元素的新下标x2 = (x1+k) % n。

    接下来要做的就是将每个数组元素的下标都变换一次,如果按照数组元素的顺序开始循环,会导致一部分元素被覆盖,得不到记录,因此这里新定义一种变换方法,即从第一个数组元素开始,依次变换它的新下标的元素的下标,以数组 a2 = [1, 2, 3, 4, 5, 6],k = 2为例,即a[0] -> a[2],使用一个中间变量 temp记录 a[2] 的值,在a[0] 完成变换后紧接着对a[2]进行变换。

    算法模拟1:数组 1 2 3 4 5 6 7,k = 3
    a[0] -> a[(0+3)%7 = 3],a[3] -> a[(3+3)%7 = 6],a[6] -> [(6+3)%7 = 2],a[2] -> a[(2+3)%5 = 5],a[5] -> a[(5+3)%7 = 1],a[1] -> a[(1+3)%7 = 4],a[4] -> a[(4+3)%7 = 0],a[0] -> ...
    

    从a[0]开始到重新回到a[0]的过程,我将其定义为一个环,在这个例子中,一次环的遍历即可完成所有数组元素下标的变换,接下来看另一个例子。

    算法模拟2:数组 1 2 3 4,k = 2
    a[0] -> a[(0+2)%4 = 2],a[2] -> a[(2+2)%4 = 0],a[0] -> ...
    

    算法模拟2中,一个环显然不能解决问题,因此当再次回到a[0]的时候,需要从a[0]的下一个元素开始,继续完成下标变换。

    • 问题:如何才能保证将数组中的每个元素都遍历到?

    • 解决方案A:使用一个变量记录当前已经遍历过元素的个数,由于每个元素都需要改变下标,因此可以使用一个中间变量count,每完成一次下标变换,count++,直到count = n,结束循环。

    • 解决方案B:计算出需要循环的次数,首先假设这个数组是往右无线延伸的,那么整个过程如下

    数组A 1 2 3 4 5 6 7,k = 3
    1 2 3 4 5 6 7 1 2 3 4 5 6 7 1 2 3 4 5 6 7 1 2 3 4 5 6 7 ...
    1 --->4---->7---->3---->6---->2---->5---->1 ...
    
    数组B 1 2 3 4,k = 2
    1 2 3 4 1 2 3 4
    1-->3-->1
      2-->4-->2
    

    经过观察可以发现这样一个数学关系式:遍历过的数组数量 a * 数组长度 n = 遍历过的元素个数 b * 移动的距离 k,对于数组A,3*7 = 7*3,对于数组B,1*4 = 2*2(有两个这样的式子)。

    现在将这个关系式推广到一般情况,其中数组长度 n 是已知,现在已经遍历过 b 个元素,那么接下来需要知道还要经过多少次这样的遍历就可以遍历完所有元素,记为count,且count为正整数。b = an/k,an需要尽可能的小,因为多的话又回到了起点,又开始重复之前的遍历了,a,n,b,k 均为正整数,an 是 n 的倍数,an 是 k 的 b 倍,要让 a 尽可能小,an 的值应该为 n,k的最小公倍数,记为lcm(n,k),那么就有 bk = lcm(n,k),b = lcm(n,k)/k。这样一次遍历就遍历了 b 个元素,那么 n 个元素需要 count = n/b = nk/lcm(n,k) = gcd(n,k),gcd指最大公约数。

    至此,循环的次数就确定下来了,接下来只需要写对应的代码就行了。

    代码

    //解决方案A
    class Solution {
    public:
        void rotate(vector<int>& nums, int k) {
              int n = nums.size();
              k %= n;
              int count = 1;
              int start = 0;
              while(count <= n){
              	int current = start;
              	int prev = nums[start];
              	do{
                    int i = (current+k)%n;
              		int temp = nums[i];
              		nums[i] = prev;
              		prev = temp;
                    current = i;
              		count++;
              	}while(current != start);
                start++;
              }
        }
    };
    
    //解决方案B
    class Solution {
    public:
        void rotate(vector<int>& nums, int k) {
              int n = nums.size();
              k %= n;
              if(k == 0)
                return;
              int count = gcd(n,k);
              for(int i = 0;i < count;i++){
              	int current = i;
              	int prev = nums[i];
              	do{
              		int j = (current+k) % n;
              		int temp = nums[j];
              		nums[j] = prev;
              		prev = temp;
              		current = j;
              	}while(current != i);
              }
        }
        int gcd(int n,int k){
        	int m = k;
        	while(n%k){
        		m = n%k;
        		n = k;
        		k = m;
        	}
        	return m;
        }
    };
    

    复杂度分析

    时间复杂度O(n)

    空间复杂度O(1)

    解法3(Reverse)

    思路

    翻转数组,先将整个数组翻转,再翻转前k个数,最后再翻转剩余的部分,翻转数组可以使用位运算,即使用异或运算实现数值交换。原理如下

    定义:对于任一位向量a,有a^a=0,a^0=a
    现假定有 a,b
    a = a^b 
    b = a^b //此时的 a = a^b,则b = a^b^b = a
    a = a^b //此时的 a = a^b,b = a,则 a = a^b^a = b
    

    除此之外,也可以使用相加寄存的方式实现数值交换,原理如下

    a = a+b
    b = a-b //此时a = a+b,则b = a+b-b = a
    a = a-b //此时a = a+b,b = a,则a = a+b-a = b
    

    代码

    class Solution {
    public:
        void rotate(vector<int>& nums, int k) {
            int n = nums.size();
            k %= n;
            if(k == 0) 
                return;
            reverse(nums, 0, n - 1);
            reverse(nums, 0, k - 1);
            reverse(nums, k, n - 1);
        }
        void reverse(vector<int>&nums,int l,int r){
            while (l < r) {
                nums[l] = nums[l] ^ nums[r];
                nums[r] = nums[l] ^ nums[r];
                nums[l] = nums[l] ^ nums[r];
                l++;
                r--;
            }
    	}
    };
    
    Reverse()函数重写,其余部分一样
    void reverse(vector<int>&nums,int l,int r){
            while (l < r) {
                nums[l] = nums[l] + nums[r];
                nums[r] = nums[l] - nums[r];
                nums[l] = nums[l] - nums[r];
                l++;
                r--;
            }
    

    复杂度分析

    时间复杂度O(2n) = O(n)

    空间复杂度O(1)

  • 相关阅读:
    重读《从菜鸟到测试架构师》-- 职业生涯的考虑
    重读《从菜鸟到测试架构师》-- 从专家到高手
    重读《从菜鸟到测试架构师》-- 开发团队做的远不仅是开发
    重读《从菜鸟到测试架构师》-- 测试专家的第一步
    重读《从菜鸟到测试架构师》-- 前篇
    使用Nodejs+Protractor搭建测试环境
    简单的算法题之合并数组
    性能测试流程
    性能测试基础
    电脑木马
  • 原文地址:https://www.cnblogs.com/NikkiNikita/p/14343337.html
Copyright © 2011-2022 走看看