1. 程式人生 > >第四章學習小結 串的模式匹配 解題心得體會

第四章學習小結 串的模式匹配 解題心得體會

next ret ood 體會 pac 做的 目標 ostream ext

串的模式匹配 解題心得體會

關於串,模式匹配是其一個很重要的問題。針對這個問題,書上講了兩種模式匹配的算法,即BF算法和KMP算法,下面針對這兩種算法的實現談談我的心得。

一、BF算法的探索

【錯誤代碼1】

#include<iostream>
#include<string.h>
using namespace std;
 typedef struct{
    char ch[1000002];
    int length;
 }SString;


void  Index_BF(SString S, SString T, int pos){ 
    int i,j;
    i=pos;
    j=1;
    while ( i <= S.length && j <= T.length ) {
       if ( S.ch[i]==T.ch[j] ) { ++i;  ++j; }
       else { i = i-j+2;  j=1; }
     if ( j>T.length )   
     {
        cout<< i-T.length<<endl;
     }
     else{ 
     cout<<"0"<<endl;
    } 
}
}
int main()
{
    SString S,T;
    char s[1000002]={0},t[1000002]={0};
    cin>>s>>t;
    strcpy(S.ch,s);
    strcpy(T.ch,t);
    Index_BF(S, T , 1);
    return 0;
}

分析:編譯輕松通過,但是怎麽都不能輸入進去。參考其他同學的博客,發現我的這個問題有人也遇到過,原因在於在主函數中定義了兩個長度為100W的數組,程序根本跑不動。
解決:參考博客發現有兩種途徑解決數組過大問題:①將數組定義為全局變量 ②動態分配數組
針對我這一題的具體情況,因為我是要用到strcpy函數來copy數組的內容的,所以第一種方式顯然更適合。


【錯誤代碼2】

#include<iostream>
#include<string.h>
using namespace std;
 typedef struct{
    char ch[1000002];
    int length;
 }SString;
    char s[1000002]={0},t[1000002]={0};
    SString S,T;
    
void  Index_BF(SString S, SString T, int pos){ 
    int i,j;
    i=pos;
    j=1;
    cout<<"smooth"<<endl;
    while ( i <= S.length && j <= T.length ) {
       if ( S.ch[i]==T.ch[j] ) { 
       ++i; 
       ++j; }
       else {
        i = i-j+2;
         j=1; }
     if ( j>T.length )   
     {
        cout<< i-T.length<<endl;
     }
     else{ 
     cout<<"0"<<endl;
    } 
}
}
int main()
{
    cin>>s>>t;
    cout<<"good"<<endl;
    strcpy(S.ch,s);
    cout<<"OK"<<endl;
    strcpy(T.ch,t);
    cout<<"OK"<<endl;
    Index_BF(S, T , 1);
    return 0;
}

分析:這一次程序是順利運行完了,就是沒有想要的結果出來。
解決:為了查找程序在哪裏斷掉了,我在一些步驟之後設置了一些輸出,比如cout<<"good"。此法說明我的問題出在BF算法上。經過仔仔細細的檢查,發現BF算法中我打while循環的時候掉了一個括號,加上去就正常了。
接下來需要解決位置和下標的問題,我現在輸出的還是下標,不是題目要求的位置。思考過後發現解決的途徑有兩種:改數組;改算法。這裏我選擇的是改算法的方法。


【正確程序1】

#include<iostream>
using namespace std;
#include<string.h>

//采用靜態順序存儲結構(定長) 
typedef struct{
    char ch[1000001];   //存儲串的一維數組 
    int length;     //串的長度 
}SString;
 
 SString S,T; 
 
 char s[1000001];
 char t[1000001];
 
//BF算法
//查找 模式T 在 主串S 中第pos個字符開始第一次出現的位置,並返回 
//若不存在,則返回0 (T非空,1<=pos<=S.length) 
int Index_BF(SString S,SString T,int pos)  
{               
    int i,j;
    i=pos;
    j=0;
    while(i<=S.length-1 && j<=T.length-1)
    {
        if(S.ch[i]==T.ch[j]){   //從各自的第一位開始比較,如果相同,比較下一位 
            ++i;
            ++j;
        }
        else {//如果不同,主串指針回到 上次開始比較時的字符 的下一個字符,
              //模式回到第一個字符,重新開始比較 
            i=i-j+1;
            j=0;
        } 
    }
    if(j>T.length-1)        //匹配成功 
        return i-T.length+1;//主串指針位置往回退模式長度個單位,就回到了該模式在主串中第一次出現的位置 
    else            //匹配失敗 
        return 0;   //返回0(順序存儲的字符串是從下標為1的數組分量開始存儲的,下標為0的分量閑置不用) 
} 
 
//主函數 
int main()
{
    cin>>s>>t;
    strcpy(S.ch,s);
    strcpy(T.ch,t);
    S.length=strlen(S.ch);
    T.length=strlen(T.ch);
    cout<<Index_BF(S,T,0)<<endl;;
    return 0;
}

現在把程序放到PTA上面跑,得到了15分,唯一錯誤的一個點就是超時。但這個是BF算法無法解決的了,所以我決定一鼓作氣把他改成KMP算法。

一、KMP算法的探索

在真正著手寫代碼前,我花了很久閱讀課本,也想了很久,終於弄明白了這個算法的核心思想。以下是我的思路:

①BF究竟是哪裏麻煩了?


設想一種情況:主串是abcab... 子串是abcac... 如果按照BF算法我們知道,一旦b和c不匹配了,接下來比較的就是主串的第二個字符b和子串的第一個字符a,不匹配時主串和子串都右移。直到子串的首位字符a與主串的a對齊之前,我們做的都是無用功。這就是從一個具體的例子看出來的麻煩之處。


②如果說KMP可以跳過這些“無用功”,那什麽情況下可以跳?這是一個特殊情況,真的能上升到一個算法的高度嗎?


為了解決我以上的疑慮,我決定用抽象的模型進行說明。


設主串Si(大家不要把它看成矽元素的元素符號啦)與模式tj失配……這句話暗含(S1S2……Si-1與t1t2……tj-1是匹配的)
這裏我們將主串Si固定死,讓他與模式中的tk匹配(因為不知道,所以設k這個未知數,也就是說這時候模式要從j跳到k)即:

Si-k+1...Si-1 =t1...tk-1


我們由第一步的匹配就已經得到:

Si-k+1...Si-1 = tj-k+1...tj-1


把這兩個式子聯立,得到:
t1...tk-1 = tj-k+1...tj-1

③求k


以上我們得到了一個很重要的關系式,即t1...tk-1 = tj-k+1...tj-1,它隱含了我們想要知道的k,也就是子串該跳到哪一個地方這一信息。


觀察式子,我們首先考慮一些邊邊角角的問題,比如k=1,j=1這些情況。


1)k=1 這時式子變成了t1...t0 = tj...tj-1 這種情況說明子串不符合要求,沒有那個相等的部分。順便也解決了我們第二問中的疑惑,其實KMP算法的優化是有條件的,要求子串與主串匹配的那部分有“相等的部分”。
這時候的處理辦法也只有老老實實跳到t1與Si比較。模式啊模式!你若是墮落,KMP也救不了你!

2)j=1 其實就是模式的第一位就與主串Si不匹配。那還猶豫啥?直接把模式向右移一個唄。

④怎麽用算法實現?

其實整個大框架在BF算法的基礎上來說不用怎麽改,只需要把回溯的那塊兒稍微修改一下,即改成主串i不回溯,模式跳到k位置。

但是現在說來說去我們還是只有一串關系式,還有兩個邊界的情況,k到底怎麽表示?對解決這個問題還是一臉懵逼。

閱讀課本發現他使用一個next[j]函數來表示子串下一個回溯的位置。 其實想一想next[j]僅與模式有關,和主串半毛錢關系也沒有。從已知的關系式t1...tk-1 = tj-k+1...tj-1入手,此時next[j] = k。我們求一求next[j+1]?這時候產生兩種情況需要討論:

1)若tk = tj 那麽t1...tk = tj-k+1...tj 也就是next[j+1] = k+1 , 亦即next[j+1] = next[j] +1

2)若tk!=tj 是不是有一種似曾相識的感覺?對了,這又是一次模式匹配,只不過此時的模式既充當了主串又充當模式。所以我們要把模式中的第next[j]個字符和主串中的第j個字符對齊進行比較。這種情況可以利用函數的遞歸調用,讓j=next[j]。


【正確程序2】

#include<iostream>
using namespace std;
#include<string.h>

//采用靜態順序存儲結構(定長) 
typedef struct{
    char ch[1000002];   //存儲串的一維數組 
    int length;     //串的長度 
}SString;
 
 SString S,T; 
 
 char s[1000002];
 char t[1000002];
 int nex[1000002];
 
//KMP算法
//查找 模式T 在 主串S 中第pos個字符開始第一次出現的位置,並返回 
//若不存在,則返回0 (T非空,1<=pos<=S.length) 
int Index_KMP(SString S,SString T,int next[])  
{               
    int i,j;
    i=j=0;
    while(i<=S.length-1 && j<=T.length-1)
    {
        if(j==-1||S.ch[i]==T.ch[j]){    //從各自的第一位開始比較,如果相同,比較下一位 
            ++i;
            ++j;
        }
        else {
            j=next[j];
        } 
    }
    if(j>T.length-1)        //匹配成功 
        return i-T.length+1;// 
    else            //匹配失敗 
        return 0;
} 
 
 void get_next(SString T,int next[]){
    int i=0;
    next[0]=-1;
    int j=-1;
    while(i<T.length-1){
        if(j==-1||T.ch[i]==T.ch[j]){
            ++i;
            ++j;
            next[i]=j;
         }
         else {
            j=next[j];
         }
     }
 }
//主函數 
int main()
{
    cin>>s>>t;
    strcpy(S.ch,s);
    strcpy(T.ch,t);
    S.length=strlen(S.ch);
    T.length=strlen(T.ch);
    get_next(T,nex);
    cout<<Index_KMP(S,T,nex)<<endl;
    return 0;
}

下一周的計劃之一是抽空做做前面幾章的實踐2,我感覺代碼是越打越熟練的,所以要多加練習。而且上次小測成績不是特別理想,一個很大的問題就是我有時候太依賴書本了,所以我的第二個目標是要培養自己不看書本打代碼的能力,這也是對思維完備性的鍛煉。

第四章學習小結 串的模式匹配 解題心得體會