程序員編程藝術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)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章