每天学一点系列~字符串左/右旋的本质,你真的认清了嘛?

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白龙码
在这里插入图片描述

以上是关于每天学一点系列~字符串左/右旋的本质,你真的认清了嘛?的主要内容,如果未能解决你的问题,请参考以下文章

每天学一点系列~一文带你彻底弄懂结构体大小和内存对齐

红黑树理解右旋

每天学一点系列~看得见摸不着的“隐式类型转换”

avl树左旋右旋的理解

每天学一点系列~诡异的死循环

每天学一点系列~“Hello World“的诞生