当前位置:网站首页>轮转数组问题:如何实现数组“整体逆序,内部有序”?“三步转换法”妙转数组
轮转数组问题:如何实现数组“整体逆序,内部有序”?“三步转换法”妙转数组
2022-08-11 03:03:00 【碳基肥宅】
本文以C语言实现。
本文以力扣轮转整型数组问题为例,详解解题思路(主要是“三步转换法”,其他方法也会简单说明)。最后附有一道同题型拓展:倒置字符串,该拓展题也会附上详解。希望本文对诸位读者有所帮助。
目录
一、引例:数组向右轮转k个位置
题干
给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

力扣OJ链接
189. 轮转数组
https://leetcode.cn/problems/rotate-array/
二、题干解析
根据对“数组轮转”这一操作的不同理解,本题可以分为两种宏观思路。我们一种一种介绍。
1. 思路一:从“轮转”的过程分析
从示例中我们不难发现,本题“向右轮转”的每次操作都相同:将数组中最右(最后)一个元素取出,换成数组的第一个元素;而其它元素整体顺序不变。即可将“轮转k次”拆分为:
数组每次向右轮转1次,一共轮转了k次。

从这个角度出发,我们可将整个轮转的大过程看作是一个大循环,我们只需要搞清楚进行一次轮转做了哪些事,再循环k次,就能获得最终的结果。
轮转k次法
我们考虑直接在原数组上进行操作,对每一次“轮转”的过程进行拆解。
用nums表示数组,用numsSize表示数组的长度。
每轮转转1次时,可归纳出如下步骤:
- 取走数组中最后一个数(是nums[ numsSize-1 ] ,注意下标),暂时存放在变量tmp中;
- 从 0到(numsSize-2) 的每一个数都向后挪动一位,即 nums[end+1] = nums[end],以此来空出首元素位置;
- 空下来的第一位存放原数组的最后一位,即tmp
- 以上这个过程循环 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桶水只需要搬运一趟。这就是额外数组法。
具体操作如下:
- 开辟一个临时数组temp,暂时存放要移出原数组的那几个数。(相当于在原数组后面又拼了一段temp,移出去的数都存放到了temp里面)
- 然后再向右移动原数组内的数,一次移动 k 个单位(因为移出去了 k 个数)
- 最后把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. 题干
牛客网链接
倒置字符串
https://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;
}

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

Summary of Logstash log data write exception troubleshooting

Idea (优选)cherry-pick操作

①CAS SSO单点登录框架源码深度分析

深度学习-第二次

A practice arrangement about map GIS (below) GIS practice of Redis

BUU刷题记录

《人生若如初见》命运多舛,人物饱满,朱亚文角色反差太惊喜

【Unity入门计划】Unity2D动画(1)-动画系统的组成及功能的使用

图解LeetCode——640. 求解方程(难度:中等)

ES6 advanced string processing new features
随机推荐
Goodbye Chongqing paper invoices!The issuance of electronic invoices for accommodation expenses will soon completely replace the invoices of hotels, catering and gas stations
Briefly, talk about the use of @Transactional in the project
ES6 advanced string processing new features
【Unity入门计划】Unity2D动画(1)-动画系统的组成及功能的使用
互换性测量技术-几何误差
四大组件---ContentResolver
SQL 开发的十个高级概念
CSDN blog replacement skin
KingbaseES有什么办法,默认不读取sys_catalog下的系统视图?
ifconfig与ip命令的比较
ES进阶 函数功能语法新特性详解
JS-DOM元素对象
df和df -lh的意思
The problem that Merge will be lost again after code Revert has been solved
Multi-threaded ThreadPoolExecutor
关于地图GIS的一次实践整理(下) Redis的GIS实践
leetcode: 358. Reorder strings at K distance intervals
PIFuHD配置记录
CC0 vs. commercial IP: which model is better for NFTs?
互换性测量与技术——偏差与公差的计算,公差图的绘制,配合与公差等级的选择方法