1. 程式人生 > >程式設計師面試100題之一 對稱字串的最大長度

程式設計師面試100題之一 對稱字串的最大長度

                題目:輸入一個字串,輸出該字串中對稱的子字串的最大長度。比如輸入字串“google”,由於該字串裡最長的對稱子字串是“goog”,因此輸出4。

分析:可能很多人都寫過判斷一個字串是不是對稱的函式,這個題目可以看成是該函式的加強版。

要判斷一個字串是不是對稱的,不是一件很難的事情。我們可以先得到字串首尾兩個字元,判斷是不是相等。如果不相等,那該字串肯定不是對稱的。否則我們接著判斷裡面的兩個字元是不是相等,以此類推。基於這個思路,我們不難寫出如下程式碼:

/*判斷起始指標為pBegin,結束指標為pEnd的字串是否對稱*/bool IsSymmetrical(char* pBegin, char* pEnd)
if(pBegin == NULL || pEnd == NULL || pBegin > pEnd)  return falsewhile(pBegin < pEnd) {  if(*pBegin != *pEnd)   return false;  pBegin++;  pEnd --; } return true;}

        要判斷一個字串pString是不是對稱的,我們只需要呼叫IsSymmetrical(pString, &pString[strlen(pString) – 1])就可以了。

        現在我們試著來得到對稱子字串的最大長度。最直觀的做法就是得到輸入字串的所有子字串,並逐個判斷是不是對稱的。如果一個子字串是對稱的,我們就得到它的長度。這樣經過比較,就能得到最長的對稱子字串的長度了。於是,我們可以寫出如下程式碼:

/*取得所有對稱子串的最大長度時間複雜度: O(n^3)*/int GetLongestSymmetricalLength_1(char* pString)if(pString == NULL)  return 0int symmeticalLength = 1char* pFirst = pString; int length = strlen(pString); while(pFirst < &pString[length - 1]) {  char* pLast = pFirst + 1;  while(pLast <= &pString[length - 1
])  {   if(IsSymmetrical(pFirst, pLast))   {    int newLength = pLast - pFirst + 1;    if(newLength > symmeticalLength)     symmeticalLength = newLength;                             }   pLast++;  }  pFirst++; } return symmeticalLength;}

         我們來分析一下上述方法的時間效率。由於我們需要兩重while迴圈,每重迴圈需要O(n)的時間。另外,我們在迴圈中呼叫了IsSymmetrical,每次呼叫也需要O(n)的時間。因此整個函式的時間效率是O(n^3)。

        通常O(n^3)不會是一個高效的演算法。如果我們仔細分析上述方法的比較過程,我們就能發現其中有很多重複的比較。假設我們需要判斷一個子字串具有aAa的形式(A是aAa的子字串,可能含有多個字元)。我們先把pFirst指向最前面的字元a,把pLast指向最後面的字元a,由於兩個字元相同,我們在IsSymtical函式內部向後移動pFirst,向前移動pLast,以判斷A是不是對稱的。接下來若干步驟之後,由於A也是輸入字串的一個子字串,我們需要再一次判斷它是不是對稱的。也就是說,我們重複多次地在判斷A是不是對稱的。

        造成上述重複比較的根源在於IsSymmetrical的比較是從外向裡進行的。在判斷aAa是不是對稱的時候,我們不知道A是不是對稱的,因此需要花費O(n)的時間來判斷。下次我們判斷A是不是對稱的時候,我們仍然需要O(n)的時間。

        如果我們換一種思路,我們從裡向外來判斷。也就是我們先判斷子字串A是不是對稱的。如果A不是對稱的,那麼向該子字串兩端各延長一個字元得到的字串肯定不是對稱的。如果A對稱,那麼我們只需要判斷A兩端延長的一個字元是不是相等的,如果相等,則延長後的字串是對稱的。因此在知道A是否對稱之後,只需要O(1)的時間就能知道aAa是不是對稱的。

       我們可以根據從裡向外比較的思路寫出如下程式碼:

/*取得所有對稱子串的最大長度時間複雜度: O(n^2)*/int GetLongestSymmetricalLength(char* pString)if(pString == NULL)  return 0int symmeticalLength = 1char* pChar = pString; while(*pChar != '\0') {  // Substrings with odd length  char* left = pChar - 1;  char* right = pChar + 1;  while(left >= pString && *right != '\0' && *left == *right)  {   left--;   right++;  }  int newLength = right - left - 1;    //退出while迴圈時,*left != *right  if(newLength > symmeticalLength)   symmeticalLength = newLength;   // Substrings with even length  left = pChar;  right = pChar + 1;  while(left >= pString && *right != '\0' && *left == *right)  {   left--;   right++;  }  newLength = right - left - 1;        //退出while迴圈時,*left != *right  if(newLength > symmeticalLength)   symmeticalLength = newLength;  pChar++; } return symmeticalLength;}

        由於子字串的長度可能是奇數也可能是偶數。長度是奇數的字串是從只有一個字元的中心向兩端延長出來,而長度為偶數的字串是從一個有兩個字元的中心向兩端延長出來。因此我們的程式碼要把這種情況都考慮進去。

       在上述程式碼中,我們從字串的每個字串兩端開始延長,如果當前的子字串是對稱的,再判斷延長之後的字串是不是對稱的。由於總共有O(n)個字元,每個字元可能延長O(n)次,每次延長時只需要O(1)就能判斷出是不是對稱的,因此整個函式的時間效率是O(n^2)。

迴文串定義:“迴文串”是一個正讀和反讀都一樣的字串,比如“level”或者“noon”等等就是迴文串。迴文子串,顧名思義,即字串中滿足迴文性質的子串。經常有一些題目圍繞回文子串進行討論,比如 HDOJ_3068_最長迴文,求最長迴文子串的長度。樸素演算法是依次以每一個字元為中心向兩側進行擴充套件,顯然這個複雜度是O(N^2)的,關於字串的題目常用的演算法有KMP、字尾陣列、AC自動機,這道題目利用擴充套件KMP可以解答,其時間複雜度也很快O(N*logN)。但是,今天筆者介紹一個專門針對迴文子串的演算法,其時間複雜度為O(n),這就是manacher演算法。大家都知道,求迴文串時需要判斷其奇偶性,也就是求aba和abba的演算法略有差距。然而,這個演算法做了一個簡單的處理,很巧妙地把奇數長度迴文串與偶數長度迴文串統一考慮,也就是在每個相鄰的字元之間插入一個分隔符,串的首尾也要加,當然這個分隔符不能再原串中出現,一般可以用‘#’或者‘$’等字元。例如:原串:abaab新串:#a#b#a#a#b#這樣一來,原來的奇數長度迴文串還是奇數長度,偶數長度的也變成以‘#’為中心的奇數迴文串了。接下來就是演算法的中心思想,用一個輔助陣列P記錄以每個字元為中心的最長迴文半徑,也就是P[i]記錄以Str[i]字元為中心的最長迴文串半徑。P[i]最小為1,此時迴文串為Str[i]本身。我們可以對上述例子寫出其P陣列,如下新串: # a # b # a # a # b #P[]  :  1 2 1 4 1 2 5 2 1 2 1我們可以證明P[i]-1就是以Str[i]為中心的迴文串在原串當中的長度。證明:1、顯然L=2*P[i]-1即為新串中以Str[i]為中心最長迴文串長度。2、以Str[i]為中心的迴文串一定是以#開頭和結尾的,例如“#b#b#”或“#b#a#b#”所以L減去最前或者最後的‘#’字元就是原串中長度的二倍,即原串長度為(L-1)/2,化簡的P[i]-1。得證。依次從前往後求得P陣列就可以了,這裡用到了DP(動態規劃)的思想,也就是求P[i]的時候,前面的P[]值已經得到了,我們利用迴文串的特殊性質可以進行一個大大的優化。我先把核心程式碼貼上:

for(i=1;i<n;i++){ if(MaxId>i) {  p[i]=Min(p[2*id-i],MaxId-i); } else {  p[i]=1; } while(Str[i+p[i]]==Str[i-p[i]]) {  p[i]++; } if(p[i]+i>MaxId) {  MaxId=p[i]+i;  id=i; }}
為了防止求P[i]向兩邊擴充套件時可能陣列越界,我們需要在陣列最前面和最後面加一個特殊字元,令P[0]=‘$’最後位置預設為‘\0’不需要特殊處理。此外,我們用MaxId變數記錄在求i之前的迴文串中,延伸至最右端的位置,同時用id記錄取這個MaxId的id值。通過下面這句話,演算法避免了很多沒必要的重複匹配。
if(MaxId>i){    p[i]=Min(p[2*id-i],MaxId-i);}
那麼這句話是怎麼得來的呢,其實就是利用了迴文串的對稱性,如下圖:j=2*id-1即為i關於id的對稱點,根據對稱性,P[j]的迴文串也是可以對稱到i這邊的,但是如果P[j]的迴文串對稱過來以後超過MaxId的話,超出部分就不能對稱過來了,如下圖,所以這裡P[i]為的下限為兩者中的較小者,p[i]=Min(p[2*id-i],MaxId-i)。演算法的有效比較次數為MaxId次,所以說這個演算法的時間複雜度為O(n)。附HDOJ_3068_最長迴文程式碼:
#include <stdio.h>#define M 110010char b[M],a[M<<1];int p[M<<1];int Min(int a,int b){    return a<b?a:b;}int main(void){    int i,n,id,MaxL,MaxId;    while(scanf("%s",&b[1])!=EOF)    {        MaxL=MaxId=0;        for(i=1;b[i]!='\0';i++)        {            a[(i<<1)]=b[i];            a[(i<<1)+1]='#';        }        a[0]='?';a[1]='#';        n=(i<<1)+2;a[n]=0;        MaxId=MaxL=0;        for(i=1;i<n;i++)        {            if(MaxId>i)            {                p[i]=Min(p[2*id-i],MaxId-i);            }            else            {                p[i]=1;            }            while(a[i+p[i]]==a[i-p[i]])            {                p[i]++;            }            if(p[i]+i>MaxId)            {                MaxId=p[i]+i;                id=i;            }            if(p[i]>MaxL)            {                MaxL=p[i];            }        }        printf("%d\n",MaxL-1);    }    return 0;}