旋转字符串

此文章参考《程序员编程艺术》第一章

题目描述:
定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾
部,如把字符串abcdef 左旋转2 位得到字符串cdefab。请实现字符串左旋转的
函数,要求对长度为n 的字符串操作的时间复杂度为O(n),空间复杂度为O(1)。


思路1:暴力翻转法

初看此题,咱们最先想到的笨方法可能就是一位一位移动,故咱们写一个函
数叫做leftshiftone(char *s,int n) 完成左移动一位的功能

void reverse_one(char *p)
{
	int i = 0;
	int n = strlen(p)-1;
	char temp = p[0];
	for(i=0; i<n; i++)	//从前往后覆盖。
	{
		p[i] = p[i+1];
	}
	p[n] = temp;
}
如果左移m位,则执行上面的函数m次

void reverse(char *p,int m)
{
	while(n--)
	{
		reverse_one(p);
	}
}

思路二:指针翻转法

咱们先来看个例子,如下:abc defghi,若要让abc 移动至最后的过程可以
是:abc defghi->def abcghi->def ghiabc

如此,我们可定义俩指针,p1 指向ch[0],p2 指向ch[m];
一下过程循环m 次,交换p1 和p2 所指元素,然后p1++, p2++;。

图示如下



这是刚好9个元素 是m(左移数)的整数倍,但是如果是10个元素,就多了一个尾巴,则尾巴就要进行额外的操作。

例如 abcdefghij,

当p2+m越界时,abc不动,将j前移m个单元。


代码如下

void swap(char *a, char *b)	//交换。。
{
	char temp = *a;
	*a = *b;
	*b = temp;
}
void reverse(char *p,int m)
{
	int n = strlen(p);
	int k = 0;
	int r = 0;
	int p1 = 0;
	int p2 = 0;
	assert(p);
	if(m%n <= 0)	//如果翻转的个数为负数或等于总个数,直接返回
		return;
	p1 = 0;
	p2 = m;
	k = (n-m)-(n%m);
	/*以p2 为移动的参照系:
	n-m 是开始时p2 到末尾的长度,n%m 是尾巴长度
	(n-m)-n%m 就是p2 移动的距离
	比如abc def efg hi
	开始时p2->d,那么n-m 为def efg hi 的长度8,
	n%m 为尾巴hi 的长度2,
	因为我知道abc 要移动到hi 的前面,所以移动长度是
	(n-m)-n%m = 8-2 = 6。
	*/
	while(k--)
	{
		swap(p+p1,p+p2);
		p1++;
		p2++;
	}
	r = n - p2;	//r为尾巴
	while(r--)	//剩下的尾巴逐个交换
	{
		int i = p2;
		while(p1<i)
		{
			swap(p+i,p+i-1);
			i--;
		}
		p1++;
		p2++;
	}
	
}

思路3:递归翻转法

对于后面的尾巴,还可以用递归的方法解决

1、对于字符串abc def ghi gk,
将abc 右移到def ghi gk 后面,此时n = 11,m = 3,
m’ = n % m = 2;
abc def ghi gk -> def ghi abc gk
2、问题变成gk 左移到abc 前面,此时n = m’ + m = 5,m = 2,
m’ = n % m 1;
abc gk -> a gk bc

3、问题变成a 右移到gk 后面,此时n = m’ + m = 3,m = 1,
m’ = n % m = 0;
a gk bc-> gk a bc。由于此刻,n % m = 0,满足结束条件,
返回结果。
即从左至右,后从右至左,再从左至右,如此反反复复,直到满足条件,返
回退出。

代码如下:

void reverse(char *p,int n, int m, int head, int tail, int flag)
{
	assert(p);
	if(m%n == 0)	//如果m == 0则直接返回
		return;
	if(flag==1)
	{
		int p1 = head;
		int p2 = head+m;
		int i = 0;
		int k = (n-m)-(n%m);
		for(i=0; i<k; i++,p1++,p2++)
			swap(p+p1,p+p2);
		reverse(p, n-k, n%m, p1, tail,0);//进入右旋 
	}
	else
	{
		int p1 = tail-m;
		int p2 = tail;
		int k = (n-m)-(n%m);
		int i = 0;
		for(i=0; i<k; i++,p1--,p2--)
			swap(p+p1,p+p2);
		reverse(p, n-k, n%m, head, p2, 1);
		//再次进入上面的左旋部分,
		//3、左旋:问题变成a 右移到gk 后面,此时n = m’ + m = 3,m = 1,m’ = n % m = 0;
		//a gk bc-> gk a bc。由于此刻,n % m = 0,满足结束条件,返回结果。
	}

}

思路4:循环移位法

下面代码用到了辗转相除法(欧几里得算法.gcd算法),所以先简单介绍一下

辗转相除:

1、[求余数],令r=m%n,r 为n 除m 所得余数(0<=r<n);
2、[余数为0?],若r=0,算法结束,此刻,n 即为所求答案,否则,继续,
转到3;
3、[重置],置m<-n,n<-r,返回步骤1.

例:

int gcd(n,m)
{
	while(m%n != 0)
	{
		int ret = m%n;
		m = n;
		n = ret;
	}
	return n;
}

好,下面介绍循环移位法

所有序号为(j+i *m) % n (j 表示每个循环链起始位置,i 为计数变
量,m 表示左旋转位数,n 表示字符串长度),会构成一个循环链(共有gcd(n,m)
个,gcd 为n、m 的最大公约数),每个循环链上的元素只要移动一个位置即
可,最后整个过程总共交换了n 次(每一次循环链,是交换n/gcd(n,m)次,
总共gcd(n,m)个循环链。所以,总共交换n 次)。
stl 的rotate 的三种迭代器,即是,分别采用了后三种方法。
在给出stl rotate 的源码之前,先来看下我的朋友ys 对上述第4 种方法的
评论:
ys:这条思路个人认为绝妙,也正好说明了数学对算法的重要影响。
通过前面思路的阐述,我们知道对于循环移位,最重要的是指针所指单元不
能重复。例如要使abcd 循环移位变成dabc(这里m=3,n=4),经过以下一系列眼
花缭乱的赋值过程就可以实现:ch[0]->temp, ch[3]->ch[0], ch[2]->ch[3],

ch[1]->ch[2], temp->ch[1]; 
字符串变化为:abcd->_bcd->dbc_->db_c->d_bc->dabc;
是不是很神奇?其实这是有规律可循的。
请先看下面的说明再回过头来看。
对于左旋转字符串,我们知道每个单元都需要且只需要赋值一次,什么样的
序列能保证每个单元都只赋值一次呢?
1、对于正整数m、n 互为质数的情况,通过以下过程得到序列的满足上面的
要求:
for i = 0; i<n-1;i++

k = i * m % n;
end
举个例子来说明一下,例如对于m=3,n=4 的情况,
1、我们得到的序列:即通过上述式子求出来的k 序列,是0, 3, 2, 1。
2、然后,你只要只需按这个顺序赋值一遍就达到左旋3 的目的了:
ch[0]->temp, ch[3]->ch[0], ch[2]->ch[3], ch[1]->ch[2],
temp->ch[1]; (*)
ok,这是不是就是按上面(*)式子的顺序所依次赋值的序列阿?哈哈,很巧
妙吧。当然,以上只是特例,作为一个循环链,相当于rotate 算法的一次内循

环。
2、对于正整数m、n 不是互为质数的情况(因为不可能所有的m,n 都是互
质整数对),那么我们把它分成一个个互不影响的循环链,正如flyinghearts
所言,所有序号为(j + i * m) % n(j 为0 到gcd(n, m)-1 之间的某一整数,
i = 0:n-1)会构成一个循环链,一共有gcd(n, m)个循环链,对每个循环链分
别进行一次内循环就行了。
综合上述两种情况,可简单编写代码如下:

void reverse(char *str, int m)
{
// 所有序号为(j+i *m) % n (j 表示每个循环链起始位置,i 为计数变量,m 表示左旋转位数,n
//表示字符串长度),
//会构成一个循环链(共有gcd(n,m)个,gcd 为n、m 的最大公约数),
//每个循环链上的元素只要移动一个位置即可,最后整个过程总共交换了n 次
//(每一次循环链,是交换n/gcd(n,m)次,共有gcd(n,m)个循环链,所以,总共交换n 次。
	int lenOfStr = strlen(str);
	int numOfGroup = gcd(lenOfStr, m);	//算出循环链数
	int elemInSub = lenOfStr / numOfGroup;	//每个循环链上的元素个数
	int j = 0;
	int i = 0;
	for(j = 0; j < numOfGroup; j++)
	{
		char tmp = str[j];
		for (i = 0; i<elemInSub - 1; i++)
			str[(j + i * m) % lenOfStr] = str[(j + (i + 1) * m) % lenOfStr];
		str[(j + i * m) % lenOfStr] = tmp;
	}
}


思路5:三步翻转法

将一个字符串分成两部分,X 和Y 两个部分,在字符串上定义反转的操作X^T,
即把X 的所有字符反转(如,X="abc",那么X^T="cba"),那么我们可以得到
下面的结论:(X^TY^T)^T=YX。显然我们这就可以转化为字符串的反转的问题了。



就拿abcdef 这个例子来说,若要让def 翻转到abc 的前头,那
么只要按下述3 个步骤操作即可:
1、首先分为俩部分,X:abc,Y:def;
2、X->X^T,abc->cba, Y->Y^T,def->fed。
3、(X^TY^T)^T=YX,cbafed->defabc,即整个翻转。

代码如下

char * invert(char *start, char *end)	//翻转
{
	char tmp, *ptmp = start;
	while (start != NULL && end != NULL && start < end)
	{
		tmp = *start;
		*start = *end;
		*end = tmp;
		start ++;
		end --;
	}
	return ptmp;
}
void reverse(char *str, int pos)	//pos为翻转个数
{
	int len = strlen(str);
	//三步翻转
	invert(str,str+(pos-1));
	invert(str+pos,str+len-1);
	invert(str,str+len-1);
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值