您的位置:首页 > 其它

反转字符串中的单词

2012-03-08 21:41 239 查看
一:

反转字符串中的单词(Reverse Words)2008年05月25日 星期日 17:40前言:在前面一篇文章中,我们在反转长度不等的两块连续内存块的算法的基础上推导出了反转长度不等的两块不连续内存块的算法。在本文中,我将使用前文分析的算法去解决一个更复杂的问题:反转字符串中的单词。

引子

话说有这样一道算法题:

PROBLEM:Reverse Words

COMMENT:Write a function that reverses the order of the words in a string.For instance,your function should transform the string "Do or do not,there is no try." to "try.no is there not,do or Do". Assume that all words are space delimited and treat punctuation
the same as letters.

这个问题常见国内外各大IT论坛中,而且被多家著名的IT公司频繁拿来用来作为面试题去考察面试者的基本功。从题目涉及到的内容来看,这个题并不是很难,但是如果对时间复杂度和空间复杂度进行限制的话,在短时间内设计出一个让面试官满意的算法也并不是一件简单的事情。接下来,我首先简单介绍一下最常见的算法,这个算法思路比较直接,但是在时间复杂度和空间复杂度上很难满足条件。然后我就"隆重"介绍我所采用的算法,这个算法是前篇文章介绍的算法的一个延伸,并采用一个重要的解决问题的技巧-"Divide-and-Conquer"。

分析篇

当我们初次接触这个题的时候,我相信我们的脑海了已经浮现出一个完整的解决方案:

1。开辟一段内存用来存放反转后的结果,这段内存的大小应该不小于源字符串的大小。

2。由于我们要反转字符串,我们很自然的就会想到从字符串的尾部向字符串的头部遍历。

3。在遍历的过程中,当我们找到一个单词的时候,就把这个单词拷贝到目标内存块中。

4。当我们遍历了字符串中所有的字符时,遍历过程中。由于是从后向前遍历,这时并没有什么标识符标识结束,我们只有先记录下字符串的长度,当剩下的字符串长度为0时,遍历结束。

这个解决方案虽然简单,但是在实现的时候却总能碰到一些困难。由于我们是从后向前遍历,我们按顺序获得的单词中的字符是反序的,但是当我们把这个单词拷贝到目标内存块时却要正序拷贝。我们同时还要记录要拷贝单词的首地址和末地址,还要提防"Off-By-One"的错误。

除了实现起来会遇到点麻烦外,这个算法最大的问题就是在于目标内存块的开辟,导致在空间复杂度上不够理想。由于我们事先不可能知道源字符串的长度,我们更不能假定源字符串的最大长度(还记得"缓冲区溢出"么?),所以我们至少要开辟一段和源字符串一样长度的内存块,这样算法的空间复杂度就达到O(n)。同时这个算法首先要遍历字符串来获得字符串的长度,然后还要对字符串再进行一次遍历来获得字符串中的单词,这样算法的时间复杂度就达到O(n)。

是不是还存在优化的空间呢?答案当然时肯定的。如果我们跳出我们熟悉的思路框架,站的更高一点,从更高一级的抽象去分析这个问题,我们获得了一个完全不同于传统算法的解决方案。

从整体来说,我们要处理的是由多个单词(单词之间用空格分割)构成的字符串,这个字符串中基本的元素就是单词和空格。我首先拿几个简单的特例来分析一下,看看有没有什么收获。

1> 字符串中没有空格。这样字符串中的所有元素可以被看成是一个单词。这时候,就没有既要进行任何操作。

2> 字符串中有一个空格(此时暂时不考虑空格出现在字符串最前面或者最后面的情况)。这是反转两个单词的操作就等价于反转长度不等的两个不连续内存块的操作,这个操作我们已经在前篇文章中实现了。

3> 字符串中有两个空格。例如这样的字符串"Tom Jerry Mike",我们此时该如何处理这个字符串呢?结合前两个特例的分析,我立即想到了一种最常见的解决问题的思路-"Divide-and-Conquer"。总体来说,我们可以将包含多于一个空格的字符串划分成三个部分: |位于空格左边的子字符串|空格|位于空格右边的子字符串|

这样我们就要完成下面的处理:

step1。对位于空格左边的子字符串进行"Reverse Words"操作;

step2。对位于空格右边的子字符串进行"Reverse Words"操作;

step3。对整个字符串进行分转处理。

如果左右字符串中还包含多于一个空格,我们可以分别对他们再进行这样的分割+反转的操作,直到字符串中空格的数量不大于1。

看到这里,你是不是联想到一个常见的算法和这个算法有惊人的相似?对,就是"Merge Sort"算法,它们之间唯一的区别就在于"Merge Sort"中是进行排序操作,而这里需要进行反转操作。借用"Merge Sort"算法的人气,我把本文的算法称为"Merge Reverse"算法。

这里还剩下一个问题,对于字符串中的多个空格,我们该选用那个空格做分割点?如果你联想到了"Binary Search",答案就霍然明朗了,我们可以选用处于中间位置(median)的空格作为分割点。

实现篇

有了上面的分析,再结合前篇文章中实现的算法,本文的算法实现起来就显得十分简单了: void* reverseStringByWord(void* pMemory,size_t memTotalSize)

...{

if (NULL == pMemory) return pMemory;

if (memTotalSize < 2) return pMemory;

unsigned char* pByteMemory = reinterpret_cast<unsigned char*>(pMemory);

int iTotalSeparator = 0;

size_t* pSeparatorIndexArray = reinterpret_cast<size_t*>(malloc(sizeof(size_t)*memTotalSize));

if (NULL == pSeparatorIndexArray) return pMemory;

for (size_t i = 0; i < memTotalSize; i++) ...{

if (*(pByteMemory+i) == SPACESEPARATOR) ...{

*(pSeparatorIndexArray+iTotalSeparator) = i;

iTotalSeparator++;

}

}

if (iTotalSeparator == 0) ...{

//do nothing

}else if (iTotalSeparator == 1) ...{

size_t iMiddleSeparatorIndex = pSeparatorIndexArray[0];

size_t iHeadBlockSize = iMiddleSeparatorIndex;

size_t iEndBlockSize = memTotalSize-iHeadBlockSize-SEPARATORLENGH;

swapNonadjacentMemory(pByteMemory,memTotalSize,iHeadBlockSize,iEndBlockSize);

}else ...{

size_t iMiddleSeparatorIndex = pSeparatorIndexArray[iTotalSeparator/2];

size_t iHeadBlockSize = iMiddleSeparatorIndex;

size_t iEndBlockSize = memTotalSize-iHeadBlockSize-SEPARATORLENGH;

reverseStringByWord(pByteMemory,iHeadBlockSize);

reverseStringByWord(pByteMemory+iHeadBlockSize+1,iEndBlockSize);

swapNonadjacentMemory(pByteMemory,memTotalSize,iHeadBlockSize,iEndBlockSize);

}

free(pSeparatorIndexArray);

return pMemory;

}

测试篇

我写了一段测试用例来测试本文实现的算法: void Test_reverseStringByWord() ...{

//table-driven test case

static const char* testString[] = ...{

"",

"ab",

" a",

"a ",

"a b",

"ab cd ef",

"ab cd ef ",

" ab cd ed",

"aaa bbb ccc"

};

void* pMemory = malloc(MAXMEMBUFFERSIZE);

if (NULL == pMemory) return;

for (int i = 0; i < sizeof(testString)/sizeof(const char*); i++) ...{

printf("|%s|==>",testString[i]);

size_t iStringLength = strlen(testString[i]);

memset(pMemory,0,MAXMEMBUFFERSIZE);

memcpy(pMemory,testString[i],iStringLength);

reverseStringByWord(pMemory,iStringLength);

printf("|%s| ",reinterpret_cast<char*>(pMemory));

}

free(pMemory);

}

值得注意的是,这个算法在处理空格位于字符串的首部(或者尾部)的情况,还需要商榷和进一步的分析。

后记

通过本文的算法分析,我们能发现和总结一些算法设计的原则:

原则1:The Power of Primitive

