1. 程式人生 > >簡單易懂的KMP,NEXT陣列,BF演算法(例項講解)!!!

簡單易懂的KMP,NEXT陣列,BF演算法(例項講解)!!!

去了360面試,問了一個關於KMP的知識點,呀,完全忘了啊,太不應該了,然後就打算看看這個KMP,,,

看了好多關於KMP演算法的書籍和資料,總感覺沒有說的很清楚,為什麼會產生next陣列,為什麼給出了那麼簡短的程式,沒有一個過程,而有的帖子雖然next及其字串匹配說的很清楚,但是推理的一些過程相當複雜,比較抽象,今天在這裡簡單的提一下我的理解,儘可能的把這個過程講的簡單,容易理解

從模式匹配之初:我們先是研究的是BF演算法,鑑於我們經常行的需要回溯,總是做一些無用功,為了提高演算法的時間度和空間度,引入了next陣列(至於為什麼提高時間,下面會提到),也就是採用next陣列,我們不需要再去回溯了,可以直接比較,這也就是KMP演算法了

所以說:從BF到KMP,只是簡單的嫌棄BF演算法,花費的時間空間太長太大了而已,一切都在進步!!!

串的模式匹配或者串匹配:

子串的定位操作是找子串在主串中從POS個字元後首次出現的位置

一般將主串S稱為目標串,子串T稱為模式串

BF演算法:

Brute-Force,也稱為蠻力(暴力)匹配演算法

1,從主串S的第pos個字元開始,和模式串T的第一個字元進行比較,

2,若相等,則逐個比較後續的字元;不然,就回溯到主串的第POS+1個字元開始,繼續和模式串T的第一個字元進行比較,反覆執行步驟2,知道模式串T中的每一個字元都和主串相等(返回當前主串S匹配上的第一個字元)或者找完主串S(POS位置後的所有字元(返回0)),

程式1:

#include <stdio.h>                                                                                                                              
#include <string.h>
#include <malloc.h>

int BFmode(char *s, char *t, int pos)
{
        int flag1,flag;
        int i,j;
        int slength, tlength;
        slength = strlen(s);
        tlength = strlen(t);

        for(i = pos; i < slength; i++)
        {
                flag1 = i,flag = 0;
                for(j = 0; j < tlength; j++)
                {
                        if(s[i] == t[j] && i < slength)
                        {
                                flag ++;
                                i++;    
                        }
                        else
                                break;  
                }
                if(flag == tlength)
                {
                        return flag + 1;
                }
                i = flag1;
        }

        return 0;
}

 int main()
{
        char s[50];   //目標串
        char t[20];   //模式串同時也是子串
        int pos;
        scanf("%s", s);
        scanf("%s", t);
        scanf("%d", &pos);
        pos = BFmode(s,t, pos);

        printf("POS:%d\n", pos);

        return 0;
}           

編寫完了之後,總覺得怪怪的,程式不應該這麼複雜啊,

程式2:

int BFmode(char *s, char *t, int pos)
{
        int flag1,flag;
        int i,j = 0;
        int slength, tlength;
        slength = strlen(s);
        tlength = strlen(t);

        for(i = pos; i < slength && j < tlength; i++)
        {

                if(s[i] == t[j])
                {
                        j++;    
                        if(j == tlength)
                                return i - j + 2;       
                }
                else
                {       
                        i = i - j;
                        j = 0;  
                }
        }

        return 0;
}

我們還可以將程式優化一下:畢竟程式的重點是(時間和空間),

演算法比較簡單,但是在最壞的情況下,演算法的時間複雜度為O(n×m),n,m分別為主串和模式串的長度。從第一個程式就可以很容易的看出來了,主要時間都耗費在失配後的比較位置有回溯,主要都給拉回來,因而比較次數過多

為了時間都不浪費在主串的回溯上面,那麼我們引入了next陣列

什麼是next陣列呢?

