程序员编程艺术1:左旋转字符串

程序员编程艺术系列(简称TAOPP系列),围绕“面试”、“算法”、“编程”三个主题,注重提高广大初学者的编程能力,以及如何运用编程技巧和高效的算法解决实际应用问题。感谢“ 研究者July“分享的博客,这也是我一个朋友给我推荐他的博客,阅读同时实践,为了加深自己的理解,学习后写个博客总结下还是很有必要。
第一章是字符串的左旋转操作:
题目描述:
定义字符串的左旋转操作:把字符串前面的若干个字符移动到字符串的尾部,如把字符串abcdef左旋转2位得到字符串cdefab。
请实现字符串左旋转的函数,要求对长度为n的字符串操作的时间复杂度为O(n),空间复杂度为O(1)。
看到这题目,我首先想了一下,好像不复杂,比如:n个字符要左移m位。
思路1:首先想到的一种处理方式是通过临时字符数组,保存左边的m个字符,然后其他的都已到左移m位,然后把临时数组的m写道最右边。这种算法时间复杂度为O(n+m)
void StringReverse::LeftShitChar2(string &str,int m)
{
	int length=str.length();
	if(length<=0 || length<=m)
	{
		return;
	}
	char temp[100];
	for(int i=0;i<m;i++)
	{
		temp[i]=str[i];
	}
	for(int j=0;j<(length-m);j++)
	{
		str[j]=str[j+m];
	}
	for(int k=length-m;k<length;k++)
	{
		str[k]=temp[k-length+m];
	}
}
然后看了下其他思路,我都自己写了下,接着下面就来。
思路2:比较简单的思路,要左移m位,可以没吃左移1位总共要移n-1个字符,循环m次就行了。
时间复杂度O(n*m);
void leftshiftone(char *s,int n) {    
    char t = s[0];   
    for (int i = 1; i < n; ++i) 
	{    
        s[i - 1] = s[i];    
    }    
    s[n - 1] = t;    
}  
void leftshift(char *s,int n,int m) {    
    while (m--) 
	{    
        leftshiftone(s, n);    
    }    
}     
思路3:指针翻转放(我理解跳跃式移位,类似希尔排序思想,在思路2基础上改进的,减少移位次数)。时间 复杂度是O(m+n)
引入原作者的解释:
咱们先来看个例子,如下:abc defghi,若要让abc移动至最后的过程可以是:abc defghi->def abcghi->def ghiabc
如此,我们可定义俩指针,p1指向ch[0],p2指向ch[m];
一下过程循环m次,交换p1和p2所指元素,然后p1++, p2++;。
- 第一步,交换abc 和def ,abc defghi->def abcghi
- 第二步,交换abc 和 ghi,def abcghi->def ghiabc
 整个过程,看起来,就是abc 一步一步 向后移动
- abc defghi
- def abcghi
- def ghi abc
//最后的 复杂度是O(m+n)
但如果是 abcdefghijk,通过上面移位后会有剩余 接下来又2种处理方式
方式1:移位后变成def ghi abc jk
当p1指向a,p2指向j时,由于p2+m越界,那么此时p1,p2不要变
这里p1之后(abcjk)就是尾巴,处理尾巴只需将j,k移到abc之前
方式2: def ghi abc jk

当p1指向a,p2指向j时,那么交换p1和p2,
此时为:
def ghi jbc ak

p1++,p2++,p1指向b,p2指向k,继续上面步骤得:
def ghi jkc ab

p1++,p2不动,p1指向c,p2指向b,p1和p2之间(cab)也就是尾巴,
那么处理尾巴(cab)需要循环左移一定次数

下面使用方式1实现代码:
void StringReverse::LeftShitChar(string &str,int m)
{
	int length=str.length();
	if(length<=0 || length<=m)
	{
		return;
	}
	int c=length/m;
	int p1=0,p2=m;
	for (int i=0;i<(c-1)*m;i++)
	{
		swap(str[p1],str[p2]);
		p1++;
		p2++;
	}
	
	int remain=length-p2;
	while(remain--)
	{
                int i=p2;
		while(i>p1)
		{
			swap(str[i],str[i-1]);
			i--;
		}
		p1++;
		p2++;
	}
}