一个复杂的问题往往可以被抽象成一个最基本的问题,这个基本的问题虽然简单,但是往往能够描述问题的实质。在我们解决问题的时候,能否正确的抓住问题的实质往往就是能够正确解决问题的关键。在本文介绍的算法中我们发现,在反转字符串中的单词的算法中最本质的操作就是交换不等长的两段不连续内存块。有了这个对问题本质的深入理解,我们就可以借助现有的算法轻松这个问题。

原则2:Divide-and-Conquer

这个思想不愧是解决很多问题的"不二法则"。看看算法书中有多少经典的算法应用了这个思路,我们就会惊叹的发现我们对它的认识还是很浅。这个思路最核心的本质,或者说成功的关键,就在于它可以将一个规模稍大的问题划分成一个或者多个规模稍小的问题,同时规模稍小的问题要比规模稍大的问题容易解决,只有这样,"Divide-and-Conquer" 才能现露它的神奇之处。在本文的算法中,反转字符串中的单词的问题最终被划分成反转两个不连续内存块的问题。

历史

12/16/2006 v1.0

原文的第一版

12/17/2006 v1.1

添加了后记部分,对本文中使用的两个算法设计原则进行了简单的总结

二:

文章中单词个数统计 字符串以空格反转 2010-11-14 20:58:15| 分类: 数据结构 | 标签: |字号大



小 订阅

1、有一篇文章存储在数组中统计出现的单词并按照出现次数排序

2、将一个存储在数组中的英文句子的单词倒排例如“my name is laolaoliu2002"变成”laolaoliu2002 is name my“不要使用额外的数组存储空间

#include<iostream>

using namespace std;

char* change(char *str)

{

char *p=str,*q=str,*str1,*q1;

int len=strlen(str)-1;

str1=new char[len+2];

//if(!str1)cout<<"ERROR!"<<endl;

while(*p)

{

if(*p!=' ')

{

if(*q==' '||*q==0)

{

q1=q;

do

{

str1[len]=*(--q);

len--;

}while(p!=q);

p=q=q1;

}

else q++;

}

else

{

str1[len]=' ';len--;p++;q++;

}

}

str1[strlen(str)]=*p;

return str1;

}

int main()

{

cout<<"请输入你要转换的字符串!"<<endl;

char str[80];

cin.getline(str,80);

cout<<change(str)<<endl;

system("PAUSE");

return 0;

}

三:

文章中单词个数统计 字符串以空格反转(发表时间: 2005-4-16 20:21:00)【评论】 【打印】 【字体:大 中 小】 本文链接:http://blog.pfan.cn/du51/577.html 复制链接

分享到: 0

1、有一篇文章存储在数组中统计出现的单词并按照出现次数排序

2、将一个存储在数组中的英文句子的单词倒排例如“my name is laolaoliu2002"变成”laolaoliu2002 is name my“不要使用额外的数组存储空间

是一个朋友的问题由于我最近比较忙(要出差)没有时间写了。

#include<iostream>

using namespace std;

char* change(char *str)

{

char *p=str,*q=str,*str1,*q1;

int len=strlen(str)-1;

str1=new char[len+2];

//if(!str1)cout<<"ERROR!"<<endl;

while(*p)

{

if(*p!=' ')

{

if(*q==' '||*q==0)

{

q1=q;

do

{

str1[len]=*(--q);

len--;

}while(p!=q);

p=q=q1;

}

else q++;

}

else

{

str1[len]=' ';len--;p++;q++;

}

}

str1[strlen(str)]=*p;

return str1;

}

int main()

{

cout<<"请输入你要转换的字符串!"<<endl;

char str[80];

cin.getline(str,80);

cout<<change(str)<<endl;

system("PAUSE");

return 0;

}

我自己写的.

#include<iostream>

using namespace std;

char *rev(char *src)

{

char *b, *e,ch;

b = src;

strrev(src);

while (*b)

{

while (isspace(*b))

++b;

e = b;

while (*e && !isspace(*e))

++e;

ch = *e,*e = '\0';

strrev(b);

b = e,*e = ch;

}

return src;

}

int main()

{

char a[80];

cout<<"请输入字符串"<<endl;

cin.getline(a,80);

cout<<rev(a)<<endl;

system("PAUSE");

return 0;

}

一个网友写的.很好.收下了.

=====================================================================

#include<iostream>

