轮转数组问题:如何实现数组“整体逆序,内部有序”?“三步转换法”妙转数组

本文以C语言实现。

本文以力扣轮转整型数组问题为例,详解解题思路(主要是“三步转换法”,其他方法也会简单说明)。最后附有一道同题型拓展:倒置字符串,该拓展题也会附上详解。希望本文对诸位读者有所帮助。


目录

一、引例:数组向右轮转k个位置

题干

力扣OJ链接

二、题干解析

1. 思路一:从“轮转”的过程分析

轮转k次法

额外数组法

2. 思路二:从“轮转”的结果分析 

三、接上:三步转换法妙转数组

1. 从轮转结果分析思路

结论

2. 代码实现

三、拓展:倒置字符串

1. 题干

牛客网链接

2. 思路

3. 代码

四、总结


一、引例:数组向右轮转k个位置

题干

给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

 


力扣OJ链接

189. 轮转数组icon-default.png?t=M666https://leetcode.cn/problems/rotate-array/


二、题干解析

根据对“数组轮转”这一操作的不同理解,本题可以分为两种宏观思路。我们一种一种介绍。

1. 思路一:从“轮转”的过程分析

从示例中我们不难发现,本题“向右轮转”的每次操作都相同:将数组中最右(最后)一个元素取出,换成数组的第一个元素;而其它元素整体顺序不变。即可将“轮转k次”拆分为:

数组每次向右轮转1次,一共轮转了k次。

从这个角度出发,我们可将整个轮转的大过程看作是一个大循环,我们只需要搞清楚进行一次轮转做了哪些事,再循环k次,就能获得最终的结果。

轮转k次法

我们考虑直接在原数组上进行操作,对每一次“轮转”的过程进行拆解。

用nums表示数组,用numsSize表示数组的长度。

每轮转转1次时,可归纳出如下步骤:

  1. 取走数组中最后一个数(是nums[ numsSize-1 ] ,注意下标),暂时存放在变量tmp中;
  2. 从 0到(numsSize-2) 的每一个数都向后挪动一位,即 nums[end+1] = nums[end],以此来空出首元素位置;
  3. 空下来的第一位存放原数组的最后一位,即tmp
  4. 以上这个过程循环 k 次,就是轮转k次后的结果
轮转k次法 图示

这种思路最直接,也最容易想到。基于以上思路,我们可以给出如下接口函数:

void rotate(int* nums,int numsSize, int k) {
    for(int i = 0; i < k; i++){
        int tmp = nums[numsSize-1];
        for (int end = numsSize-2; end >= 0; end--) {
            nums[end+1] = nums[end];
        }
        nums[0] = tmp;
    }
}

这种方式是可以实现的。但并不够好。

它的时间复杂度为O(N*K),事实上该代码在leetcode中是跑不过的,因为效率实在太低:

因而我们考虑,能否对现有的代码进行优化,提高代码的时间效率。 


额外数组法

额外数组法的宏观思路与轮转k次法相同,均是通过拆解单次轮转的过程,再配上k次循环实现。但在细节操作上有所差异:额外数组法通过开辟一个临时数组temp,暂时存放要移出原数组的数。

这就像小和尚从山下提水到山顶的水缸里。轮转k次法相当于每次提水都只有小和尚一个人干,而且他每打满一桶水,就嘎嘎地从山底往山顶跑,卸完一桶水又跑下山再打一桶。一个人,每打完一桶水就要往返山顶和山脚,如果一共需要6桶水,那小和尚就要一个人跑6次,自然效率就不高了。

但小和尚很聪明,他对这个办法进行了改变:他又叫来了5个人,每个人提个桶帮他一块儿打水。这样,小和尚打完一桶水不必要立刻跑上山,等其余5个人都打完水了,再一块上山,一块儿把水卸进水缸里。这样一口气搬运,人手充足,6桶水只需要搬运一趟。这就是额外数组法

具体操作如下:

  1. 开辟一个临时数组temp,暂时存放要移出原数组的那几个数。(相当于在原数组后面又拼了一段temp,移出去的数都存放到了temp里面)
  2. 然后再向右移动原数组内的数,一次移动 k 个单位(因为移出去了 k 个数)
  3. 最后把temp里面的元素归还到nums数组的前几个

 

额外数组法 图示

依次思路,可以给出如下代码:

void rotate(int* nums,int numsSize,int k) {
    //开辟一个新的数组
    k = k % numsSize;    //考虑到 k > numsSize 的情况
    int* temp = (int*)malloc(k * sizeof (int));    //根据k的实际情况开辟新数组
    //移出去的元素放进新的数组里面
    int j = 0;
    for (int i = numsSize-k; i <= numsSize-1; i++) {
        temp[j] = nums[i];
        j++;
    }
    //原数组各元素向右移动k位
    for (int i = numsSize-1; i >= k; i--) {
        nums[i] = nums[i-k];
    }
    //再把多出来的元素放到原数组首
    for (int i = k-1;i>= 0; i--) {
        nums[i] = temp[i];
    }
}

注意

  • 遇到数组,一定要注意控制下标!!!注意数组越界问题。强烈建议画图找规律(用“三板斧”分析),凭空想象的话,很容易想错,而且下标出现的问题一时不太好找,容易被忽略,最好从头开始就不要出错。
  • 该算法的时间复杂度与空间复杂度均为O(N)。
  • 注意 k %= numsSize这一语句:这是防止数组越界的关键步骤之一。当k大于数组长度时,进行取模运算。

2. 思路二:从“轮转”的结果分析 

这就是我们今天要重点介绍的“三步转换法”。我们单独开一个目录板块来聊聊这个。


三、接上:三步转换法妙转数组

1. 从轮转结果分析思路

