字符串匹配算法:KMP與BM

字符串匹配算法

樸素思想(暴力)

任何一種問題,咱們都習慣先寫出暴力作法,而後再去想如何優化。對於字符串匹配也是如此,話很少說,直接上代碼,暴力遍歷比較。c++

for (int i = 0; i < n; i ++ )
{
    bool flag = true;
    for (int j = 0; j < n-m+1; j ++ )
    {
        if (text[i + j ] != pattern[j])
        {
            flag=false;
            break;
        }
    }
    if(flag)
        return i;
}

雖說暴力作法一直是效率低下的代言詞,但對於日常使用來講,這種作法就已經夠用了。由於在實際開發中,大部分狀況下的主串和模式串的長度都不會太長。而且它思路清晰簡單,出問題也好修復。實際上通常 string 的查找函數就是這種作法。算法

可是也不能就會這一招就阿尼陀佛,萬事大吉了。總有須要優化的時候,計算機之因此在現代社會有着舉足輕重的地位,就在於人在不斷思考,並優化它的執行方式。缺乏了人的思考,它只是一坨笨的不能再笨的鐵而已。數組

KMP

樸素作法雖然簡單,但主串與模式串的指針都要來回移動。咱們去書上找東西通常都是一眼掃過去,有沒有什麼辦法可讓主串的指針不往回走,下降比較的趟數?函數

在上面這個例子中,很明顯更快的作法是在第一次匹配失敗以後,跳過第二次匹配,進行第三次匹配。也就是說,應該向後移動兩格而不是一格。KMP的思想就在於利用已知信息,不把"搜索位置"移回已經比較過的位置,繼續向後移。性能

咱們能夠利用模式串自身的特色,利用已經匹配過的結果來減小枚舉過程,跳過不可能成功的比較,加速匹配過程。畫個抽象點的圖:優化

在主串指針不日後退的前提下, 咱們的模式串指針最多回退到哪裏就能夠繼續進行匹配呢?3d

如上圖,回退以後的字符串確定是要能與主串匹配成功的。也就是說它與以前的模式串有着交集。從圖中能夠看到,P串的後綴與P'的前綴相同。那個這個問題:在已有N個匹配成功的結果下,第N+1個字符匹配失敗時,模式串應該退回到第?個匹配成功的結果下?也就轉換成了:模式串的各個字串中,能使先後綴相等的最大長度是多少?指針

咱們用next數組來存儲回退到的下標code

雖說1那裏應該等於0,但其實這時候已經退無可退了。因此應該退出循環,讓主串指針向前走一位,開始新一輪比較。blog

利用這個next數組,好比咱們比較到下標3的時候發現不匹配,不要緊,咱們退一步。退一步以後就到了下標1(回退看的next應該是已經匹配成功的那個,也就是看下標2存的是幾),若是這時候還不匹配,那咱們就會退回到-1,也就是說,這輪匹配失敗了,主串能夠往前走了。

看看代碼就清楚了

int strStr(string haystack, string needle)
{
	if (!needle.size()) return 0;
	if (!haystack.size()) return -1;
	vector<int> next(needle.size());
	next[0] = -1;
	//求next數組
	for (int i = 1, j = -1; i < needle.size(); i++)
	{
		while (j >= 0 && needle[i]!=needle[j+1])
		{
			//不匹配就退一步看看
			j = next[j];
		}
		if (needle[i] == needle[j + 1])
		{
			//匹配成功就繼續日後走
			//看看還能不能匹配成功
			j++;
		}
		next[i] = j;
	}
	//開始匹配
	for (int i = 0, j = -1; i < haystack.size(); i++)
	{
		while (j != -1 && haystack[i] != needle[j+1])
		{
			//不匹配咱就回退
			j = next[j];
		}
		if (haystack[i] == needle[j+1])
		{
			j++;
		}
		if (j == needle.size() - 1)
		{
			return i-j;
		}
	}
	return -1;
}

BM

雖然KMP比較出名,但其實只是由於它比較難懂而已。在效率上有不少的算法都比它要好。如今介紹的BM算法,其效率就要比KMP好上3到4倍。

BM算法包含兩部分:壞字符規則和好後綴規則

壞字符規則

BM算法與咱們日常接觸的字符串比較方法不一樣,它是按模式串從大到小的順序,倒着比的。這樣作也是有好處的,起碼直觀上是這樣感受的。就像作算數選擇題,出卷老師爲了讓你花的時間久一點,故意把正確答案放到C跟D上。因此聰明點的作法應該是先算C跟D。這跟這個比較方法有點相似。

考慮下面這張圖:

別忘了咱們是從模式串最大的開始日後匹配,因此這裏先比較了C和D。這個時候,有意思的來了,這個 D 在模式串中就沒有出現過,是一個壞字符,有它在的字串可能不匹配。

惹不起還躲不起嗎?逃之夭夭,模式串直接移動五位,從新匹配。一下移動這麼多,是否是特別的爽?

但你千萬不要覺得只有沒在模式串中出現過的才叫壞字符,實際上這個只是從後往前第一個不匹配的字符。一旦發生不匹配,壞字符規則的作法是模式串指針繼續往回走,找到第一個與其匹配的字符中止,而後再繼續新一輪的匹配。

也就是說,移動的距離等於:當前模式串的下標(Si) - 往回走找到的第一個與當前不匹配字符匹配的下標(Xi)。若是沒有找到,則減數爲-1。

這麼一想,好像用壞字符規則就萬事大吉了。但由於實際代碼中,咱們不會每次不匹配都會往前找,那樣太耗費時間,取而代之的是使用散列表紀錄不一樣字符在模式串中「最後出現的位置」,並非 Si 的位置往前查找的第一個位置,因此可能會出現 Xi 大於 Si 的狀況。好比上圖的例子,主串"ABABDABABCAB",模式串"ABABC"當從左開始數的一個B不匹配時,找到的A的下標是最後一次在模式串中出現的下標(也就是最後一個a的位置,比A大)。這時候,模式串非但不往前滑動,還回退了。爲了解決這個問題,咱們須要好後綴來幫忙。(實際上,兩個規則均可以獨立使用,若是壞字符你是往回遍歷而不是保存在散列表裏面的話)

好後綴規則

當遇到上圖的狀況時,咱們依然能夠用壞字符規則來移動,但此次讓咱們來看看好後綴是如何工做的?

咱們把在主串中已經匹配成功的字符串用 u標記,如今要作的是找到模式串中與其匹配的u*若是找到了,那模式串就滑動到使得u*與u對齊的位置。若是不匹配,那麼逃之夭夭,直接移動一個模式串長度的位置。雖然一次移動那麼可能是很爽,但這樣作有可能錯過能夠匹配的狀況。

實際上這裏應該跟KMP同樣,若是模式串中前綴與好後綴的後綴相同,那麼就移動相應位置使其匹配。

雙劍合璧

兩個規則你都知道了,而且他們均可以獨立使用,那麼到底該選哪個呢?

能夠參考的建議

  • 若是處理的是字符集很大的字符串匹配問題,那麼壞字符規則對內存的消耗會比較多。由於兩個規則是獨立的,因此能夠考慮僅使用好後綴
  • 若是同時使用兩個規則,滑動的大小應該是兩個規則中最大的那一個,這樣能夠避免負數的產生

代碼實現

如前面所說,壞字符若是每次都在模式串中遍歷的話,會對性能形成影響。那麼有沒有什麼高效的辦法代替遍歷呢?

咱們能夠將模式串中的每一個字符及其下標都存到散列表中,這樣就能快速的找到壞字符在模式串的對應下標了。

先打好壞字符規則

private:
    static const int SIZE = 256;
    
    //求bc
    vector<int> getBC(string pattern)
    {
        vector<int> bc(SIZE,-1);
        for (int i = 0; i < pattern.size(); i++)
        {
            int ascii = (int)pattern[i];
            bc[ascii] = i;
        }
        return bc;
    }

    int strStr(string haystack, string needle)
    {
        if (needle.size() == 0) return 0;

        vector<int> bc(SIZE);
        bc = getBC(needle);
        
        int i = 0;
        while (i <= haystack.size() - needle.size())
        {
            int j;
            //別忘了,BM是從後往前匹配哦
            for (j = needle.size() - 1; j >= 0; j--)
            {
                //不等於咱就要去找滑動位置了
                if (haystack[i + j] != needle[j])
                    break;
            }
            if (j < 0)
            {
                //這是匹配成功!咱們是從後往前的!!!
                return i;
            }
            //找到模式串中最近的壞字符
            i = i + j - bc[(int)haystack[i + j]];
        }
        return -1;
    }