using namespace std;

struct node{

char *s;

int num;

node *next;

};

node* orderInsertnode(node *&head,node *p) //排序

{

node *p1,*p2;

if(!head) //建头

{

head=p;

p->next=0;

return head;

}

if(head->num>=p->num) //插入头之前

{

p->next=head;

head=p;

return head;

}

p1=p2=head;

while(p2->next&&p2->num<=p->num)

{

p1=p2;p2=p2->next;

}

if(p2->num<=p->num) //插入中间

{

p2->next=p;

p->next=0;

}

else //插入尾.

{

p->next=p2;

p1->next=p;

}

return head;

}

node *Insertnode(node *&head,node *p) //构建链表

{

node *q,*q1;

if(!head) //建头

{

head=p;

p->next=0;

p->num=1;

return head;

}

q1=q=head;

while(q) //处理中间情况

{

if(strcmp(q->s,p->s))

{

q1=q;q=q->next;

}

else

{

q->num+=1;

return head;

}

}

if(!q)q1->next=p; //插入尾部

return head;

}

node *filespik(char *file)

{

char *p=file,*q=p,*q1,e;

node *head=0,*head1=0,*word;

while(*p)

{

while(!isalpha(*p))++p; //指向单词开始处

q1=p;

while(isalpha(*q1)&&*q1)++q1; //指向单词尾处

e=*q1; //留COPY

*q1='\0'; //置结尾符

word=new node;

word->s=new char[strlen(p)+1];

strcpy(word->s,p); //放入单词

word->num=1;

word->next=0;

head=Insertnode(head,word); //插成无序表

word=0;p=q1; *p=e;

} //完成循环后所有单词被无序插入head中.

node *hh=head;

while(hh)

{

word=new node;

word->s=new char[strlen(hh->s)+1];

strcpy(word->s,hh->s);

word->num=hh->num;

word->next=0;

head1=orderInsertnode(head1,word);

hh=hh->next;

} //完成排序

while(head) //回收空间

{

hh=head;

head=hh->next;

delete hh->s;

delete hh;

}

return head1;

}

int main()

{

char test[]="aa aa bb bb bb bb test i love you but do you love me? so big disgrace! good luck";

node *result=filespik(test);

node *p=result;

while(p)

{

cout<<p->s<<" "<<p->num<<endl;

p=p->next;

}

while(result)

{

p=result;

result=result->next;

delete p->s;

delete p;

}

system("PAUSE");

return 0;

}

我自己写的,太麻烦了.可以省一个链表的,改天要改一下.

四:

关于反转字符串(Reverse Words)的思考及三种解法

2011年9月4日 Yx.Ac 发表评论 阅读评论