1,首先:next陣列是針對於子串而言的

      求子串相對的字首和字尾,看是否相等(相等即退出),最大的相等個數即就是next的值

          

           如上:上面的數字代表字元在字元陣列中的下標

      next[0]:         "a"     (無字首,無後綴)                             next[0] = 0

      next[1]:         "ab"   ("a"  !=  "b")                                      next[1] = 0

      next[2]:         "aba" ("ab"!="ab", "a" == "a")                     next[2] = 1

      next[3]:         "abab"

                           ("aba" != "bab" , "ab" == "ab")                   next[3] = 2

      next[4]:         "ababc"

                           ("abab" != "babc", "aba" != "abc", "ab" != "bc", "a" != "c")

                                                                                             next[4] = 0

        

      這樣求到的next串沒有問題,程式碼也沒有問題

      程式:

#include <stdio.h>                                                                                                                              
#include <string.h>

int next[30] = {0};
int flag;

int get_2_next(char *p, int current, int flag1) //P為所求的字串,current為當前的位置,flag1為函式中的移動標誌位
{
        int i,j;
        char p1[30];   
        char p2[30];                             //臨時陣列,便於比較

        if(current == 0)
                next[0] = 0;                     //因為位置為0的時候,既沒有字首,也沒有後綴
        while(flag1 <= current)
        {
                for(i = 0; i <= current - flag1; i++)
                {       
                        p1[i] = p[i];
                }
                p1[i] = '\0';
                
                for(i = flag1; i <= current; i++)
                {
                        p2[i - flag1] = p[i];   
                }
                p2[i - flag1] = '\0';
                
                if(strcmp(p1, p2) == 0)
                {
                        return strlen(p1);      
                }

                flag1 ++;
        }

        return 0;
}

void get_1_next(char *p, int plength)            //p為所求的字串,plength為所求字串的長度
{
        int i;
        for(i = 0; i < plength; i++)
        {
                flag = 1;
                next[i] = get_2_next(p, i, flag);
        }
}

int main()
{
        int i;
        char p[30];
        int plength;

        scanf("%s", p); 
        plength = strlen(p);
        get_1_next(p, plength); 

        for(i = 0; i < plength; i++)
        {
                printf("%c:%d\n", p[i], next[i]);
        }
        printf("\n");
}             
 


執行結果:

嗯嗯,對,這是我初步的想法,這樣求得,但是,但是,但是,翻了翻資料,感覺在時間複雜度和空間複雜度方面太low了,別人的時間空間都是極少的,一定有優化的辦法的,一定有

經過長時間的思考,終於想明白了,

哈哈,也就是說,上面的寫法做了很多的無用功,

在求後續的next串的時候,完全沒有必要去從最大的字首去求,可以借用之前求到的next

如:串s  =  "ababc"          求next[4]

        由於我們已經知道了,next[3]    =   2,("aba" != "bab" , "ab" == "ab"),所以,我們非常的沒有必要去做求

       (“abab”是不是等於“babc”)等等這些判斷,因為從之前的next[1] = 0(可以知道“a”不等於“b”,也就是s[1]和s[0]直接的比較),

        所以當前我們要處理的就是:

        1,當前的字元(s[4]  = 'c')是不是等於next[3]也就是(s[2]),即就是判斷s[2]是不是等於s[4],如果相等:next[4]  =  next[3] + 1

        2,如果不相等的話,那麼我們需要比較的就是s[4]是不是和s[next[next[3]]],由於next[3] = 2,那就是next[2],

              next[2]等於1,那麼就是s[4]和s[1]進行比較,判斷是否相等,如果相等next[4] = next[2] + 1並且退出;

        3,如果不想等的話,那麼需要繼續執行類似於我們上面的步驟,當帶比較的字元為0的時候,那麼就退出

這裡:我們是通過next[3]簡易的求了一下next[4],當然,這裡完全可以通過遞迴或者迴圈來求出後面的next陣列,因為next[0]是知道的(next[0]沒有字首,也沒有後綴,所以說:next[0] = 0),由此,我們就可以求出next[1,2,3,4,....]等等

為了便於理解,我沒有采用i,j的說法,覺得那樣的話,可能會越說越糊塗

下面是一個簡單的程式:

#include <stdio.h>                                                                                                                              
#include <string.h>

int next[30] = {0};

