本文以C语言实现。
本文以力扣轮转整型数组问题为例,详解解题思路(主要是“三步转换法”,其他方法也会简单说明)。最后附有一道同题型拓展:倒置字符串,该拓展题也会附上详解。希望本文对诸位读者有所帮助。
目录
给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
189. 轮转数组https://leetcode.cn/problems/rotate-array/
根据对“数组轮转”这一操作的不同理解,本题可以分为两种宏观思路。我们一种一种介绍。
从示例中我们不难发现,本题“向右轮转”的每次操作都相同:将数组中最右(最后)一个元素取出,换成数组的第一个元素;而其它元素整体顺序不变。即可将“轮转k次”拆分为:
数组每次向右轮转1次,一共轮转了k次。
从这个角度出发,我们可将整个轮转的大过程看作是一个大循环,我们只需要搞清楚进行一次轮转做了哪些事,再循环k次,就能获得最终的结果。
我们考虑直接在原数组上进行操作,对每一次“轮转”的过程进行拆解。
用nums表示数组,用numsSize表示数组的长度。
每轮转转1次时,可归纳出如下步骤:
这种思路最直接,也最容易想到。基于以上思路,我们可以给出如下接口函数:
- 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桶水只需要搬运一趟。这就是额外数组法。
具体操作如下:
依次思路,可以给出如下代码:
- 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];
- }
- }
注意
这就是我们今天要重点介绍的“三步转换法”。我们单独开一个目录板块来聊聊这个。
要达到数组轮转,有一个很厉害的思路:三步转换法。我们直接进行介绍。
前面提到,这是一种从轮转的结果进行切入的思路:
这就是所谓的“整体逆序,内部有序”。当把①和②分别看作两块整体元素时,①和②相对于原数组而言是逆序的。而当①和②分别看作一个单独的数组时,它们的内部又是有序的。注意,这里说的“有序”“逆序”是和原数组相比,顺序是否发生变化(不是说升序降序的那个排序)。
要达到向右轮转k次使数组“整体逆序,内部有序”的状态,只须三步:
先将后k个逆置,再将前(n-k)个逆置,最后整体逆置。
图解如下:
同样,若以同样的规则向左轮转,依旧是三步:先将前k个逆置,再将后(n-k)个逆置,最后整体逆置。
以此我们可以进一步归纳总结,得出三步转换法结论:
若有轮转方向规定:向方向D轮转,就先将D方向的那k个逆置,再将剩下的(n-k)个元素逆置,最后整体逆置即可。(其实在数组整体逆置之前,先逆置内部哪部分是并不影响结果的,只要保证每个部分的内部都逆置过了即可。只是个人认为按方向分一分会比较好理解,不容易搞错。)
若无轮转方向规定(只要达到逆置的结果即可):就不用考虑“先向哪个方向逆置”的问题(不用考虑方向),只需要挨个将所有块内部的元素逆置,最后整体逆置即可。逆置的过程可以封装成函数,遇到时调用即可。
言而总之,就一句话:三步逆置,将数组分块,块内元素先分别逆置,最后再整体逆置。
- //将逆置操作封装为函数
- 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(* ̄▽ ̄*)ブ
这道题是上面三步转换法非常合适的一个变式。
倒置字符串https://www.nowcoder.com/questionTerminal/8869d99cf1264e60a6d9eff4295e5bab
将一句话的单词进行倒置,标点不倒置。
比如 "I like beijing.",经过处理后变为:"beijing. like I"
(字符串长度不超过100)
这也是“整体逆序,内部有序”的典例:我们将字符串内容按单词分块,单词在整个句子中的顺序发生变化,但单词还是单词,它内部的字母顺序并没有发生变化。
当我们明白这一点,接下来的事情也许会好办许多。
这里我们采用指针+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;
- }