好後綴稍微麻煩一點,與KMP相似,咱們用一個int數組suffix來保存。其下標表示後綴子串的長度。讓咱們來明確一下要乾的事:在好後綴的後綴子串中,查找最長的、能跟模式串前綴子串匹配的後綴子串。因此存儲的應該是在模式串中跟好後綴u相匹配的子串u*的起始下標值。

等等!咱們好像漏了什麼?在模式串中查找跟好後綴匹配的另外一個子串,而且在好後綴的後綴子串中,查找最長能跟模式串前綴子串匹配的後綴子串。

suffix數組只能完成前半段話,那前綴子串的問題該如何解決呢?咱們只能是再開一個bool數組prefix 來作這件事了。

求這兩個數組的方法也十分討巧,紀錄公共後綴子串u*的起始下標爲j,若是j==0,說明是前綴字串(由於已經走到盡頭了),prefix爲true。代碼實現是下面這樣:

//好後綴
    void genGS(string pattern)
    {
        int len = pattern.size();
        for (int i = 0; i < len; i++)
        {
            suffix.push_back(-1);
            prefix.push_back(false);
        }
        for (int i = 0; i < len - 1; i++)
        {
            int j = i;
            //公共後綴u*的長度
            int k = 0;
            while (j >= 0 && pattern[j] == pattern[len - 1 - k])
            {
                j--;
                k++;
                //保存的是在pattern中的起始下標
                suffix[k] = j + 1;
            }
            if (j == -1)
                prefix[k] = true;
        }
    }

如今讓咱們來把兩個規則一塊兒用在字符串匹配上

private:
    static const int SIZE = 256;
    vector<int> bc;
    vector<int> suffix;
    vector<bool> prefix;

    //壞字符
    void getBC(string pattern)
    {
        for (int i = 0; i < SIZE; i++)
        {
            bc.push_back(-1);
        }
        for (int i = 0; i < pattern.size(); i++)
        {
            int ascii = (int)pattern[i];
            bc[ascii] = i;
        }
    }
    //好後綴
    void genGS(string pattern)
    {
        int len = pattern.size();
        for (int i = 0; i < len; i++)
        {
            suffix.push_back(-1);
            prefix.push_back(false);
        }
        for (int i = 0; i < len - 1; i++)
        {
            int j = i;
            //公共後綴u*的長度
            int k = 0;
            while (j >= 0 && pattern[j] == pattern[len - 1 - k])
            {
                j--;
                k++;
                //保存的是在pattern中的起始下標
                suffix[k] = j + 1;
            }
            if (j == -1)
                prefix[k] = true;
        }
    }

    int moveByGS(int j, int len)
    {
        //好後綴長度
        int k = len - 1 - j;
        if (suffix[k] != -1)
            return j - suffix[k] + 1;
        for (int i = j + 2; i < len; i++)
        {
            if (prefix[len = i])
                return i;
        }
        return len;
    }

    int strStr(string haystack, string needle)
    {
        if (needle.size() == 0) return 0;

        getBC(needle);
        genGS(needle);
        int i = 0;
        while (i <= haystack.size() - needle.size())
        {
            int j;
            //別忘了,BM是從後往前匹配哦
            for (j = needle.size() - 1; j >= 0; j--)
            {
                //不等於咱就要去找滑動位置了
                if (haystack[i + j] != needle[j])
                    break;
            }
            if (j < 0)
            {
                //這是匹配成功!咱們是從後往前的!!!
                return i;
            }
            //找到模式串中最近的壞字符
            int x =  j - bc[(int)haystack[i + j]];
            
            int y = 0;
            //若是有好後綴
            if (j < needle.size() - 1)
            {
                y = moveByGS(j,needle.size());
            }
            i = i + max(x, y);
        }
        return -1;
    }

相關文章
相關標籤/搜索