久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

從頭到尾徹底理解KMP(2014年8月22日版)

 郝彩虹 2014-10-13

從頭到尾徹底理解KMP


作者:July
時(shí)間:最初寫于2011年12月,,2014年7月21日晚10點(diǎn) 全部刪除重寫成此文,隨后的半個多月不斷反復(fù)改進(jìn),。


1. 引言

    本KMP原文最初寫于2年多前的2011年12月,,因當(dāng)時(shí)初次接觸KMP,思路混亂導(dǎo)致寫也寫得混亂,,如此,,留言也是“罵聲”一片。所以一直想找機(jī)會重新寫下KMP,,但苦于一直以來對KMP的理解始終不夠,,故才遲遲沒有修改本文。

    然近期因在北京開了個算法班,,專門講解數(shù)據(jù)結(jié)構(gòu),、面試、算法,,才再次仔細(xì)回顧了這個KMP,,在綜合了一些網(wǎng)友的理解、以及跟我一起講算法的兩位講師朋友曹博,、鄒博的理解之后,,寫了9張PPT,發(fā)在微博上,。隨后,,一不做二不休,索性將PPT上的內(nèi)容整理到了本文之中(后來文章越寫越完整,,所含內(nèi)容早已不再是九張PPT 那樣簡單了),。

    KMP本身不復(fù)雜,但網(wǎng)上絕大部分的文章(包括本文的2011年版本)把它講混亂了,。下面,,咱們從暴力匹配算法講起,隨后闡述KMP的流程 步驟,、next 數(shù)組的簡單求解 遞推原理 代碼求解,,接著基于next 數(shù)組匹配,談到有限狀態(tài)自動機(jī),,next 數(shù)組的優(yōu)化,,KMP的時(shí)間復(fù)雜度分析,,最后簡要介紹兩個KMP的擴(kuò)展算法。

    全文力圖給你一個最為完整最為清晰的KMP,,希望更多的人不再被KMP折磨或糾纏,,不再被一些混亂的文章所混亂,有何疑問,,歡迎隨時(shí)留言評論,,thanks。


2. 暴力匹配算法

    假設(shè)現(xiàn)在我們面臨這樣一個問題:有一個文本串S,,和一個模式串P,,現(xiàn)在要查找P在S中的位置,怎么查找呢,?

    如果用暴力匹配的思路,,并假設(shè)現(xiàn)在文本串S匹配到 i 位置,模式串P匹配到 j 位置,,則有:

  • 如果當(dāng)前字符匹配成功(即S[i] == P[j]),,則i++,j++,,繼續(xù)匹配下一個字符,;
  • 如果失配(即S[i]! = P[j]),令i = i - (j - 1),,j = 0,。相當(dāng)于每次匹配失敗時(shí),i 回溯,,j 被置為0,。
    理清楚了暴力匹配算法的流程及內(nèi)在的邏輯,咱們可以寫出暴力匹配的代碼,,如下:
  1. int ViolentMatch(char* s, char* p)  
  2. {  
  3.     int sLen = strlen(s);  
  4.     int pLen = strlen(p);  
  5.   
  6.     int i = 0;  
  7.     int j = 0;  
  8.     while (i < sLen && j < pLen)  
  9.     {  
  10.         if (s[i] == p[j])  
  11.         {  
  12.             //①如果當(dāng)前字符匹配成功(即S[i] == P[j]),,則i++,j++      
  13.             i++;  
  14.             j++;  
  15.         }  
  16.         else  
  17.         {  
  18.             //②如果失配(即S[i]! = P[j]),,令i = i - (j - 1),,j = 0      
  19.             i = i - j + 1;  
  20.             j = 0;  
  21.         }  
  22.     }  
  23.     //匹配成功,返回模式串p在文本串s中的位置,,否則返回-1  
  24.     if (j == pLen)  
  25.         return i - j;  
  26.     else  
  27.         return -1;  
  28. }  

    舉個例子,,如果給定文本串S“BBC ABCDAB ABCDABCDABDE”,和模式串P“ABCDABD”,,現(xiàn)在要拿模式串P去跟文本串S匹配,,整個過程如下所示:

    1. S[0]為B,P[0]為A,不匹配,,執(zhí)行第②條指令:“如果失配(即S[i]! = P[j]),,令i = i - (j - 1),j = 0”,,S[1]跟P[0]匹配,,相當(dāng)于模式串要往右移動一位(i=1,j=0)

    2. S[1]跟P[0]還是不匹配,,繼續(xù)執(zhí)行第②條指令:“如果失配(即S[i]! = P[j]),,令i = i - (j - 1),j = 0”,,S[2]跟P[0]匹配(i=2,j=0),,從而模式串不斷的向右移動一位(不斷的執(zhí)行“令i = i - (j - 1),,j = 0”,i從2變到4,,j一直為0)

    3. 直到S[4]跟P[0]匹配成功(i=4,,j=0),此時(shí)按照上面的暴力匹配算法的思路,,轉(zhuǎn)而執(zhí)行第①條指令:“如果當(dāng)前字符匹配成功(即S[i] == P[j]),,則i++,j++”,,可得S[i]為S[5],,P[j]為P[1],即接下來S[5]跟P[1]匹配(i=5,,j=1)

     

    4. S[5]跟P[1]匹配成功,,繼續(xù)執(zhí)行第①條指令:“如果當(dāng)前字符匹配成功(即S[i] == P[j]),則i++,,j++”,,得到S[6]跟P[2]匹配(i=6,j=2),,如此進(jìn)行下去

    

    5. 直到S[10]為空格字符,,P[6]為字符D(i=10,j=6),,因?yàn)椴黄ヅ?,重新?zhí)行第②條指令:“如果失配(即S[i]! = P[j]),令i = i - (j - 1),,j = 0”,,相當(dāng)于S[5]跟P[0]匹配(i=5,j=0)

     

    6. 至此,我們可以看到,,如果按照暴力匹配算法的思路,,盡管之前文本串和模式串已經(jīng)分別匹配到了S[9]、P[5],,但因?yàn)镾[10]跟P[6]不匹配,,所以文本串回溯到S[5],模式串回溯到P[0],,從而讓S[5]跟P[0]匹配,。

    而S[5]肯定跟P[0]失配。為什么呢,?因?yàn)樵谥暗?步匹配中,,我們已經(jīng)得知S[5] = P[1] = B,而P[0] = A,,即P[1] != P[0],,故S[5]必定不等于P[0],所以回溯過去必然會導(dǎo)致失配,。那有沒有一種算法,,讓i 不往回退,只需要移動j 即可呢,?

    答案是肯定的,。這種算法就是本文的主旨KMP算法,它利用之前已經(jīng)部分匹配這個有效信息,,保持i 不回溯,,通過修改j 的位置,讓模式串盡量地移動到有效的位置,。