思路3:使用递归,很经典的思路, 把一个规模为N的问题化解为规模为M(M<N)的问题。时间复杂度O(n)
    举例来说,设字符串总长度为L,左侧要旋转的部分长度为s1,那么当从左向右循环交换长度为s1的小段,直到最后,由于剩余的部分长度为s2(s2==L%s1)而不能直接交换。
    该问题可以递归转化成规模为s1+s2的,方向相反(从右向左)的同一个问题。随着递归的进行,左右反复回荡,直到某一次满足条件L%s1==0而交换结束。
     举例解释一下:
    设原始问题为:将“123abcdefg”左旋转为“abcdefg123”,即总长度为10,旋转部("123")长度为3的左旋转。按照思路二的运算,演变过程为“123abcdefg”->"abc123defg"->"abcdef123g"。这时,"123"无法和"g"作对调,该问题递归转化为:将“123g”右旋转为"g123",即总长度为4,旋转部("g")长度为1的右旋转。

举个具体事例说明,如下:
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 ShitChar(string &str,int n,int m,int head,int tail,bool flag)
{
	if(flag==true)
	{
		int k=n-m-n%m;
		int p1=head;
		int p2=head+m;
		for(int i=0;i<k;i++)
		{
			swap(str[p1+i],str[p2+i]);
			head++;
		}
		if(n%m>0)
		{
			ShitChar(str,n-k,n%m,head,tail,false);
		}
	}
	else
	{
		int k=n-m-n%m;
		int p1=tail;
		int p2=tail-m;
		for(int i=0;i<k;i++)
		{
			swap(str[p1-i],str[p2-i]);
			tail--;
		}
		if(n%m>0)
		{
			ShitChar(str,n-k,n%m,head,tail,true);
		}
	}
}

void StringReverse::LeftShitChar3(string &str,int m)
{
	int length=str.length();
	if(length<=0 || length<=m)
	{
		return;
	}
	
	int head=0;
	int tail=length-1;
	bool flag=true;
	ShitChar(str,length,m,head,tail,true);
}

思路4:循环移位(gcd),非常经典的方法,我看了好几遍才明白,时间复杂度(n)
我理解是将1个字符串分为几条循环链(>1 且 <=m),每条循环链2个节点之间的距离都是m,每条链每个字符都只需左移一位就好了。关键在于有多少条,经过数学推理,有gcd(n,m)条,即n,m的最大公约数条。
int gcd(int a,int b)
{
	if(a%b==0)
	{
		return b;
	}
	else 
	{
	    return gcd(b,a%b);
	}
}

void StringReverse::LeftShitChar4(string &str,int m)
{
	int lenOfStr = str.length();   
    int numOfGroup = gcd(lenOfStr, m);   
    int elemInSub = lenOfStr / numOfGroup;    
	
    for(int j = 0; j < numOfGroup; j++)         
    {   
        char tmp = str[j];   
		
        for (int i = 0; i < elemInSub - 1; i++)      
		{
            str[(j + i * m) % lenOfStr] = str[(j + (i + 1) * m) % lenOfStr];  
		}
        str[(j + i * m) % lenOfStr] = tmp;   
    }  
}

思路5:三步旋转法,也是非常经典的方法,时间复杂度(n)
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,即整个翻转。
void invest (string &str,int begin,int end)
{
	char temp;
	int  number=end-begin;
    while(begin<=end)
	{
		temp=str[begin];
		str[begin]=str[end];
		str[end]=temp;
		begin++;
		end--;
	}
}
void StringReverse::LeftShitChar5(string &str,int m)
{
	int length=str.length();
	invest(str,0,m-1);
	invest(str,m,length-1);
	invest(str,0,length-1);
}
总结比较时间复杂度:
1.循环链法=递归发=三步翻转法=O(n)
2.指针翻转法= 临时数组法= O(n+m)
3.循环移位法=O(n*m)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值