要达到数组轮转,有一个很厉害的思路:三步转换法。我们直接进行介绍。

前面提到,这是一种从轮转的结果进行切入的思路:

从轮转结果分析

这就是所谓的“整体逆序,内部有序”。当把①和②分别看作两块整体元素时,①和②相对于原数组而言是逆序的。而当①和②分别看作一个单独的数组时,它们的内部又是有序的。注意,这里说的“有序”“逆序”是和原数组相比,顺序是否发生变化(不是说升序降序的那个排序)。

结论

要达到向右轮转k次使数组“整体逆序,内部有序”的状态,只须三步:

将后k个逆置,将前(n-k)个逆置,最后整体逆置。

图解如下:

三步转换法 图示

同样,若以同样的规则向左轮转,依旧是三步:将前k个逆置,将后(n-k)个逆置,最后整体逆置。

三步转换法--向左轮转

 

以此我们可以进一步归纳总结,得出三步转换法结论:

若有轮转方向规定:向方向D轮转,就先将D方向的那k个逆置,再将剩下的(n-k)个元素逆置,最后整体逆置即可。(其实在数组整体逆置之前,先逆置内部哪部分是并不影响结果的,只要保证每个部分的内部都逆置过了即可。只是个人认为按方向分一分会比较好理解,不容易搞错。)

若无轮转方向规定(只要达到逆置的结果即可):就不用考虑“先向哪个方向逆置”的问题(不用考虑方向),只需要挨个将所有块内部的元素逆置,最后整体逆置即可。逆置的过程可以封装成函数,遇到时调用即可。

言而总之,就一句话:三步逆置,将数组分块,块内元素先分别逆置,最后再整体逆置。


2. 代码实现

//将逆置操作封装为函数
void reverse(int* nums,int left,int right) {
    while(left < right) {
        int tmp = nums[left];
        nums[left] = nums[right];
        nums[right] = tmp;
        left++;
        right--;
    }
}

void rotate(int* nums, int numsSize, int k){
    if(k > numsSize){
        k %= numsSize;
    }
    reverse(nums,numsSize-k,numsSize-1);
    reverse(nums,0,numsSize-k-1);
    reverse(nums,0,numsSize-1);
}

总结一下:

  • 这个结论非常巧妙,我们最好记住。
  • 该算法的空间复杂度为O(1),时间复杂度为O(N)。
  • 对于逆置函数:可以用交换位置的算法实现,并设置上界left和下界right,实现在某一个区间内(left到right之间)进行逆置操作,用while( left<right )控制。顺带一提:这是一个最简单的双指针+区间划分类问题,对于该类题型,我们后续会慢慢讨论
  • 由于逆置操作要进行多次且每次的逻辑步骤都一样,所以可以单独封装成函数来调用。

三、拓展:倒置字符串

说实话,当我做到这道题的时候,我还是很感动的o(* ̄▽ ̄*)ブ

这道题是上面三步转换法非常合适的一个变式。 

1. 题干

牛客网链接

倒置字符串icon-default.png?t=M666https://www.nowcoder.com/questionTerminal/8869d99cf1264e60a6d9eff4295e5bab

将一句话的单词进行倒置,标点不倒置。

比如 "I like beijing.",经过处理后变为:"beijing. like I"

(字符串长度不超过100)


2. 思路

这也是“整体逆序,内部有序”的典例:我们将字符串内容按单词分块,单词在整个句子中的顺序发生变化,但单词还是单词,它内部的字母顺序并没有发生变化。

当我们明白这一点,接下来的事情也许会好办许多。

整体逆序,内部有序

 


3. 代码

这里我们采用指针+while循环的方式来对字符串数组进行操作。这里是单纯的倒置,不存在向左或向右轮转的方向问题,因此方向这里就不用考虑了。

此外还有一些细节方面需要注意,如scanf读不了空格,当字符串中需要空格的时候,最好用gets()函数读取 

//将逆置操作封装成函数
void reverse(char* left, char* right)
{
    //加了两句断言
    assert(left);
    assert(right);

    while (left < right)
    {
        char tmp = *left;
        *left = *right;
        *right = tmp;
        left++;
        right--;
    }
}

int main()
{
    char arr[101] = { 0 };
    //注意要用gets()函数,因为它可以读入空格
    gets(arr);

    //用指针来操作数组
    char* cur = arr;

    //逆序每个单词
    while (*cur)
    {
        char* start = cur;
        char* end = cur;
        while (*end != ' ' && *end != '\0')
        {
            end++;
        }
        reverse(start, end - 1);
        if (*end != '\0')
            cur = end + 1;
        else
            cur = end;
    }

    //逆序整个字符串
    int len = strlen(arr);
    reverse(arr, arr + len - 1);

    printf("%s\n", arr);

    return 0;
}

 


四、总结

  1. 三步逆置:将数组分块,块内元素先分别逆置,最后再整体逆置。若无轮转顺序要求,则不需要考虑先逆置数组中的哪一块内的元素。
  2. 本文详细介绍了三步逆置法在倒置字符串和整型数组中的应用,以及两种代码实现。在倒置字符串时我们不需要考虑轮转顺序,只要达到逆置的结果即可,该代码是用指针+区间划分的思想实现的,我们用了while循环来控制。
  3. 在轮转数组的引例中,题目要求的向右轮转,我们就先将右边的k个元素逆置,再将剩下的左边(n-k)个元素逆置,最后整体逆置。虽然本文似乎有强调轮转方向的差别,但其实在数组整体逆置之前,先逆置内部哪部分是并不影响结果的,只要保证每个部分的内部都逆置过了即可。先逆置左边的(n-k)个,再逆置右边的k个,最后再整体逆置,达到的效果是一样的。强调方向仅仅是便于一些同学理解。
  • 27
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值