3. KMP算法

3.1 定義

    Knuth-Morris-Pratt 字符串查找算法,,簡稱為 “KMP算法”,常用于在一個文本串S內(nèi)查找一個模式串P 的出現(xiàn)位置,,這個算法由Donald Knuth,、Vaughan Pratt、James H. Morris三人于1977年聯(lián)合發(fā)表,,故取這3人的姓氏命名此算法,。
    下面先直接給出KMP的算法流程(如果感到一點(diǎn)點(diǎn)不適,沒關(guān)系,,堅(jiān)持下,,稍后會有具體步驟及解釋,越往后看越會柳暗花明?):
  • 假設(shè)現(xiàn)在文本串S匹配到 i 位置,,模式串P匹配到 j 位置
    • 如果j = -1,,或者當(dāng)前字符匹配成功(即S[i] == P[j]),,都令i++,j++,,繼續(xù)匹配下一個字符,;
    • 如果j != -1,且當(dāng)前字符匹配失?。碨[i] != P[j]),,則令 i 不變,j = next[j],。此舉意味著失配時(shí),,模式串P相對于文本串S向右移動了j - next [j] 位。
      • 換言之,,當(dāng)匹配失敗時(shí),,模式串向右移動的位數(shù)為:失配字符所在位置 - 失配字符對應(yīng)的next 值(next 數(shù)組的求解會在下文的3.3.3節(jié)中詳細(xì)闡述),即移動的實(shí)際位數(shù)為:j - next[j],,且此值大于等于1,。
    很快,你也會意識到next 數(shù)組各值的含義:代表當(dāng)前字符之前的字符串中,,有多大長度的相同前綴后綴。例如如果next [j] = k,,代表j 之前的字符串中有最大長度為k 的相同前綴后綴,。
    此也意味著在某個字符失配時(shí),該字符對應(yīng)的next 值會告訴你下一步匹配中,,模式串應(yīng)該跳到哪個位置(跳到next [j] 的位置),。如果next [j] 等于0或-1,則跳到模式串的開頭字符,,若next [j] = k 且 k > 0,,代表下次匹配跳到j(luò) 之前的某個字符,而不是跳到開頭,,且具體跳過了k 個字符,。
    轉(zhuǎn)換成代碼表示,則是:
  1. int KmpSearch(char* s, char* p)  
  2. {  
  3.     int i = 0;  
  4.     int j = 0;  
  5.     int sLen = strlen(s);  
  6.     int pLen = strlen(p);  
  7.     while (i < sLen && j < pLen)  
  8.     {  
  9.         //①如果j = -1,,或者當(dāng)前字符匹配成功(即S[i] == P[j]),,都令i++,j++      
  10.         if (j == -1 || s[i] == p[j])  
  11.         {  
  12.             i++;  
  13.             j++;  
  14.         }  
  15.         else  
  16.         {  
  17.             //②如果j != -1,,且當(dāng)前字符匹配失?。碨[i] != P[j]),則令 i 不變,,j = next[j]      
  18.             //next[j]即為j所對應(yīng)的next值        
  19.             j = next[j];  
  20.         }  
  21.     }  
  22.     if (j == pLen)  
  23.         return i - j;  
  24.     else  
  25.         return -1;  
  26. }  
    繼續(xù)拿之前的例子來說,,當(dāng)S[10]跟P[6]匹配失敗時(shí),KMP不是跟暴力匹配那樣簡單的把模式串右移一位,而是執(zhí)行第②條指令:“如果j != -1,,且當(dāng)前字符匹配失?。碨[i] != P[j]),則令 i 不變,,j = next[j]”,,即j 從6變到2(后面我們將求得P[6],即字符D對應(yīng)的next 值為2),,所以相當(dāng)于模式串向右移動的位數(shù)為j - next[j](j - next[j] = 6-2 = 4),。
    向右移動4位后,S[10]跟P[2]繼續(xù)匹配,。為什么要向右移動4位呢,,因?yàn)橐苿?位后,模式串中又有個“AB”可以繼續(xù)跟S[8]S[9]對應(yīng)著,,從而不用讓i 回溯,。相當(dāng)于在除去字符D的模式串子串中尋找相同的前綴和后綴,然后根據(jù)前綴后綴求出next 數(shù)組,,最后基于next 數(shù)組進(jìn)行匹配(不關(guān)心next 數(shù)組是怎么求來的,,只想看匹配過程是咋樣的,可直接跳到下文3.3.4節(jié)),。

3.2 步驟

  • ①尋找前綴后綴最長公共元素長度
    • 對于P = p0 p1 ...pj-1 pj,,尋找模式串P中長度最大且相等的前綴和后綴。如果存在p0 p1 ...pk-1 pk = pj- k pj-k+1...pj-1 pj,,那么在包含pj的模式串中有最大長度為k+1的相同前綴后綴,。舉個例子,如果給定的模式串為“abab”,,那么它的各個子串的前綴后綴的公共元素的最大長度如下表格所示:

比如對于字符串a(chǎn)ba來說,,它有長度為1的相同前綴后綴a;而對于字符串a(chǎn)bab來說,,它有長度為2的相同前綴后綴ab(相同前綴后綴的長度為k + 1,,k + 1 = 2)。

  • ②求next數(shù)組
    • next 數(shù)組考慮的是除當(dāng)前字符外的最長相同前綴后綴,,所以通過第①步驟求得各個前綴后綴的公共元素的最大長度后,,只要稍作變形即可:將第①步驟中求得的值整體右移一位,然后初值賦為-1,,如下表格所示:

比如對于aba來說,,第3個字符a之前的字符串a(chǎn)b中有長度為0的相同前綴后綴,所以第3個字符a對應(yīng)的next值為0,;而對于abab來說,,第4個字符b之前的字符串a(chǎn)ba中有長度為1的相同前綴后綴a,,所以第4個字符b對應(yīng)的next值為1(相同前綴后綴的長度為k,k = 1),。

  • ③根據(jù)next數(shù)組進(jìn)行匹配
    • 匹配失配,,j = next [j],模式串向右移動的位數(shù)為:j - next[j],。換言之,,當(dāng)模式串的后綴pj-k pj-k+1, ..., pj-1 跟文本串si-k si-k+1, ..., si-1匹配成功,但pj 跟si匹配失敗時(shí),,因?yàn)?span style="font-family: 'Comic Sans MS';">next[j] = k,,相當(dāng)于在不包含pj的模式串中有最大長度為k 的相同前綴后綴,即p0 p1 ...pk-1 = pj-k pj-k+1...pj-1,,故令j = next[j],,從而讓模式串右移j - next[j] 位,使得模式串的前綴p0 p1, ..., pk-1對應(yīng)著文本串 si-k si-k+1, ..., si-1,,而后讓pk 跟si 繼續(xù)匹配,。如下圖所示:

    綜上,KMP的next 數(shù)組相當(dāng)于告訴我們:當(dāng)模式串中的某個字符跟文本串中的某個字符匹配失配時(shí),,模式串下一步應(yīng)該跳到哪個位置,。如模式串中在j 處的字符跟文本串在i 處的字符匹配失配時(shí),下一步用next [j] 處的字符繼續(xù)跟文本串i 處的字符匹配,,相當(dāng)于模式串向右移動 j - next[j] 位,。
    接下來,分別具體解釋上述3個步驟,。

3.3 解釋

3.3.1 尋找最長前綴后綴

    如果給定的模式串是:“ABCDABD”,從左至右遍歷整個模式串,,其各個子串的前綴后綴分別如下表格所示:
    也就是說,,原模式串子串對應(yīng)的各個前綴后綴的公共元素的最大長度表為(下簡稱《最大長度表):

3.3.2 基于《最大長度表》匹配

    因?yàn)槟J酱惺孜部赡軙兄貜?fù)的字符,故可得出下述結(jié)論:
失配時(shí),,模式串向右移動的位數(shù)為:已匹配字符數(shù) - 失配字符的上一位字符所對應(yīng)的最大長度值

    下面,,咱們就結(jié)合之前的《最大長度表》和上述結(jié)論,進(jìn)行字符串的匹配,。如果給定文本串“BBC ABCDAB ABCDABCDABDE”,,和模式串“ABCDABD”,現(xiàn)在要拿模式串去跟文本串匹配,,如下圖所示:

        

  • 1. 因?yàn)槟J酱械淖址鸄跟文本串中的字符B,、B、C,、空格一開始就不匹配,,所以不必考慮結(jié)論,,直接將模式串不斷的右移一位即可,直到模式串中的字符A跟文本串的第5個字符A匹配成功:

  • 2. 繼續(xù)往后匹配,,當(dāng)模式串最后一個字符D跟文本串匹配時(shí)失配,,顯而易見,模式串需要向右移動,。但向右移動多少位呢,?因?yàn)榇藭r(shí)已經(jīng)匹配的字符數(shù)為6個(ABCDAB),然后根據(jù)《最大長度表》可得失配字符D的上一位字符B對應(yīng)的長度值為2,,所以根據(jù)之前的結(jié)論,,可知需要向右移動6 - 2 = 4 位。
  • 3. 模式串向右移動4位后,,發(fā)現(xiàn)C處再度失配,,因?yàn)榇藭r(shí)已經(jīng)匹配了2個字符(AB),且上一位字符B對應(yīng)的最大長度值為0,,所以向右移動:2 - 0 =2 位,。
           
  • 4. A與空格失配,向右移動1 位,。
  • 5. 繼續(xù)比較,,發(fā)現(xiàn)D與C 失配,故向右移動的位數(shù)為:已匹配的字符數(shù)6減去上一位字符B對應(yīng)的最大長度2,,即向右移動6 - 2 = 4 位,。
           
  • 6. 經(jīng)歷第5步后,發(fā)現(xiàn)匹配成功,,過程結(jié)束,。

         

    通過上述匹配過程可以看出,問題的關(guān)鍵就是尋找模式串中最大長度的相同前綴和后綴,,找到了模式串中每個字符之前的前綴和后綴公共部分的最大長度后,,便可基于此匹配。而這個最大長度便正是next 數(shù)組要表達(dá)的含義,。

3.3.3 根據(jù)《最大長度表》求next 數(shù)組

    由上文,,我們已經(jīng)知道,字符串“ABCDABD”各個前綴后綴的最大公共元素長度分別為:

    而且,,根據(jù)這個表可以得出下述結(jié)論

  • 失配時(shí),,模式串向右移動的位數(shù)為:已匹配字符數(shù) - 失配字符的上一位字符所對應(yīng)的最大長度值
    上文利用這個表和結(jié)論進(jìn)行匹配時(shí),我們發(fā)現(xiàn),,當(dāng)匹配到一個字符失配時(shí),,其實(shí)沒必要考慮當(dāng)前失配的字符,更何況我們每次失配時(shí),,都是看的失配字符的上一位字符對應(yīng)的最大長度值,。如此,,便引出了next 數(shù)組。
    給定字符串“ABCDABD”,,可求得它的next 數(shù)組如下:

    把next 數(shù)組跟之前求得的最大長度表對比后,,不難發(fā)現(xiàn),next 數(shù)組相當(dāng)于“最大長度值” 整體向右移動一位,,然后初始值賦為-1,。意識到了這一點(diǎn),你會驚呼原來next 數(shù)組的求解竟然如此簡單:就是找最大對稱長度的前綴后綴,,然后整體右移一位,,初值賦為-1(當(dāng)然,你也可以直接計(jì)算某個字符對應(yīng)的next值,,就是看這個字符之前的字符串中有多大長度的相同前綴后綴),。

    換言之,對于給定的模式串:ABCDABD,,它的最大長度表及next 數(shù)組分別如下:


    根據(jù)最大長度表求出了next 數(shù)組后,,從而有