文章作者:Yx.Ac 文章来源:勇幸|Thinking (http://www.yongxingblog.com) 转载请注明,谢谢合作。

早些时候在水木上看到一道面试题,于是就思考着如何去解,也就有了上篇博客的整理以及这篇文章的思考。

题:反转一个字符串句子中的单词,例如“Do or do not, there is no try.”反转后为“try. no is there not, do or Do”假设所有的单词间都是以空格分离的,将标点符号视为单词的一部分。

基于上篇文章,本文提供三种方法解决该问题,第一种是Houdy博客中提到的方法,后两种方法是我在思考这个问题时想到的。其中,前两种方法都是基于Divide-and-Conquer (分治)的思想,该思想非常强大,是解决问题的一个重要的方法;第三种方法是基于非分治的思想,比分治能优化一些,较为直接,欢迎讨论。

注:以下方法在实现中所用到的函数都是源自与上一篇博文:交换两段连续(不连续)内存。

方法一(分治A):

上篇文章我们提到了反转内存以及如何交换两段连续和非连续的内存,这里的分析也要基于此。个人感觉Houdy的分析很到位,循序渐进,由浅入深,他的核心思想我就直接转载了,呵呵。

从整体考虑这个问题,我们要处理的是由多个单词(单词间用空格分隔)构成的字符串,这个字符串中基本的元素就是单词和空格。首先拿几个特例来分析一下,看看有没有什么收获。

字符串中没有空格。这时候相当于字符串中只有一个单词,我们不需要进行任何操作。

字符串中有一个空格(暂且认为空格在中间)。这是反转两个单词的操作,等价于反转两块不连续的内存块的操作,这个操作上篇文章我们已经实现了

字符串中有两个空格。例如 ”Tom Jerry Mike”,我们应该怎么处理这种情况呢?结合前面两个特例的分析,我们可以马上想到一种常见的解决问题思路,即分治,也就是Houdy说的 “Divide-and-Conquer”。总体来说,我们可以将包含多于一个空格的字符串划分成三个部分: |位于空格左边的字符串|空格|位于空格右边的字符串|

这样,我们就完成下面的处理就可以了:

对空格左边的字符串进行“反转字符串”处理

对空格右边的字符串进行“反转字符串”处理

将字符串看成一个整体,对左边的串和右边的串进行“交换两块不连续内存”处理

如果左右字符串中还包含多于一个空格,我们可以分别对他们再进行“分割”+“反转”的操作,直到字符串中的空格数量不大于1,空格数等于1时,做“交换两块不连续内存”的操作。所以,我们需要计算空格数目并记录空格的索引。

还有一个问题就是,字符串中的多个空格,我们该选用哪个空格做为分割点呢?其实,基于这个问题出发,反转字符串我们就有了两种不同的做法,一种是上面提到的基于“Binary”空格的分割方法,另一种是则我刚开始用分治来思考这个问题想到的,见分治B方法:

方法二(分治B):

核心思想就是不用中间的空格对字符串进行分割,而是总用第一个空格进行分割,还以上面的例子分析,”Tom Jerry Mike”,当我们读到第一个空格时,我们知道该空格之前一定是一个单词,是不需要进行“反转字符串”操作的,所以我们只需对右边剩下的部分进行“反转字符串”操作即可,于是我们做以下处理:

读到第一个空格,对空格右边的字符串进行“反转字符串”处理

将字符串看成一个整体,对左串和右串进行“交换两块不连续内存”处理

分析分治B与分治A方法比起来,程序在编写方面分治B方法比较容易,不需要计算空格数目和记录空格的位置,不需要根据空格数目考虑那么多种情况;在效率方面,都是差不多的,都是分治递归,只是分治A方法用两个递归树,每个递归的层次能较浅一些,而分治B方法用一个递归树,递归的层次自然能深一些。

下面给出了两种方法的实现:

方法一(分治A)

01 //reverse the words of one string

02 // merge reverse

03

04 void * reverseWords(void * pMemory, const size_t memSize)

05 {

06 if(NULL == pMemory) return pMemory;

07 if(memSize < 2) return pMemory;

08

09 char* pByteMemory = reinterpret_cast<char*>(pMemory);

10 size_t spaceNum = 0;

11 size_t* spaceIndex = new size_t [memSize];

12

13 for(size_t i = 0; i < memSize; i++)

14 {

15 if(pByteMemory[i] == SPACESEPARATOR)

16 {

17 spaceIndex[spaceNum] = i;

18 spaceNum ++;

19 }

20 }

21

22 if(spaceNum == 0){ //do nothing;

23 }

24 else if(spaceNum == 1)

25 {

26 swapNonAdjacentMemory(pByteMemory,spaceIndex[0],memSize-spaceIndex[0]-1,memSize);

27 }else

28 {

29 size_t spaceMiddle = spaceNum/2;

30 // reverse the left block of space

31 reverseWords(pByteMemory,spaceIndex[spaceMiddle]);

32 // reverse the right block of space

33 reverseWords(pByteMemory+spaceIndex[spaceMiddle]+1,memSize-spaceIndex[spaceMiddle]-1);

34 // swap the whole block

35 swapNonAdjacentMemory(pByteMemory,spaceIndex[spaceMiddle],memSize-spaceIndex[spaceMiddle]-1,memSize);

36 }

37

38 delete [] spaceIndex;

39 return pMemory;

40 }

方法二(分治B)

01 // Reverse Words

02 // 分治B方法

03 void * reverseWords(void *pMemory, const size_t memSize)

04 {

05 if(NULL == pMemory) return pMemory;

06 if(memSize < 2) return pMemory;

07

08 char *pStart = static_cast<char*>(pMemory);

09 for(size_t index = 0; index < memSize; ++index)

10 {

11 if(SPACESEPARATOR == pStart[index])

12 {

13 reverseWords(pStart+index+1,memSize-index-1);

14 swapNonAdjacentMemory(pStart,index,memSize-index-1,memSize);

15 break;

16 }

17 }

18 return pMemory;

19 }

可以看出,B方法的代码量只是A方法的一半,能稍微容易理解一些。

有了上述两种方法,我们给出测试用例(该测试用例亦可用于第三种方法):

01 #include <iostream>

02 using namespace std;

03

04 #define MAXBUFFERSIZE 100

05 #define SPACESEPARATOR ' '

06

07 void main()

08 {

09 static const char* stringArr[]=

10 {

11 "",

12 "ab",

13 "ab ",

14 " ab",

15 "a b",

16 "ab cd ef",

17 "ab cd ef ",

18 " ab cd ef",

19 "aaa bbbb ccccc",

20 };

21

22 void * resultArr = malloc(MAXBUFFERSIZE); // store the test result

23 if(NULL == resultArr) return ;

24

25 size_t arrLen = sizeof(stringArr)/sizeof(*stringArr);

26

27 // reverse words

28 for(size_t i = 0; i < arrLen; i++)

29 {

30 memset(resultArr,0,MAXBUFFERSIZE);

31 memcpy(resultArr,stringArr[i],strlen(stringArr[i]));

32 reverseWords(resultArr,strlen(stringArr[i]));

33 printf("%s\n",resultArr);

34 }

35 free(resultArr);

36 }

思考:

这道题给我最大的 启发就是,看问题要有全局观,能从“整体”上去把握、分析问题,才能将问题抽象成一个最基本的问题,然后从简单到复杂,由浅入深地去分析,便能找到问题的本质。另外,分治思想不愧为解决很多问题的“不二法则”,它的关键就在于它可以将一个较大的问题划分成一个或多个规模较小的问题,同时较小的问题容易解决,只有这样才能显露分治的神奇之处。本文算法中,最终将问题转化成“交换两段不连续内存”的问题。

方法三(非分治):

第三种方法不需要递归,不需要“交换不连续内存”,就是完全利用上篇文章提到的“反转内存”这一基本操作。

Idea:很simple的一个想法:给定一个字符串,反转一次是反的,题目要求是单词为单位,那么把单词再反转过来不就可以了么?还以先前的例子分析,”Tom Jerry Mike”,反转一次为 ”ekiM yrreJ moT”,将单词反转过来得到 ”Mike Jerry Tom”,得解。于是我们做以下处理即可:

对整个字符串做“反转内存”处理

对每个单词做“反转内存”处理

实现如下:

01 void* reverseWords(void *pMemory, const size_t memSize)

02 {

03 char* pStart = static_cast<char*>(pMemory);

04

05 // check having space or not

06 bool isHaveSpace = false;

07 for(size_t i = 0; i < memSize; ++i)

08 {

09 if(SPACESEPARATOR == pStart[i])

10 {

11 isHaveSpace = true;

12 break;

13 }

14 }

15 if(isHaveSpace) // if have not space, only one word, then return

16 {

17 reverseMemory(pMemory,memSize); // reverse the whole string

18 size_t index = 0, istart = 0;

19 for(; index <= memSize; ++index) // reverse every word

20 {

21 if(SPACESEPARATOR == pStart[index] || NULL == pStart[index])

22 {

23 reverseMemory(pStart+istart,index-istart);

24 istart = index + 1;

25 }

26 }

27 }

28 return pMemory;

29 }

程序先检查字符串是否只有一个单词,即没有空格,如果没有空格则直接返回即可。这里将只含有一个单词和空格的字符串当做另一种情况处理,即有空格的情况。

这个题目整理下来发现仔细去思考一些问题的时候还是很有收获的,第二种方法是思考分治的时候突然想到的,发现程序写起来比常用的“二分”分割思想要容易一些。第三种方法则属于没有思想的赖皮方法了,不过能用非递归的方法很好地解决这个问题,我也就贴出来了。

以上程序均测试正确,如有问题,欢迎讨论!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: