左旋转字符串

                   第一章、左旋转字符串


作者:July,yansha。
时间:二零一一年四月十四日。
微博:http://weibo.com/julyweibo
出处:http://blog.csdn.net/v_JULY_v
-------------------------------------------

目录

前言

第一节、左旋转字符串

 

前言
    本人整理微软等公司面试100题系列,包括原题整理,资源上传,帖子维护,答案整理,勘误,修正与优化工作,包括后续全新整理的80道,总计180道面试题,已有半年的时间了。

    关于这180道面试题的一切详情,请参见:横空出世,席卷Csdn [评微软等数据结构+算法面试180题]

    一直觉得,这180道题中的任何一题都值得自己反复思考,反复研究,不断修正,不断优化。之前的答案整理由于时间仓促,加之受最开始的认识局限,更兼水平有限,所以,这180道面试题的答案,有很多问题都值得进一步商榷与完善。

    特此,想针对这180道面试题,再写一个系列,叫做:程序员编程艺术系列。如你所见,我一般确定要写成一个系列的东西,一般都会永久写下去的。

    “他似风儿一般奔跑,很多人渐渐的停下来了,而只有他一直在飞,一直在飞....”
    
    ok,本次程序员编程艺术系列以之前本人最初整理的微软面试100题中的第26题、左旋转字符串,为开篇,希望就此问题进行彻底而深入的阐述。然以下所有任何代码仅仅只是全部测试正确了而已,还有很多的优化工作要做。欢迎任何人,不吝赐教。谢谢。

 

第一节、左旋转字符串
题目描述:

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

编程之美上有这样一个类似的问题,咱们先来看一下:

设计一个算法,把一个含有N个元素的数组循环右移K位,要求时间复杂度为O(N),
且只允许使用两个附加变量。

分析:

我们先试验简单的办法,可以每次将数组中的元素右移一位,循环K次。
abcd1234→4abcd123→34abcd12→234abcd1→1234abcd。
RightShift(int* arr, int N, int K)
{
     while(K--)
     {
          int t = arr[N - 1];
          for(int i = N - 1; i > 0; i --)
               arr[i] = arr[i - 1];
          arr[0] = t;
     }
}

虽然这个算法可以实现数组的循环右移,但是算法复杂度为O(K * N),不符合题目的要求,要继续探索。

假如数组为abcd1234,循环右移4位的话,我们希望到达的状态是1234abcd。
不妨设K是一个非负的整数,当K为负整数的时候,右移K位,相当于左移(-K)位。
左移和右移在本质上是一样的。

解法一:
大家开始可能会有这样的潜在假设,K<N。事实上,很多时候也的确是这样的。但严格来说,我们不能用这样的“惯性思维”来思考问题。
尤其在编程的时候,全面地考虑问题是很重要的,K可能是一个远大于N的整数,在这个时候,上面的解法是需要改进的。
仔细观察循环右移的特点,不难发现:每个元素右移N位后都会回到自己的位置上。因此,如果K > N,右移K-N之后的数组序列跟右移K位的结果是一样的。

进而可得出一条通用的规律:
右移K位之后的情形,跟右移K’= K % N位之后的情形一样,如代码清单2-34所示。
//代码清单2-34
RightShift(int* arr, int N, int K)
{
     K %= N;
     while(K--)
     {
          int t = arr[N - 1];
          for(int i = N - 1; i > 0; i --)
               arr[i] = arr[i - 1];
          arr[0] = t;
     }
}
可见,增加考虑循环右移的特点之后,算法复杂度降为O(N^2),这跟K无关,与题目的要求又接近了一步。但时间复杂度还不够低,接下来让我们继续挖掘循环右移前后,数组之间的关联。


解法二:
假设原数组序列为abcd1234,要求变换成的数组序列为1234abcd,即循环右移了4位。比较之后,不难看出,其中有两段的顺序是不变的:1234和abcd,可把这两段看成两个整体。右移K位的过程就是把数组的两部分交换一下。
变换的过程通过以下步骤完成:
 逆序排列abcd:abcd1234 → dcba1234;
 逆序排列1234:dcba1234 → dcba4321;
 全部逆序:dcba4321 → 1234abcd。
伪代码可以参考清单2-35。
//代码清单2-35
Reverse(int* arr, int b, int e)
{
     for(; b < e; b++, e--)
     {
          int temp = arr[e];
          arr[e] = arr[b];
          arr[b] = temp;
     }
}

RightShift(int* arr, int N, int k)
{
     K %= N;
     Reverse(arr, 0, N – K - 1);
     Reverse(arr, N - K, N - 1);
     Reverse(arr, 0, N - 1);
}

这样,我们就可以在线性时间内实现右移操作了。

稍微总结下:
编程之美上,
(限制书中思路的根本原因是,题目要求:“且只允许使用两个附加变量”,去掉这个限制,思路便可如泉喷涌)
1、第一个想法 ,是一个字符一个字符的右移,所以,复杂度为O(N*K)
2、后来,它改进了,通过这条规律:右移K位之后的情形,跟右移K’= K % N位之后的情形一样
复杂度为O(N^2)
3、直到最后,它才提出三次翻转的算法,得到线性复杂度。

下面,你将看到,本章里我们的做法是:
1、三次翻转,直接线性
2、两个指针逐步翻转,线性
3、stl的rotate算法,线性

好的,现在,回到咱们的左旋转字符串的问题中来,对于这个左旋转字符串的问题,咱们可以如下这样考虑:
1.1、思路一:

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

不是么?ok,就拿abcdef 这个例子来说(非常简短的三句,请细看,一看就懂):
1、首先分为俩部分,X:abc,Y:def;
2、X->X^T,abc->cba, Y->Y^T,def->fed。
3、(X^TY^T)^T=YX,cbafed->defabc,即整个翻转。

我想,这下,你应该了然了。
然后,代码可以这么写(已测试正确):

  1. //Copyright@ 小桥流水 && July  
  2. //c代码实现,已测试正确。  
  3. //http://www.smallbridge.co.cc/2011/03/13/100%E9%A2%98  
  4. //_21-%E5%B7%A6%E6%97%8B%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2.html  
  5. //July、updated,2011.04.17。  
  6. #include <stdio.h>  
  7. #include <string.h>  
  8.   
  9. char * invert(char *start, char *end)  
  10. {     
  11.     char tmp, *ptmp = start;      
  12.     while (start != NULL && end != NULL && start < end)    
  13.     {     
  14.         tmp = *start;     
  15.         *start = *end;        
  16.         *end = tmp;       
  17.         start ++;     
  18.         end --;   
  19.     }  
  20.     return ptmp;  
  21. }  
  22.   
  23. char *left(char *s, int pos)   //pos为要旋转的字符个数,或长度,下面主函数测试中,pos=3。  
  24. {  
  25.     int len = strlen(s);  
  26.     invert(s, s + (pos - 1));  //如上,X->X^T,即 abc->cba  
  27.     invert(s + pos, s + (len - 1)); //如上,Y->Y^T,即 def->fed  
  28.     invert(s, s + (len - 1));  //如上,整个翻转,(X^TY^T)^T=YX,即 cbafed->defabc。  
  29.     return s;  
  30. }  
  31.   
  32. int main()  
  33. {     
  34.     char s[] = "abcdefghij";      
  35.     puts(left(s, 3));  
  36.     return 0;  
  37. }  

1.2、答案V0.3版中,第26题勘误:

    之前的答案V0.3版[第21-40题答案]中,第26题、贴的答案有误,那段代码的问题,最早是被网友Sorehead给指出来的:

第二十六题:
楼主的思路确实很巧妙,我真没想到还有这种方法,学习了。
不过楼主代码中存在问题,主要是条件判断部分:
函数LeftRotateString中 if (nLength > 0 || n == 0 || n > nLength)
函数ReverseString中 if (pStart == NULL || pEnd == NULL)

    当时,以答案整理因时间仓促,及最开始考虑问题不够周全为由,没有深入细看下去。后来,朋友达摩流浪者再次指出了上述代码的问题:

  26题 这句 if(nLength > 0 || n == 0 || n > nLength),有问题吧?
  还有一句,应该是if(!(pStart == NULL || pEnd == NULL)),吧。

    而后,修改如下(已测试正确)

  1. //zhedahht  
  2. //July、k,updated  
  3. //copyright @2011.04.14,by July。  
  4. //引用,请注明原作者,出处。  
  5. #include <string.h>  
  6. #include <iostream>  
  7. using namespace std;  
  8.   
  9. void Swap(char* a,char* b)  //特此把交换函数,独立抽取出来。当然,不排除会有人认为,此为多此一举。  
  10. {  
  11.     char temp =*a;  
  12.     *a = *b;  
  13.     *b = temp;  
  14. }  
  15.   
  16. // Reverse the string between pStart and pEnd  
  17. void ReverseString(char* pStart, char* pEnd)  
  18. {  
  19.     if(*pStart != '/0' && *pEnd != '/0')     
  20.         //这句也可以是:if(pStart != NULL && pEnd != NULL)。  
  21.     {  
  22.         while(pStart <= pEnd)  
  23.         {  
  24.             Swap(pStart,pEnd);   //交换  
  25.   
  26.             pStart++;  
  27.             pEnd--;  
  28.         }  
  29.     }  
  30. }  
  31.   
  32. // Move the first n chars in a string to its end   
  33. char* LeftRotateString(char* pStr, unsigned int n)  
  34. {  
  35.     if(pStr != NULL)  
  36.     {  
  37.         int nLength = static_cast<int>(strlen(pStr));  
  38.         if(nLength >0 && n != 0 && n<nLength)   //n可以=0,也可以说不该=0。  
  39.             //nLength是整个字符串的长度,n是左边的一部分,所以应该是n<nLength。  
  40.             //之前上传的答案(代码),就错在这里,最初的为n>nLength,当然,就是错的了。July、k,updated。  
  41.         {  
  42.             char* pFirstStart = pStr;  
  43.             char* pFirstEnd = pStr + n - 1;  
  44.             char* pSecondStart = pStr + n;  
  45.             char* pSecondEnd = pStr + nLength - 1;  
  46.               
  47.             // reverse the first part of the string  
  48.             ReverseString(pFirstStart, pFirstEnd);  
  49.             // reverse the second part of the strint  
  50.             ReverseString(pSecondStart, pSecondEnd);  
  51.             // reverse the whole string  
  52.             ReverseString(pFirstStart, pSecondEnd);  
  53.         }  
  54.     }  
  55.     return pStr;  
  56. }  
  57.   
  58. int main()  
  59. {  
  60.     char a[11]="hello July";   //2、修正,以一个数组实现存储整个字符串  
  61.     char *ps=a;  
  62.     LeftRotateString(ps, 6);  
  63.     for(;*ps!='/0';ps++)  
  64.         cout<<*ps;  
  65.     cout<<endl;  
  66.     ps=NULL;   //代码规范  
  67.     return 0;  
  68. }  
