每天学一点系列~字符串左/右旋的本质,你真的认清了嘛?
Posted 白龙码~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了每天学一点系列~字符串左/右旋的本质,你真的认清了嘛?相关的知识,希望对你有一定的参考价值。
学透字符串的旋转
Part I、前言
每天学一点,进步多一点!
今天的主题:string rotation 字符串的左/右旋
Part II、左/右旋
1、定义
其实用文字比较难描述出来,我们用例子能更加简单的理解:
给出字符串:ABCDE
左旋一次:BCDEA
左旋两次:CDEAB
左旋三次:DEABC
左旋四次:EABCD
左旋五次:ABCDE
我们注意到,左旋K次,就是把前K个字符整体挪动到字符串的右边,注意,是整体挪动,这意味着挪动的部分,它的顺序与原先相同。
我们还注意到,经过五次左旋,字符串回到了初始的样子。(这句不是废话哦)
同样是字符串:ABCDE
右旋一次:EABCD
右旋两次:DEABC
右旋三次:CDEAB
右旋四次:BCDEA
右旋五次:ABCDE
与左旋类似,我们将后K个字符整体挪动到字符的左边,顺序不变。
我们同样注意到,经过五次右旋,字符串回到了初始的样子。
2、共同特点
特点一:无论是左旋K次还是右旋K次,无非就是将左边的或者右边的K个连续字符,整体的进行位置的挪动,概括下来就是:左旋就是左边整体右挪,右旋就是右边整体左挪,且顺序不变;
特点二:当旋转的次数K达到某一特定值时,字符串维持原样,相当于没有进行旋转,而且我们不难发现,也不难论证出:这个特定值,就是字符串的长度(将整个字符串整体挪动,自然是维持原样)
Part III、初阶解法
由于左右旋具有许多共同特性,考虑到篇幅问题,我们主要讨论左旋的情况,右旋的话,简单改变一下代码中的某些控制量就OK啦~
解法一:创建新数组
我们根据上面的特点1,将后len-K%len个存到一个数组ret中,然后将前K个存到数组ret的剩余位置。
K%len是什么意思? 【K:旋转次数 len:字符串长度(不包括空字符’\\0’)】
根据我们先前的特点二:“当旋转的次数K达到字符串长度len时,字符串维持原样,相当于没有进行旋转”
所以当我们让len÷K,得到的结果的整数部分,意味着我们要对这个字符串整体挪动这整数部分次,这并没有对字符串造成实质性的影响
而余数部分,才是我们需要考虑的真正有用的那几次旋转——也就是K%len次;
理解了做法后,我们代码实现一波:
char* left_rotation(char arr[], int len, int K)
{
char* ret = (char*)malloc((len + 1) * sizeof(char));
if (ret)//malloc成功
{
int i;
for (i = 0; i < len - K % len; i++)//将后len-K%len个存到一个数组ret中
{
ret[i] = arr[i + K % len];
}
for (i = 0; i < K % len; i++)//将前K个存到数组ret的剩余位置
{
ret[i + len - K % len] = arr[i];
}
ret[len] = '\\0';
return ret;
}
else//malloc失败,返回空指针
{
return NULL;
}
}
注意:调用该函数后需要将返回的指针free掉,避免内存泄漏!
解法二:原地算法(直接法)
原地算法,顾名思义,就是通过覆盖的方式,在arr的内部直接进行旋转操作,同样,我们拿左旋举例——
我们将arr的第一个字符取出,然后拿后方的数据覆盖前面的数据,最后再把取出的字符接到数组的末尾,重复该操作K%len次。
【注:这里的K%len参考上面的解释哦~】
代码实现:
void left_rotation(char arr[], int len, int K)
{
int k=K%len;//执行次数
while (k--)
{
int i, tmp = arr[0];//取出数组的第一个数
for (i = 0; i < len-1; i++)
{
arr[i] = arr[i + 1];//后方数据覆盖前方数据
}
arr[len - 1] = tmp;//将取出的数放到最后一个
}
}
Part IV、进阶解法
1、 三步反转法
我们刚刚说过,左旋的本质实际上就是:将前面的K%len个整体挪动到后方,将后面的挪到前方,也就是交换前后两部分的位置,并且要求它的顺序与原先一致;
借鉴大神的一种解释方式:
我们原先的数组是这样的:
我们将其分为两个部分:
我们想要的结果是这样:
而想要做到这样,我们只需要反转S1和S2:
然后整体反转:
surprise!目的达成。
这样的做法有一定的数学依据(经评论区老铁建议后修改)——
学过线性代数的知道:当我们假定数组为AB,A代表S1,B代表S2,想要得到BA
只需要:(AT·BT)T,
也就是说,对A和B分别求转置矩阵后,在整体求转置矩阵!与这里三次反转如出一辙!
代码实现
void Reverse(char arr[], int left, int right)
{
while (left < right)
{
char tmp = arr[left];
arr[left] = arr[right];
arr[right] = tmp;
left++;
right--;
}
}
void left_rotation(char arr[], int len, int K)
{
Reverse(arr, 0, K % len-1);
Reverse(arr, K % len, len-1);
Reverse(arr, 0, len-1);
}
2、找子串法
这个方法同上面的方法一样,很难想到,但是也很好理解!
本解法灵感来自这道题的一道改编题:给定一个字符串,判断其是否能通过另一个字符串的旋转得到。
比如:CDEFAB可以通过字符串ABCDEF左旋两个字符得到。
对于这道题的做法,我们采取接长字符串ABCDEF的方法,也就是说,我们在ABCDEF的后面再接上一个ABCDEF,让其变成:ABCDEFABCDEF,此时,我们会惊奇的发现,由于字符串旋转的性质一,ABCDEF这个字符串所有可能的旋转结果都是这个接长后的字符串的子串!
如果将这种做法放到这道题上,我们不难看出:
假设字符串长度为len,左旋次数为K,那么这个接长字符串中下标为K%len的就是我们所需要的那个旋转后的字符串的首地址!
比如:ABCDEF左旋两次,就是ABCDEFABCDEF中下标为2的,就是我们需要返回的字符串的首地址。
代码实现:
char* left_rotation(char arr[], int len, int K)
{
strncat(arr,arr,len);//strncat函数可以在数组后追加自己
arr[K%len + len] = '\\0';
return arr + K%len;//返回下标为K的那个地址
}
这个函数的使用需要确保arr有足够的空间来追加自己!
Part V、写在最后
一道简单的C语言题目,从第一反应的初阶解法再到进阶解法,真的不得不感叹:想出这种解法的人有多聪明!反正,笔者是真的相当佩服!
不知道这篇文章对你是否有所帮助呢?你又是否对字符串的旋转有什么新的见解?欢迎评论区赐教!
这是《每天学一点系列~》的第一篇,后续还会有更新,喜欢的话,欢迎给个三连哦!
by白龙码
以上是关于每天学一点系列~字符串左/右旋的本质,你真的认清了嘛?的主要内容,如果未能解决你的问题,请参考以下文章