void get_1_next(char *p, int plength)            //p為所求的字串,plength為所求字串的長度
{
        int i, j,a;
        next[0] = 0; //沒有字首,也沒有後綴
        for(i = 1; i < plength; i++)
        {
                j = i;
                while(1)
                {
                        if(p[next[j - 1]] == p[i])      
                        {
                                next[i] = next[j - 1] + 1;
                                break;
                        }
                        else if(next[j - 1] == 0)
                        {
                                next[i] = 0;
                                break;  
                        }
                        else
                        {
                                a = next[j - 1];   //這兩行是為了說明 j = j- 1;(便於迴圈)
                                j = a + 1;
                        }
                }
        }
}

int main()
{
        int i;
        char p[30];
        int plength;

        scanf("%s", p); 
        plength = strlen(p);
        get_1_next(p, plength); 

        for(i = 0; i < plength; i++)
        {
                printf("%c:%d\n", p[i], next[i]);
        }
        printf("\n");
} 
執行結果:


通過程式的驗證,證明了我們的猜想,也就是說:我們上面說的完全正確,可以大大的削減時間和空間來達到我們的目的,不需要多餘佔用額外的儲存空間,不需要多次迴圈,不需要做一些無畏的事情

可是,可是,可是???有人到這裡就會問了,這個和我們所說的KMP有什麼關係啊,這個又和我們上面所說的不用回溯又有什麼關係啊???哈哈哈,這可算是問到重點上了,,,

下面我們討論一下回溯的問題,以及什麼是KMP演算法

    
1,先看這個問題(給定一個子串,一個主串,求模式匹配的過程時)

      可以知道的是:

      子串的next陣列是:0   0    1    2   0

       主串是從0開始的

       我們開始匹配:

        1,s[0]與t[0]進行比較,如果不想等,那麼主串往後移動,如果相等,那麼兩者一起移動,對於當前的情況是,兩者不想等,那麼主串後移

         

        2,s[1]與t[0]進行比較,不相等的話,繼續主串後移,相等的話,一起移動, 直到兩者字元不相等,或者完全匹配後退出,當前的情況是:

       

                可以直觀的看到:s[4] != t[3],這裡就呼叫了next,為什麼要呼叫next呢???因為前面的部分字元“aba”在主             串和子串中已經完全匹配,為了不用回溯和浪費,也為了保證能保留剩餘部分的匹配,故引入了next

                由於已經比較到了t[3],所以,我們求next[2]的值,然後比較t[next[2]]和s[4](即就是s[4]和t[1]),可以看到

                s[4] != t[1] ,然後繼續如上的步驟,比較s[4]和t[next[1]](即就是比較s[4]和s[0]),繼續比較:

                1,兩者相同,還是以上的步驟,兩者整體移動

                2,不同,將主串後移

                當前的情況滿足條件1,故s[4] == t[0],繼續兩者後移

        3,當前的位置如下:

             

              可知:s[8] != t[4],嗯嗯,如上,繼續呼叫next陣列:

              比較s[8]和t[next[3]],即就是比較(s[8]和t[2]),判斷兩者是否相等,可知,對於上圖,兩者是相等的,所以此時(i = 8 , j = 2),(這裡就明顯的體現了不用回溯的直接價值啊,哈哈哈),然後,同步後移

          4,當前的狀態如下:

               

                同理,還是上面的判斷步驟,就可以找到了(完全匹配完成了),然後返回匹配後的第一個值

           程式碼如下:

#include <stdio.h>                                                                                                                              
#include <string.h>

int next[30] = {0};

void get_1_next(char *t, int tlength)            //p為所求的字串,plength為所求字串的長度
{
        int i, j,a;
        next[0] = 0; //沒有字首,也沒有後綴
        for(i = 1; i < tlength; i++)
        {
                j = i;
                while(1)
                {
                        if(t[next[j - 1]] == t[i])      
                        {
                                next[i] = next[j - 1] + 1;
                                break;
                        }
                        else if(next[j - 1] == 0)
                        {
                                next[i] = 0;
                                break;  
                        }
                        else
                        {
                                a = next[j - 1];   //這兩行是為了說明 j = j- 1;(便於迴圈)
                                j = a + 1;
                        }
                }
        }
}