上述,修正的俩处错误,如下所示:
1、如上注释中所述:        
if(nLength >0 && n<nLength)  
//nLength是整个字符串的长度吧,n是左边的一部分,所以应该是n<nLength。

2、至于之前的主函数为什么编写错误,请看下面的注释:
int main()
{
    char *ps="hello July";  //本身没错,但你不能对ps所指的字符串做任何修改。
    //这句其实等价于:const char *ps = "hello July"
    LeftShiftString( ps, 4 );  //而在这里,试图修改ps所指的字符串常量,所以将出现错误。
    puts( ps );
    return 0;
}

当然,上面的解释也不是完全正确的,正如ivan所说:从编程实践来说,不完全对。
如果在一个大的工程里面,你怎么知道ps指向的是""字符串,还是malloc出来的东西?  
那么如何决定要不要对ps进行delete?

不过,至少第26题的思路一的代码,最终完整修正完全了。

1.3、updated:
    可能你还是感觉上述代码,有点不好理解,ok,下面再给出一段c实现的代码
然后,我们可以看到c的高效与简洁。

  1. //copyright@ yiyibupt&&July  
  2. //已测试正确,July、updated,2011.04.17.  
  3. //不要小看每一段程序,July。  
  4. #include <cstdio>  
  5. #include <cstring>  
  6.   
  7. void rotate(char *start, char *end)  
  8. {  
  9.     while(start != NULL && end !=NULL && start<end)  
  10.     {  
  11.         char temp=*start;  
  12.         *start=*end;  
  13.         *end=temp;  
  14.         start++;  
  15.         end--;  
  16.     }  
  17.       
  18. }  
  19.   
  20. void leftrotate(char *p,int m)  
  21. {  
  22.     if(p==NULL)  
  23.         return ;  
  24.     int len=strlen(p);  
  25.     if(m>0&&m<=len)  
  26.     {  
  27.         char *xfirst,*xend;  
  28.         char *yfirst,*yend;  
  29.         xfirst=p;  
  30.         xend=p+m-1;  
  31.         yfirst=p+m;  
  32.         yend=p+len-1;  
  33.         rotate(xfirst,xend);  
  34.         rotate(yfirst,yend);  
  35.         rotate(p,p+len-1);  
  36.     }  
  37. }  
  38.   
  39. int main(void)  
  40. {     
  41.     char str[]="abcdefghij";  
  42.     leftrotate(str,3);  
  43.     printf("%s/n",str);  
  44.     return 0;  
  45. }  

#include<iostream>
using namespace std;
void left_change0(char* str,int n ,int k)
{
	while(k--)
	{
		char tmp=str[n-1];
		for (int i=n-1;i>0;--i)
		{
			str[i]=str[i-1];
		}
		str[0]=tmp;
	}
	
}//时间复杂度是O(n*k)

void left_change1(char *str,int n,int k)
{
	int t=k%n;
	while (t--)
	{
		char tmp=str[n-1];
		for (int j=n-1;j>0;--j)
		{
			str[j]=str[j-1];
		}
		str[0]=tmp;
	}
}//当k是很大的时候的话,k》n,那么的话就是可以循环调用,k%n的调用和k调用一样的考虑。O(n^2)

void left_change2_sbu(char *str ,int s,int e)
{
	for (;s<e;s++,e--)
	{
		char tmp=str[e];
		str[e]=str[s];
		str[s]=tmp;
	}
}//给出一个时间复杂度比较高的情况,就是如下o(n)

void left_change2(char *str,int n,int k)
{
	left_change2_sbu(str,0,k-1);
	left_change2_sbu(str,k,n-1);
	left_change2_sbu(str,0,n-1);
}



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值