失配時(shí),模式串向右移動的位數(shù)為:失配字符所在位置 - 失配字符對應(yīng)的next 值

    而后,,你會發(fā)現(xiàn),,無論是基于《最大長度表》的匹配,還是基于next 數(shù)組的匹配,,兩者得出來的向右移動的位數(shù)是一樣的,。為什么呢?因?yàn)椋?/p>

  • 根據(jù)《最大長度表》,,失配時(shí),,模式串向右移動的位數(shù) = 已經(jīng)匹配的字符數(shù) - 失配字符的上一位字符的最大長度值
  • 而根據(jù)《next 數(shù)組》,失配時(shí),,模式串向右移動的位數(shù) = 失配字符的位置 - 失配字符對應(yīng)的next 值
    • 其中,,從0開始計(jì)數(shù)時(shí),失配字符的位置 = 已經(jīng)匹配的字符數(shù)(失配字符不計(jì)數(shù)),,而失配字符對應(yīng)的next 值 = 失配字符的上一位字符的最大長度值,兩相比較,,結(jié)果必然完全一致,。

    所以,你可以把《最大長度表》看做是next 數(shù)組的雛形,,甚至就把它當(dāng)做next 數(shù)組也是可以的,,區(qū)別不過是怎么用的問題。

3.3.4 通過代碼遞推計(jì)算next 數(shù)組

    接下來,,咱們來寫代碼求下next 數(shù)組,。

    基于之前的理解,,可知計(jì)算next 數(shù)組的方法可以采用遞推:

  • 1. 如果對于值k,已有p0 p1, ..., pk-1 = pj-k pj-k+1, ..., pj-1,,相當(dāng)于next[j] = k,。
    • 此意味著什么呢?究其本質(zhì),,next[j] = k 代表p[j] 之前的模式串子串中,,有長度為k 的相同前綴和后綴。有了這個next 數(shù)組,,在KMP匹配中,,當(dāng)模式串中j 處的字符失配時(shí),下一步用next[j]處的字符繼續(xù)跟文本串匹配,,相當(dāng)于模式串向右移動j - next[j] 位,。

舉個例子,如下圖,,根據(jù)模式串“ABCDABD”的next 數(shù)組可知失配位置的字符D對應(yīng)的next 值為2,,代表字符D前有長度為2的相同前綴和后綴(這個相同的前綴后綴即為“AB”),失配后,,模式串需要向右移動j - next [j] = 6 - 2 =4位,。

