LeetCode之旅(C):189. 旋转数组

PS:不明之处,请君留言,以期共同进步!

1. 题目描述

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

示例 1:

输入: [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]

示例 2:

输入: [-1,-100,3,99] 和 k = 2
输出: [3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]

说明:

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

2. 思路一(这种思路存在错误,有的测试用例通不过!)

刚开始的时候我是这么想的:
(1)先将目标下标处的值缓存到一个变量中;
(2)再将源下标处的值存入目标下标处;
(3)再将目标下标当做源下标使用
如此循环,源下标和目标下标跳来跳去,就像跳舞一样,多美妙啊!最终源下标和目标下标都能跳过所有的位置且在每个位置上只跳了一次,这样就能实现题目的要求。
我自认为这样的思路很巧妙,时间复杂度为O(n),空间复杂度为O(1),殊不知经过一番测试,发现有的测试用例通不过:
LeetCode之旅(C):189. 旋转数组_第1张图片
分析发现,当numsSize能被k(k != 1)整除时,源下标和目标下标不能在所有的位置上跳舞,就拿上面的截图做例子,numsSize = 6, k =2。当 i = 0 时,j = 2;当 i = 2 时,j = 4;当 i = 4 时,j = 0;接下来就又回到了 i = 0,i 在 0 的位置跳了两次,所以出错了。这种思路错就错在这里!

void rotate(int* nums, int numsSize, int k)
{
    if(!(k % numsSize))
        return;
        
    int n = 0;
    int i = 0, j;
    int num1 = nums[i], num2;

    while(n < numsSize)
    {
        j = (i + k) % numsSize;
        num2 = nums[j];
        nums[j] = num1;
        num1 = num2;
        i = j;
        ++n;
    }
}

3. 思路二(对思路一进行改进,执行超时!)

我们这样对思路一进行改进:既然每次跳 k 步会出现思路一的错误,那我每次跳一步,然后一共跳 k 次,这肯定不会出错了吧?是的。但是这又引入了新的问题,时间复杂度变成了O(n*k),执行超时!所以这也不是理想的解决方法。

void rotate(int* nums, int numsSize, int k)
{
    if(!(k % numsSize))
        return;
        
    k %= numsSize;
    
    while(k)
    {
    	int temp = nums[numsSize - 1];
        for(int i = numsSize - 2; i >= 0; --i)
        {
            nums[i + 1] = nums[i];
        }
        nums[0] = temp;
        --k;
    }
}

4. 思路三

通过申请一个临时数组来暂存移动后的所有元素,再把临时数组复制到nums来实现题目要求。
这样的做法空间复杂度是O(n),也不太好。

void rotate(int* nums, int numsSize, int k) 
{
    if(!(k % numsSize))
        return;
    
    int i;
	int result[numsSize];
    
	for(i = 0; i < numsSize; ++i)
	{
		result[(i + k) % numsSize] = nums[i]; 
    }  
    
    for(i = 0; i < numsSize; ++i)
    {
        nums[i]=result[i];
    }
}

5. 思路四

我实在没辙了,去网上搜索别人的解法,发现一个很巧妙的思路:
首先把nums数组在逻辑上分成两段,前半段是下标 0 ~ (numsSize - k - 1)之间的元素,后半段是下标(numsSize - k)~(numsSize - 1)之间的元素,也就是前半段是前(numsSize - k)个元素,后半段是后 k 个元素。然后将前半段的元素做镜像交换,再将后半段的元素做镜像交换,最后将整个nums数组的元素做镜像交换。这样就能实现题目的要求。
同样的,思路三的时间复杂度为O(n),空间复杂度为O(1),而且三次镜像交换着实巧妙。
我顺着思路三写代码,又遇到了一个新的难题,刚开始我在每个for循环里只设置了一个循环变量 i,那样的话会带来很多麻烦,不仅要计算循环停止位置的下标(它是在前半段或后半段数组中间的一个位置),还要计算交换元素的两个位置中的后一个位置的下标,我怎么也算不对。后来我看到别人设置了两个循环变量 i 和 j,i 作为开始,j 作为结束,两者相遇则停止循环,这样一来麻烦的问题迎刃而解。不是自己不会这么写,真的是好久没写算法了,警钟!

void rotate(int* nums, int numsSize, int k)
{
    if(!(k % numsSize))
        return;
    
    int i, j, temp;
    k %= numsSize;
    
    for(i = 0, j = numsSize - k - 1; i < j; ++i, --j)
    {
        temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    
    for(i = numsSize - k, j = numsSize - 1; i < j; ++i, --j)
    {
        temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
    
    for(i = 0, j = numsSize - 1; i < j; ++i, --j)
    {
        temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

到目前为止,我最满意的是思路四。

你可能感兴趣的:(LeetCode之旅)