樸素查找子串算法和KMP算法

 如何在一個字符串查找子串呢?按照我們以前學過的查找方法無非是這樣的。
         假如就給定字符串A爲“ababcabcd.....”,設要從A查找的子串a爲"abc",我們就同時遍歷A串,和a串,分別用i和j記錄遍歷的下標,如果A串的i和a串的j 各自對應的字符一樣,我們就讓i,j同時往後+,然而大部分情況是i和j還沒到它們各自的終點就出現匹配失敗了。匹配失敗子串的i要置0,j退回到j這輪開始的下一個位置。
       



然而這種查找速度太慢,時間複雜度過高,有太多不必要的遍歷。
何爲“不必要”?
舉個栗子
就拿"ababcabcd.....”中找"abc"來說吧,第一次從i=0開始找子串,i到第三位就失配了腰折了,按照樸素查找的思想,我們要把i再返還到i=0的下一個i=1;可是我們用眼睛看看,i=1的開頭是'b',和子串a j置爲0的'a',開頭就不一樣了!那我們還有必要把i退到i=1這個不必要的位置嗎?顯然沒有必要。

        可是如果i不退的話,可能又會錯過匹配成功的地方。也就是說一輪和子串匹配下來 各種++後的i  和 各種++前的原始i之間會有一處地方k可能會匹配成功,並且那個k纔是我們真正想要的退回的地方。
然而我們再想想還要什麼可以再優化的,既然是A串和a串匹配直到i的位置失配了,是不是代表i之前  A串和a串匹配成功的內容全部一樣的,我們剛說了讓i退到某個地方k,那是不是轉換成i不退,子串a的j退到k。
      換句話說,如果假如真的i退到某個k地方,那麼j置爲0,i和j肯定又能匹配成功同步++直到上一輪的i位置,最後的結果是i回到上輪的原位置,而j到了k的位置,這都是我們預料到的100%會發生的事情,那麼何必讓計算機再重複這些操作增加複雜度呢?
    別亂,千萬別亂,請想清楚這步再往下看,否則再往下也是天書!
    再簡化說,我們先不管A串什麼樣子,我們就光看a串,因爲之前匹配,所以就可以通過a串知道A串是什麼樣子!然後我們只要對a串做一些牛逼的想法就行了,什麼牛逼的想法?我們直到我們要極力避免的情況就是進行不必要的匹配判斷, 我們要找到a串匹配失敗元素前中重複的段落,a串匹配失敗前的段位是和A串的我們正用的一部分是完全一樣的。所以a串中找到重複的東西,就相當於映射了A串中的重複。
比如“ababcsdasdsa”中找"ababe"   我們直到i和j都到第5位的時候,'c'和'e'匹配會失敗,而兩串之前的"abab"的部分是一樣的吧!然而按照樸素思想,我們要把i回到第一個'b'位置,可是我們明知'b'開頭是不會有結果的!
而後面第二組的“ab”和第一組“ab”一樣,那我們爲什麼不從第二組的“ab”開始?然而與其讓j置0,i從第二組“ab”開始,爲什麼不讓i不動,j從第二組"ab"開始?就目前來看兩個串的前四位似乎沒有什麼不同吧?那麼下一次匹配的時候,i並沒有動!而j也沒有退到0那麼遠的位置,而是隻退了2步,到了第二組“ab”的開頭。



   不管上述原理你有沒有聽懂,你也不必糾結,請記住我們只對a串即要查找的小串進行處理,對於被查找的大串A串是不用管的,其中自然有對應關係!
    而我們要找到的東西就是子串失配前的那段中是否存在兩個相同的最大真子串,如果有,這兩個最大真子串有多大。我們要靠它來確定j的回退的地方!。
   而約束這兩個最大真子串的條件也是有的,一條真子串要以0開頭,另一條要以失配位的前一位爲結尾
 還有一個問題,爲什麼要是最大真子串?你想想如果任意要任意的子串,是不是違背了我們的初衷,會有不徹底的重複段落的篩選,簡單來說,一條真子串就是一個字母在0位,另一個真子串也是相同的字母 位置在失配前一位,中間有大量類似“abababa”這樣的,你看看你這樣要重複判斷多少次!
  然而如何求串中每個位置出現失配情況下的它之前的兩個最大真子串長度呢。(如果沒有就是0)
  
  我們隨便舉個例子看看
  “abababcdab”
    -1
     a失配了,可它前面並沒有任何串了,比較特殊,記爲-1;
 
 “abababcdab”
    -10
     第二位b失配了,它前面只有一個“a”串,真子串是不能和它的父串完全等同的,所以沒有,記爲0
  
  “abababcdab”
    -100
    第三位a失配,它前面的串是“ab”   很顯然,它只有“a”和“b”兩個真子串。按規則來,也是沒有相同的真子串的,記爲0
  “
      abababcdab”
    -1001
   第四位b失配,它前面的串是“aba”,“a”和“a”兩個真子串符合規則,記爲1

      abababcdab”
     -10012
    第五位a失配,它前面的串是“abab”,“ab”和“ab”,記爲2

      abababcdab”
     -100123
    第6位b失配,它前面的串是“ababa”,“aba”和“aba”,記爲3

       abababcdab”
      -1001234
    第7位c失配,它前面的串是“ababab”,“abab”和“abab”,記爲4
     

      abababcdab”
      -10012340
    第8位d失配,它前面的串是“abababd”,然而並沒有0號位開始的'd'結尾的真子串,記爲0
     
......

 我們把從這個串裏找到這些數字稱之爲next數組,只要找到了這些數組,我們就知道了每次匹配失敗後,j究竟該回到哪個位置!
  那麼它如何求呢?
  除了next[0]和next[1]固定爲-1和0,我們可以發現規律,就是next數組的增長最多爲1,降低不確定。增加的觸發條件是什麼?

我把next增長成功的一部分截了下來,不難發現,每次新增的失配位的前一位 如果和上次記錄第一個最大真子串的下一個字符  一樣的話,就可以增長。
而且增長規律也很簡單,因爲每次最大比上次多出一位,所以next元素也最多加1.
    
可是如果,每次新增的失配位的前一位 如果和上次記錄的第一個最大真子串的下一個字符  不一樣的話,那麼規律又是什麼呢?明顯沒有增長那麼簡單。

正當我們毫無頭緒的時候,我們看看下面這張增長失敗的圖。
藍色表示新增的不一樣。


眼熟不!!!
是不是我們最開始找子串匹配失敗的情況!
我們要做什麼???是不是讓下標往回退!退到next[它自己位置]的地方!如果退的地方還是出現
失配位的前一位 和 退到的位置的下一個字符  不一樣的話怎麼辦?
那就繼續退,直到退到-1.無路可退位置!
這裏就凸顯了第一位設爲-1的智慧,如果設爲0的話,next數組中間也有0存在,會出現誤判!
到此next數組的求法就出來,next數組求出之後,我麼只要把樸素查找稍作修改,就可以變成變態的KMP算法!
怎麼修改。樸素算法的匹配成功代碼塊不動,只要把失配時   i=i-j+1; j=0;   修改爲j=next[j];
把判斷匹配成功的判斷條件改爲   if(j==-1 || (str1[i]==str2[j]))   爲什麼要加入j==-1? 因爲這就考慮到上面說的,如果一直退到-1的情況。

如此以來,時間複雜度從原先0(n*m)變成了0(m+n)

代碼如下


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