向右移動4位后,模式串中的字符C繼續(xù)跟文本串匹配,。

  • 2. 下面的問題是:已知next [0, ..., j],,如何求出next [j + 1]呢?

    對于P的前j+1個序列字符:

  • 若p[k] == p[j],,則next[j + 1 ] = next [j] + 1 = k + 1,;
  • 若p[k ] ≠ p[j],如果此時(shí)p[ next[k] ] == p[j ],,則next[ j + 1 ] =  next[k] + 1,,否則繼續(xù)遞歸前綴索引k = next[k],而后重復(fù)此過程,。 相當(dāng)于在字符p[j+1]之前不存在長度為k+1的前綴"p0 p1, …, pk-1 pk"跟后綴“pj-k pj-k+1, …, pj-1 pj"相等,,那么是否可能存在另一個值t+1 < k+1,使得長度更小的前綴 “p0 p1, …, pt-1 pt” 等于長度更小的后綴 “pj-t pj-t+1, …, pj-1 pj” 呢,?如果存在,,那么這個t+1 便是next[ j+1]的值,此相當(dāng)于利用已經(jīng)求得的next 數(shù)組(next [0, ..., k, ..., j])進(jìn)行P串前綴跟P串后綴的匹配,。
   一般的文章或教材可能就此一筆帶過,,但大部分的初學(xué)者可能還是不能很好的理解上述求解next 數(shù)組的原理,故接下來,,我再來著重說明下,。
    如下圖所示,,假定給定模式串ABCDABCE,且已知next [j] = k(相當(dāng)于“p0 pk-1” = “pj-k pj-1” = AB,,可以看出k為2),,現(xiàn)要求next [j + 1]等于多少?因?yàn)閜k = pj = C,,所以next[j + 1] = next[j] + 1 = k + 1(可以看出next[j + 1] = 3),。代表字符E前的模式串中,有長度k+1 的相同前綴后綴,。

    但如果pk != pj 呢,?說明“p0 pk-1 pk”  ≠ “pj-k pj-1 pj”。換言之,,當(dāng)pk != pj后,,字符E前有多大長度的相同前綴后綴呢?很明顯,,因?yàn)镃不同于D,,所以ABC 跟 ABD不相同,即字符E前的模式串沒有長度為k+1的相同前綴后綴,,也就不能再簡單的令:next[j + 1] = next[j] + 1 ,。所以,咱們只能去尋找長度更短一點(diǎn)的相同前綴后綴,。

    結(jié)合上圖來講,,若能在前綴“ p0 pk-1 pk ” 中不斷的遞歸前綴索引k = next [k],找到一個字符pk’ 也為D,,代表pk’ = pj,,且滿足p0 pk'-1 pk' = pj-k' pj-1 pj,則最大相同的前綴后綴長度為k' + 1,,從而next [j + 1] = k’ + 1 = next [k' ] + 1,。否則前綴中沒有D,則代表沒有相同的前綴后綴,,next [j + 1] = 0,。
    為何遞歸前綴索引k = next[k],就能找到長度更小的相同前綴后綴呢,?這又歸根到next數(shù)組的含義,。為了尋找長度相同的前綴后綴,我們拿前綴 p0 pk-1 pk 去跟后綴pj-k pj-1 pj匹配,,如果pk 跟pj 失配,下一步就是用p[next[k]] 去跟pj 繼續(xù)匹配,,如果p[ next[k] ]跟pj還是不匹配,,則下一步用p[ next[ next[k] ] ]去跟pj匹配,。相當(dāng)于在不斷的遞歸k = next[k],直到要么找到長度更小的相同前綴后綴,,要么沒有長度更小的相同前綴后綴,。
    所以,因最終在前綴ABC中沒有找到D,,故E的next 值為0:

模式串的后綴:ABDE
模式串的前綴:ABC
前綴右移兩位:     ABC

    讀到此,,有的讀者可能又有疑問了,那能否舉一個能在前綴中找到字符D的例子呢,?OK,,咱們便來看一個能在前綴中找到字符D的例子,如下圖所示:
    給定模式串DABCDABDE,,我們很順利的求得字符D之前的“DABCDAB”的各個子串的最長相同前綴后綴的長度分別為0 0 0 0 1 2 3,,但當(dāng)遍歷到字符D,要求包括D在內(nèi)的“DABCDABD”最長相同前綴后綴時(shí),,我們發(fā)現(xiàn)pj處的字符D跟pk處的字符C不一樣,,換言之,前綴DABC的最后一個字符C 跟后綴DABD的最后一個字符D不相同,,所以不存在長度為4的相同前綴后綴,。
    怎么辦呢?既然沒有長度為4的相同前綴后綴,,咱們可以尋找長度短點(diǎn)的相同前綴后綴,,最終,因在p0處發(fā)現(xiàn)也有個字符D,,p0 = pj,,所以p[j]對應(yīng)的長度值為1,相當(dāng)于E對應(yīng)的next 值為1,。
    綜上,,可以通過遞推求得next 數(shù)組,代碼如下所示:
  1. void GetNext(char* p,int next[])  
  2. {  
  3.     int pLen = strlen(p);  
  4.     next[0] = -1;  
  5.     int k = -1;  
  6.     int j = 0;  
  7.     while (j < pLen - 1)  
  8.     {  
  9.         //p[k]表示前綴,,p[j]表示后綴  
  10.         if (k == -1 || p[j] == p[k])   
  11.         {  
  12.             ++k;  
  13.             ++j;  
  14.             next[j] = k;  
  15.         }  
  16.         else   
  17.         {  
  18.             k = next[k];  
  19.         }  
  20.     }  
  21. }  

    用代碼重新計(jì)算下“ABCDABD”的next 數(shù)組,,以驗(yàn)證之前通過“最長相同前綴后綴長度值右移一位,然后初值賦為-1”得到的next 數(shù)組是否正確,,計(jì)算結(jié)果如下表格所示:


    從上述表格可以看出,,無論是之前通過“最長相同前綴后綴長度值右移一位,然后初值賦為-1”得到的next 數(shù)組,,還是之后通過代碼遞推計(jì)算求得的next 數(shù)組,,結(jié)果是完全一致的。

3.3.5 基于《next 數(shù)組》匹配

    下面,我們來基于next 數(shù)組進(jìn)行匹配,。

    還是給定文本串“BBC ABCDAB ABCDABCDABDE”,,和模式串“ABCDABD”,現(xiàn)在要拿模式串去跟文本串匹配,,如下圖所示:

    在正式匹配之前,,讓我們來再次回顧下上文2.1節(jié)所述的KMP算法的匹配流程:

  • 假設(shè)現(xiàn)在文本串S匹配到 i 位置,模式串P匹配到 j 位置
    • 如果j = -1,,或者當(dāng)前字符匹配成功(即S[i] == P[j]),,都令i++,j++,,繼續(xù)匹配下一個字符,;
    • 如果j != -1,且當(dāng)前字符匹配失?。碨[i] != P[j]),,則令 i 不變,j = next[j],。此舉意味著失配時(shí),,模式串P相對于文本串S向右移動了j - next [j] 位。
      • 換言之,,當(dāng)匹配失敗時(shí),,模式串向右移動的位數(shù)為:失配字符所在位置 - 失配字符對應(yīng)的next 值,即移動的實(shí)際位數(shù)為:j - next[j],,且此值大于等于1,。
  • 1. 最開始匹配時(shí)
    • P[0]跟S[0]匹配失敗
      • 所以執(zhí)行“如果j != -1,且當(dāng)前字符匹配失?。碨[i] != P[j]),,則令 i 不變,j = next[j]”,,所以j = -1,,故轉(zhuǎn)而執(zhí)行“如果j = -1,或者當(dāng)前字符匹配成功(即S[i] == P[j]),,都令i++,,j++”,得到i = 1,,j = 0,,即P[0]繼續(xù)跟S[1]匹配。
    • P[0]跟S[1]又失配,,j再次等于-1,,i,、j繼續(xù)自增,從而P[0]跟S[2]匹配,。
    • P[0]跟S[2]失配后,,P[0]又跟S[3]匹配。
    • P[0]跟S[3]再失配,,直到P[0]跟S[4]匹配成功,開始執(zhí)行此條指令的后半段:“如果j = -1,,或者當(dāng)前字符匹配成功(即S[i] == P[j]),,都令i++,j++”,。
  • 2. P[1]跟S[5]匹配成功,,P[2]跟S[6]也匹配成功, ...,直到當(dāng)匹配到P[6]處的字符D時(shí)失配(即S[10] != P[6]),,由于P[6]處的D對應(yīng)的next 值為2,,所以下一步用P[2]處的字符C繼續(xù)跟S[10]匹配,相當(dāng)于向右移動:j - next[j] = 6 - 2 =4 位,。

  • 3. 向右移動4位后,,P[2]處的C再次失配,由于C對應(yīng)的next值為0,,所以下一步用P[0]處的字符繼續(xù)跟S[10]匹配,,相當(dāng)于向右移動:j - next[j] = 2 - 0 = 2 位。

  • 4. 移動兩位之后,,A 跟空格不匹配,,模式串后移1 位。

  • 5. P[6]處的D再次失配,,因?yàn)镻[6]對應(yīng)的next值為2,,故下一步用P[2]繼續(xù)跟文本串匹配,相當(dāng)于模式串向右移動 j - next[j] = 6 - 2 = 4 位,。
  • 6. 匹配成功,,過程結(jié)束。

    匹配過程一模一樣,。也從側(cè)面佐證了,,next 數(shù)組確實(shí)是只要將各個最大前綴后綴的公共元素的長度值右移一位,且把初值賦為-1 即可,。

3.3.6 基于《最大長度表》與基于《next 數(shù)組》等價(jià)

    我們已經(jīng)知道,,利用next 數(shù)組進(jìn)行匹配失配時(shí),模式串向右移動 j - next [ j ] 位,,等價(jià)于已匹配字符數(shù) - 失配字符的上一位字符所對應(yīng)的最大長度值,。原因是:

  1. j 從0開始計(jì)數(shù),,那么當(dāng)數(shù)到失配字符時(shí),j 的數(shù)值就是已匹配的字符數(shù),;
  2. 由于next 數(shù)組是由最大長度值表整體向右移動一位(且初值賦為-1)得到的,,那么失配字符的上一位字符所對應(yīng)的最大長度值,即為當(dāng)前失配字符的next 值,。

    但為何本文不直接利用next 數(shù)組進(jìn)行匹配呢,?因?yàn)閚ext 數(shù)組不好求,而一個字符串的前綴后綴的公共元素的最大長度值很容易求,。例如若給定模式串“ababa”,,要你快速口算出其next 數(shù)組,乍一看,,每次求對應(yīng)字符的next值時(shí),,還得把該字符排除之外,然后看該字符之前的字符串中有最大長度為多大的相同前綴后綴,,此過程不夠直接,。而如果讓你求其前綴后綴公共元素的最大長度,則很容易直接得出結(jié)果:0 0 1 2 3,,如下表格所示:

    然后這5個數(shù)字 全部整體右移一位,,且初值賦為-1,即得到其next 數(shù)組:-1 0 0 1 2,。

3.3.7 Next 數(shù)組與有限狀態(tài)自動機(jī)

    next 負(fù)責(zé)把模式串向前移動,,且當(dāng)?shù)趈位不匹配的時(shí)候,用第next[j]位和主串匹配,,就像打了張“表”,。此外,next 也可以看作有限狀態(tài)自動機(jī)的狀態(tài),,在已經(jīng)讀了多少字符的情況下,,失配后,前面讀的若干個字符是有用的,。


3.3.8 Next 數(shù)組的優(yōu)化

   行文至此,,咱們?nèi)媪私饬吮┝ζヅ涞乃悸贰MP算法的原理,、流程,、流程之間的內(nèi)在邏輯聯(lián)系,以及next 數(shù)組的簡單求解(《最大長度表》整體右移一位,,然后初值賦為-1)和代碼求解,,最后基于《next 數(shù)組》的匹配,看似洋洋灑灑,,清晰透徹,,但以上忽略了一個小問題,。

    比如,如果用之前的next 數(shù)組方法求模式串“abab”的next 數(shù)組,,可得其next 數(shù)組為-1 0 0 1(0 0 1 2整體右移一位,,初值賦為-1),當(dāng)它跟下圖中的文本串去匹配的時(shí)候,,發(fā)現(xiàn)b跟c失配,,于是模式串右移j - next[j] = 3 - 1 =2位。

    右移2位后,,b又跟c失配,。事實(shí)上,因?yàn)樵谏弦徊降钠ヅ渲?,已?jīng)得知p[3] = b,與s[3] = c失配,,而右移兩位之后,,讓p[ next[3] ] = p[1] = b 再跟s[3]匹配時(shí),必然失配,。問題出在哪呢,?

    問題出在不該出現(xiàn)p[j] = p[ next[j] ]。為什么呢,?理由是:當(dāng)p[j] != s[i] 時(shí),,下次匹配必然是p[ next [j]] 跟s[i]匹配,如果p[j] = p[ next[j] ],,必然導(dǎo)致后一步匹配失?。ㄒ?yàn)閜[j]已經(jīng)跟s[i]失配,然后你還用跟p[j]等同的值p[next[j]]去跟s[i]匹配,,很顯然,,必然失配),所以不能允許p[j] = p[ next[j ]],。如果出現(xiàn)了p[j] = p[ next[j] ]咋辦呢,?如果出現(xiàn)了,則需要再次遞歸,,即令next[j] = next[ next[j] ],。

    所以,咱們得修改下求next 數(shù)組的代碼,。

  1. //優(yōu)化過后的next 數(shù)組求法  
  2. void GetNextval(char* p, int next[])  
  3. {  
  4.     int pLen = strlen(p);  
  5.     next[0] = -1;  
  6.     int k = -1;  
  7.     int j = 0;  
  8.     while (j < pLen - 1)  
  9.     {  
  10.         //p[k]表示前綴,,p[j]表示后綴    
  11.         if (k == -1 || p[j] == p[k])  
  12.         {  
  13.             ++j;  
  14.             ++k;  
  15.             //較之前next數(shù)組求法,改動在下面4行  
  16.             if (p[j] != p[k])  
  17.                 next[j] = k;   //之前只有這一行  
  18.             else  
  19.                 //因?yàn)椴荒艹霈F(xiàn)p[j] = p[ next[j ]],,所以當(dāng)出現(xiàn)時(shí)需要繼續(xù)遞歸,,k = next[k] = next[next[k]]  
  20.                 next[j] = next[k];  
  21.         }  
  22.         else  
  23.         {  
  24.             k = next[k];  
  25.         }  
  26.     }  
  27. }  

    利用優(yōu)化過后的next 數(shù)組求法,,可知模式串“abab”的新next數(shù)組為:-1 0 -1 0??赡苡行┳x者會問:原始next 數(shù)組是前綴后綴最長公共元素長度值右移一位,, 然后初值賦為-1而得,那么優(yōu)化后的next 數(shù)組如何快速心算出呢,?實(shí)際上,,只要求出了原始next 數(shù)組,便可以根據(jù)原始next 數(shù)組快速求出優(yōu)化后的next 數(shù)組,。還是以abab為例,,如下表格所示:

    

只要出現(xiàn)了p[next[j]] = p[j]的情況,則把next[j]的值再次遞歸,。例如在求模式串“abab”的第2anext值時(shí),,如果是未優(yōu)化的next值的話,第2a對應(yīng)的next值為0,,相當(dāng)于第2a失配時(shí),,下一步匹配模式串會用p[0]處的a再次跟文本串匹配,必然失配,。所以求第2anext值時(shí),,需要再次遞歸:next[2] = next[ next[2] ] = next[0] = -1此后,根據(jù)優(yōu)化后的新next值可知,,2a失配時(shí),,執(zhí)行“如果j = -1,或者當(dāng)前字符匹配成功(即S[i] == P[j]),,都令i++,,j++,繼續(xù)匹配下一個字符,,同理,,第2b對應(yīng)的next值為0

對于優(yōu)化后的next數(shù)組可以發(fā)現(xiàn)一點(diǎn):如果模式串的后綴跟前綴相同,,那么它們的next值也是相同的,,例如模式串abcabc,它的前綴后綴都是abc,,其優(yōu)化后的next數(shù)組為:-1 0 0 -1 0 0,,前綴后綴abcnext值都為-1 0 0

    然后引用下之前3.1節(jié)的KMP代碼:

  1. int KmpSearch(char* s, char* p)  
  2. {  
  3.     int i = 0;  
  4.     int j = 0;  
  5.     int sLen = strlen(s);  
  6.     int pLen = strlen(p);  
  7.     while (i < sLen && j < pLen)  
  8.     {  
  9.         //①如果j = -1,,或者當(dāng)前字符匹配成功(即S[i] == P[j]),,都令i++,j++      
  10.         if (j == -1 || s[i] == p[j])  
  11.         {  
  12.             i++;  
  13.             j++;  
  14.         }  
  15.         else  
  16.         {  
  17.             //②如果j != -1,,且當(dāng)前字符匹配失?。碨[i] != P[j]),,則令 i 不變,j = next[j]      
  18.             //next[j]即為j所對應(yīng)的next值        
  19.             j = next[j];  
  20.         }  
  21.     }  
  22.     if (j == pLen)  
  23.         return i - j;  
  24.     else  
  25.         return -1;  
  26. }  

    接下來,,咱們繼續(xù)拿之前的例子說明,,整個匹配過程如下:

    1. S[3]與P[3]匹配失敗。

    2. S[3]保持不變,,P的下一個匹配位置是P[next[3]],,而next[3]=0,,所以P[next[3]]=P[0]與S[3]匹配。

    3.  由于上一步驟中P[0]與S[3]還是不匹配,。此時(shí)i=3,,j=next [0]=-1,,由于滿足條件j==-1,,所以執(zhí)行“++i, ++j”,,即主串指針下移一個位置,P[0]與S[4]開始匹配,。最后j==pLen,,跳出循環(huán),輸出結(jié)果i - j = 4(即模式串第一次在文本串中出現(xiàn)的位置),,匹配成功,算法結(jié)束,。

3.4 KMP的時(shí)間復(fù)雜度分析

    相信大部分讀者讀完上文之后,,已經(jīng)發(fā)覺其實(shí)理解KMP非常容易,無非是循序漸進(jìn)把握好下面幾點(diǎn):
  1. 如果模式串中存在相同前綴和后綴,,即pj-k pj-k+1, ..., pj-1 = p0 p1, ..., pk-1,,那么在pj跟si失配后,讓模式串的前綴p0 p1...pk-1對應(yīng)著文本串si-k si-k+1...si-1,,而后讓pk跟si繼續(xù)匹配,。
  2. 之前本應(yīng)是pj跟si匹配,結(jié)果失配了,,失配后,,令pk跟si匹配,相當(dāng)于j 變成了k,,模式串向右移動j - k位,。
  3. 因?yàn)閗 的值是可變的,所以我們用next[j]表示j處字符失配后,,下一次匹配模式串應(yīng)該跳到的位置,。換言之,失配前是j,,pj跟si失配時(shí),,用p[ next[j] ]繼續(xù)跟si匹配,,相當(dāng)于j變成了next[j],所以,,j = next[j],,等價(jià)于把模式串向右移動j - next [j] 位。
  4. 而next[j]應(yīng)該等于多少呢,?next[j]的值由j 之前的模式串子串中有多大長度的相同前綴后綴所決定,,如果j 之前的模式串子串中(不含j)有最大長度為k的相同前綴后綴,那么next [j] = k,。
    如之前的圖所示:

    接下來,,咱們來分析下KMP的時(shí)間復(fù)雜度。分析之前,,先來回顧下KMP匹配算法的流程:

KMP的算法流程:

  • 假設(shè)現(xiàn)在文本串S匹配到 i 位置,,模式串P匹配到 j 位置
    • 如果j = -1,或者當(dāng)前字符匹配成功(即S[i] == P[j]),,都令i++,,j++,繼續(xù)匹配下一個字符,;
    • 如果j != -1,,且當(dāng)前字符匹配失敗(即S[i] != P[j]),,則令 i 不變,,j = next[j]。此舉意味著失配時(shí),,模式串P相對于文本串S向右移動了j - next [j] 位,。

    我們發(fā)現(xiàn)如果某個字符匹配成功,模式串首字符的位置保持不動,,僅僅是i++,、j++;如果匹配失配,,i 不變(即 i 不回溯),,模式串會跳過匹配過的next [j]個字符。整個算法最壞的情況是,,當(dāng)模式串首字符位于i - j的位置時(shí)才匹配成功,,算法結(jié)束。
    所以,,如果文本串的長度為n,,模式串的長度為m,那么匹配過程的時(shí)間復(fù)雜度為O(n),算上計(jì)算next的O(m)時(shí)間,,KMP的整體時(shí)間復(fù)雜度為O(m + n),。


4. 擴(kuò)展1:BM算法

    KMP的匹配是從模式串的開頭開始匹配的,而1977年,,德克薩斯大學(xué)的Robert S. Boyer教授和J Strother Moore教授發(fā)明了一種新的字符串匹配算法:Boyer-Moore算法,,簡稱BM算法。該算法從模式串的尾部開始匹配,,且擁有在最壞情況下O(N)的時(shí)間復(fù)雜度,。在實(shí)踐中,比KMP算法的實(shí)際效能高,。

    BM算法定義了兩個規(guī)則:

  • 壞字符規(guī)則:當(dāng)文本串中的某個字符跟模式串的某個字符不匹配時(shí),,我們稱文本串中的這個失配字符為壞字符,此時(shí)模式串需要向右移動,,移動的位數(shù) = 壞字符在模式串中的位置 - 壞字符在模式串中最右出現(xiàn)的位置,。此外,如果"壞字符"不包含在模式串之中,,則最右出現(xiàn)位置為-1,。
  • 好后綴規(guī)則:當(dāng)字符失配時(shí),后移位數(shù) = 好后綴在模式串中的位置 - 好后綴在模式串上一次出現(xiàn)的位置,,且如果好后綴在模式串中沒有再次出現(xiàn),,則為-1。

    下面舉例說明BM算法,。例如,,給定文本串“HERE IS A SIMPLE EXAMPLE”,和模式串“EXAMPLE”,,現(xiàn)要查找模式串是否在文本串中,如果存在,,返回模式串在文本串中的位置,。

    1. 首先,"文本串"與"模式串"頭部對齊,,從尾部開始比較,。"S"與"E"不匹配。這時(shí),,"S"就被稱為"壞字符"(bad character),,即不匹配的字符,它對應(yīng)著模式串的第6位,。且"S"不包含在模式串"EXAMPLE"之中(相當(dāng)于最右出現(xiàn)位置是-1),,這意味著可以把模式串后移6-(-1)=7位,從而直接移到"S"的后一位,。


    2. 依然從尾部開始比較,,發(fā)現(xiàn)"P"與"E"不匹配,,所以"P"是"壞字符"。但是,,"P"包含在模式串"EXAMPLE"之中,。因?yàn)椤癙”這個“壞字符”對應(yīng)著模式串的第6位(從0開始編號),且在模式串中的最右出現(xiàn)位置為4,,所以,,將模式串后移6-4=2位,兩個"P"對齊,。


    3. 依次比較,,得到 “MPLE”匹配,稱為"好后綴"(good suffix),,即所有尾部匹配的字符串,。注意,"MPLE",、"PLE",、"LE"、"E"都是好后綴,。

    4. 發(fā)現(xiàn)“I”與“A”不匹配:“I”是壞字符,。如果是根據(jù)壞字符規(guī)則,此時(shí)模式串應(yīng)該后移2-(-1)=3位,。問題是,,有沒有更優(yōu)的移法?

    5. 更優(yōu)的移法是利用好后綴規(guī)則:當(dāng)字符失配時(shí),,后移位數(shù) = 好后綴在模式串中的位置 - 好后綴在模式串中上一次出現(xiàn)的位置,,且如果好后綴在模式串中沒有再次出現(xiàn),則為-1,。
    所有的“好后綴”(MPLE,、PLE、LE,、E)之中,,只有“E”在“EXAMPLE”的頭部出現(xiàn),所以后移6-0=6位,。
    可以看出,,“壞字符規(guī)則”只能移3位,“好后綴規(guī)則”可以移6位,。每次后移這兩個規(guī)則之中的較大值,。這兩個規(guī)則的移動位數(shù),只與模式串有關(guān),與原文本串無關(guān),。

    6. 繼續(xù)從尾部開始比較,,“P”與“E”不匹配,因此“P”是“壞字符”,,根據(jù)“壞字符規(guī)則”,,后移 6 - 4 = 2位。因?yàn)槭亲詈笠晃痪褪?,尚未獲得好后綴,。

    由上可知,BM算法不僅效率高,,而且構(gòu)思巧妙,,容易理解。