int get_position(char *s, int slength, char *t, int tlength)
{
        int i = 0,j = 0;
        int flag = 0, a;
        while(i < slength && j < tlength)
        {
                if(s[i] == t[j])
                {
                        i++; j++;       
                        flag ++;
                }
                else
                {
                        if(flag == 0)
                                i++;
                        else
                        {
                                while(1)
                                {
                                        if(t[next[j - 1]] == s[i])      
                                        {
                                                j = next[j - 1];
                                                i++;
                                                j++;
                                                break;  
                                        }
                                        else if(next[j - 1] == 0)
                                        {
                                                j = 0; flag = 0; i++;
                                                break;
                                        }
                                        else
                                        {
                                                a = next[j - 1]; //這兩行是為了說明 j = j- 1;(便於迴圈)
                                                j = a + 1;                                      
                                        }
                                }       
                        }           
                }
        }

        return i - tlength;
}

int main()
{
        int position,i;
        char s[30], t[30];      //s為主串,t為子串,返回子串在主串中完全匹配(或者不匹配)後的位置POSITION
        int slength, tlength;

        scanf("%s", s);
        scanf("%s", t);
        slength = strlen(s);    
        tlength = strlen(t);
        get_1_next(t, tlength); 
        
        position = get_position(s,slength,t,tlength);
        printf("%d\n", position);
} 

執行結果:


上面的這些也就是所謂的KMP演算法了

Knuth-Morris-Pratt演算法(簡稱KMP),是模式匹配中的經典演算法,和BF演算法相比,KMP演算法的不同點是消除BF演算法中主串S指標回溯的情況,從而完成的模式匹配,這樣的結果使得演算法的時間複雜度為O(m+n)

這就是所謂的KMP演算法了!!!

相關推薦

簡單易懂KMPNEXT陣列BF演算法例項講解

去了360面試,問了一個關於KMP的知識點,呀,完全忘了啊,太不應該了,然後就打算看看這個KMP,,, 看了好多關於KMP演算法的書籍和資料,總感覺沒有說的很清楚,為什麼會產生next陣列,為什麼給出了那麼簡短的程式,沒有一個過程,而有的帖子雖然next及其字串匹配說的很清

一個簡單易懂且實用的JQuery分頁外掛jquery.page詳解

在你的.html檔案中引入相關的檔案: 注意:jquery.min.js需在你引入jquery.page.js之前引入。 <link rel="stylesheet" type="text/c

swift:CoreData簡單入門(增加、查詢、修改、刪除)詳細講解

CoreData 是 一個可以用來管理 物件生命週期、物件層級、資料持久化儲存 的蘋果官方框架。 下面來看看如何用swift語言來使用CoreData呢? 1 開啟Xcode,選擇Xcode project 2 選擇開發平臺及模板應用,這裡選擇ios single vie

【HDU - 1867 】A + B for you againKMPnext陣列應用

題幹: Generally speaking, there are a lot of problems about strings processing. Now you encounter another such problem. If you get two strings, such

Java實現八皇后問題陣列遞迴演算法簡單易懂

八皇后問題 要將八個皇后放在棋盤上,任何兩個皇后都不能互相攻擊。即沒有兩個皇后是在同一行、同一列或者同一對角上。 典型的八皇后問題,使用Java寫的演算法,演算法雖比較簡單,但難免會有新手會犯疏漏和錯誤,希望大家可以批評指正,共同交流進步! 程式碼

PTA 陣列迴圈左移 20 分 本題要求實現一個對陣列進行迴圈左移的簡單函式:一個數組a中存有n>0個整數在不允許使用另外陣列的前提下將每個整數迴圈向左移m≥0個位置即將a中的

陣列迴圈左移 (20 分) 本題要求實現一個對陣列進行迴圈左移的簡單函式:一個數組a中存有n(>0)個整數,在不允許使用另外陣列的前提下,將每個整數迴圈向左移m(≥0)個位置,即將a中的資料由(a​0​​a​1​​⋯a​n−1​​)變換為(a​m​​⋯a​n−

超級簡單易懂的二進位制原碼反碼補碼

以123和-123為例:[123]原碼:01111011。  反碼:01111011。  補碼:01111011。[-123]原碼:11111011。 反碼:10000100。  補碼:10000101。正數的原碼,反碼,補碼均相等。負數的反碼求法:        1.符號位

poj3107Godfather樹形dpnext陣列樹的重心

Description Last years Chicago was full of gangster fights and strange murders. The chief of the police got really tired of all th

POJ 2752 Seek the Name, Seek the Fame(KMPnext陣列)

【連結】http://poj.org/problem?id=2752 【題意】給個字串,求這個串所有字首與字尾相同的所有綴長度 【思路】 其實kmp裡的next這個微妙的陣列本身就是答案了,至於為什麼,我是用以前不知道哪裡弄來的的kmp模板試了一下看出來的【喂

用 prompt 輸入字串建立陣列找出陣列中最大值問題闡述與解決。

用 prompt 輸入字串建立陣列。用三種方法找出陣列中最大值。問題闡述與解決。 實現目標:輸入一組數,並找出這組數中最大的值。 採用的方法: 雙 for迴圈,列出從小到大(從大到小)順序。 單 for 迴圈,一遍迴圈找出最大值。 使用 Math.max。

KMPnext陣列定義

KMP演算法的Next陣列詳解 轉載請註明來源,幷包含相關連結。 網上有很多講解KMP演算法的部落格,我就不浪費時間再寫一份了。直接推薦一個當初我入門時看的部落格吧: http://www.cnblogs.com/yjiyjige/p/3263858.html 這位同學用詳細的

shell 指令碼的一些常用命令 set export shell陣列esac teetime

1. set Linux set命令用來設定 shell ,設定使用shell的執行方式。 引數說明 -a  標示已修改的變數,以供輸出至環境變數。 -b  使被中止的後臺程式立刻回報執行狀態。 -C  轉向所產生的檔案無法覆蓋已存在的檔案。 -d  She

利用Python進行socket網路程式設計實現樹莓派與Ubuntu16.04之間的簡單的網路聊天

標題 目標: 採用socket程式設計,完成兩個樹莓派之間、或者樹莓派與Ubuntu系統之間的網路文字通訊(或聊天) 分析: 首先我們需要了解socket程式設計的原理以及它是怎麼實現的。 Socket的英文原義是“孔”或“插座”。作為BSD UNIX的程序通訊機制,取後一種意思。

在整型有序陣列中查詢想要的數字 找到了返回下標找不到返回-1.折半查詢

#define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> int BinarySearch(int a[], int key, int len) { int ret = -1;//找

全域性陣列區域性陣列靜態陣列的定義與初始化區別

測試 例項: 定義陣列,未初始化 #define LEN1 5 static int array_static_glogal[LEN1];//定義靜態全域性陣列,未初始化陣列成員 int array_glogal[LEN1];

js中陣列常用邏輯演算法從大到小從小到大排序去重等問題

從小到大: // 從小到大順序排序 minSort (arr) { var min for (var i = 0; i < arr.length; i++) { for (var j = i; j < arr.le

關於陣列指標指標陣列雙重陣列二維矩陣字串陣列雙重字元指標的理解

1、二維陣列     int array[10][10]; 函式宣告: void fun(int a[][10])    函式呼叫:fun(array);   訪問: 一般使用a[i][j]來訪問陣列中的元素 2、指標陣列     int *array[10

字元陣列字串陣列字串的相互轉換

怎麼把字元陣列轉換成字串? 例如:char[] a={'a','b','c'}; 最常用的方法是通過toString方法: Arrays.toString(a);可是這樣得到的字串是[a,b,c],而不是“abc”。 那麼怎麼變成“abc”呢? 下面的兩種方法最

隨機10個100到200之間的整數將這些數放入陣列列印陣列再使用 3種排序。

package com.paixu; public class Test_maopao { /** * @param args */ public static void main(String[] args) { // TODO Auto-generat

Kmpnext陣列含義

湊個字數 這篇分析了Kmp中next陣列到底是用來幹什麼的。文章假定大家已經對字串匹配演算法具有初步瞭解。但是對kmp中的next不是很懂。如果想要研究更多。可以出門左轉https://blog.csdn.net/qq_41105401/article/det