5. 擴(kuò)展2:Sunday算法

    上文中,,我們已經(jīng)介紹了KMP算法和BM算法,,這兩個算法在最壞情況下均具有線性的查找時(shí)間。但實(shí)際上,,KMP算法并不比最簡單的c庫函數(shù)strstr()快多少,,而BM算法雖然通常比KMP算法快,但BM算法也還不是現(xiàn)有字符串查找算法中最快的算法,,本文最后再介紹一種比BM算法更快的查找算法即Sunday算法,。

    Sunday算法由Daniel M.Sunday在1990年提出,它的思想跟BM算法很相似:

  • 只不過Sunday算法是從前往后匹配,,在匹配失敗時(shí)關(guān)注的是文本串中參加匹配的最末位字符的下一位字符,。
    • 如果該字符沒有在模式串中出現(xiàn)則直接跳過,即移動位數(shù) = 匹配串長度 + 1,;
    • 否則,,其移動位數(shù) = 模式串中最右端的該字符到末尾的距離+1。

    下面舉個例子說明下Sunday算法,。假定現(xiàn)在要在文本串"substring searching algorithm"中查找模式串"search",。

    1. 剛開始時(shí),把模式串與文本串左邊對齊:
substring searching algorithm
search
^
    2. 結(jié)果發(fā)現(xiàn)在第2個字符處發(fā)現(xiàn)不匹配,,不匹配時(shí)關(guān)注文本串中參加匹配的最末位字符的下一位字符,,即標(biāo)粗的字符 i,,因?yàn)槟J酱畇earch中并不存在i,,所以模式串直接跳過一大片,向右移動位數(shù) = 匹配串長度 + 1 = 6 + 1 = 7,,從 i 之后的那個字符(即字符n)開始下一步的匹配,,如下圖:

substring searching algorithm
    search
    ^
    3. 結(jié)果第一個字符就不匹配,再看文本串中參加匹配的最末位字符的下一位字符,是'r',,它出現(xiàn)在模式串中的倒數(shù)第3位,,于是把模式串向右移動3位(r 到模式串末尾的距離 + 1 = 2 + 1 =3),使兩個'r'對齊,,如下:
substring searching algorithm
      search
       ^

    4. 匹配成功,。

    回顧整個過程,我們只移動了兩次模式串就找到了匹配位置,,緣于Sunday算法每一步的移動量都比較大,,效率很高。完,。


6. 參考文獻(xiàn)

  1. 算法導(dǎo)論》的第十二章:字符串匹配,;
  2. 本文中模式串“ABCDABD”的部分圖來自于此文:http://www./blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html
  3. 本文3.3.7節(jié)中有限狀態(tài)自動機(jī)的圖由微博網(wǎng)友@龔陸安 繪制:http:///i/NEiz,;
  4. 北京7月暑假班鄒博半小時(shí)KMP視頻:http://v.youku.com/v_show/id_XNzQzMjQ1OTYw.html,;
  5. 北京7月暑假班鄒博第二次課的PPT:http://yun.baidu.com/s/1mgFmw7u
  6. 理解KMP 的9張PPT:http://weibo.com/1580904460/BeCCYrKz3#_rnd1405957424876,;
  7. 詳解KMP算法(多圖):http://www.cnblogs.com/yjiyjige/p/3263858.html,;
  8. 本文第4部分的BM算法參考自此文:http://www./blog/2013/05/boyer-moore_string_search_algorithm.html
  9. http://youlvconglin.blog.163.com/blog/static/5232042010530101020857,;
  10. 數(shù)據(jù)結(jié)構(gòu) 第二版》,,嚴(yán)蔚敏 & 吳偉民編著;
  11. http://blog.csdn.net/v_JULY_v/article/details/6545192,;
  12. http://blog.csdn.net/v_JULY_v/article/details/6111565,;
  13. Sunday算法的原理與實(shí)現(xiàn):http://blog./uid-22237530-id-1781825.html
  14. 模式匹配之Sunday算法:http://blog.csdn.net/sunnianzhong/article/details/8820123,;
  15. 一篇KMP的英文介紹:http://www.inf./lang/algorithmen/pattern/kmpen.htm,。


7. 后記    

    對之前混亂的文章給廣大讀者帶來的困擾表示致歉,對重新寫就后的本文即將給讀者帶來的清晰表示欣慰,。希望大部分的初學(xué)者,,甚至少部分的非計(jì)算機(jī)專業(yè)讀者也能看懂此文。有任何問題,,歡迎隨時(shí)批評指正,,thanks。

    July,、二零一四年八月二十二日晚九點(diǎn),。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn),。請注意甄別內(nèi)容中的聯(lián)系方式,、誘導(dǎo)購買等信息,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多