《大話數據結構》第五章 串


第五章 串

定義:串是由零個或多個字符串組成的有限序列,又叫字符串。

子串和主串:串中任意個數的連續字符組成的子序列稱爲該串的子串,相應的包含子串的串稱爲主串。
字串在主串中的位置就是子串的第一個字符在主串中的序號。

串的比較:串的比較是通過串的字符之間的編碼來進行的。

串的抽象數據類型


串的存儲結構

串的順序存儲結構:用一組地址連續的存儲單元來存儲串中的字符序列。
可能會在第一個或最後一個元素中存放長度,也可能在最後添加\0

固定長度的缺陷:固定後可能出現上溢提示或者截斷,這時用"堆"來處理更好,堆可以用C語言的malloc()和free()來管理。

串的鏈式存儲結構:結構中的每個數據元素都是一個字符,但是隻放一個會很浪費,因此一個系欸但也可以存放多個字符,如果最後一個結點沒有被佔滿的話,可以用#補全。

鏈式缺陷:一個結點存放多少個字符才合適變得很重要,這會直接影響串的處理效率,需要根據實際情況做出選擇。

兩種對比:鏈式除了連接串與串操作時有一定方便之外,總的來說不如順序存儲靈活,性能也不如順序存儲結構好。


樸素的模式匹配算法

串的模式匹配:像尋找一個單詞在一篇文章中的定位問題,這種子串的定位操作通常稱爲串的模式匹配。

子串尋找例題

// 返回子串T在主串S中第pos個字符之後的位置,不存在則返回0
// T非空,1 <= pos <= StrLength(S)
// 設字符串的長度存放在第一個位置中
int Index(String S, String T, int pos)
{
    int i = pos;    // 匹配開始的位置
    int j = 1;    // 用來記錄子串中的下標
    while (i<=S[0] && j<=T[0])
    {
        if (S[i] == T[j])
        {
            i++;
            j++;
        }
        else    // 出現不相等的情況了
        {
            i = i - j + 2;    // 回到剛纔的位置的後面
            j = 1;    // 重置T的位置
        }
    }
    if (j > T[0])    // 說明走完了T全部,匹配
        return i - T[0];
    else
        return 0;
}

時間複雜度分析

  • 最好情況下,每次子串第一個就能檢測出是否匹配,然後根據等概率原則,平均是(n+m)/2次查找,時間複雜度爲O[n+m]。
  • 最壞情況下,每次要找到子串最後一個才知道是否匹配,要執行(nm+1)m(n-m+1)*m次。時間複雜度爲O[(nm+1)m(n-m+1)*m]

性能:低效。


KMP模式匹配算法

算法原理
如果串中元素都不相同,那麼可以跳過比較的長度,如下圖可省略2-5步驟:

如果有相同的,那麼相同的,可以在斷掉的前面的那個相同除,如在45處直接取結果,省了一部分比較,其中2-5步驟是多餘的:

優點:KMP可以避免不必要的回溯

子串中的新位置選擇問題
新建一個數組next,每個元素的下標 j 是當判斷出現不同時,子串結束的位置,存放的內容爲子串下次開始的位置;j的值主要取決於子串中是否有重複的問題。

根據經驗如果前後綴一個字符相等,k值是2,兩個字符k值是3,n個相等k就是n+1

思路:
應該是先分析情況,得出上面的規律,然後根據規律寫代碼。
標記重複位置,主數組的位置是一直往後移的,動的是被匹配的數組,不用每次都從頭開始,根據被匹配數組的重複度和當前比較的位置,來確定被匹配數組下次從哪裏開始

計算規則
只管首尾,中間不管:

前綴和後綴可重疊,但是不可完全相同:

案例


KMP代碼實現
思路:j代表前綴,通過next[j]來回溯;i表示後綴,一直在往後加,不會考慮後綴和中間內容的關係。

//  計算子串T的next數組
void get_next(String T, int *next)
{
    int i, j;
    i = 1;
    j = 0;
    next[1] = 0;
    while (i < T[0])    // T[0]存放的是長度
    {
        // j=0的時候,就是連續不等回溯重置的時候
        // 利用j=0來推動i和j的移動,因爲j=1表示串的第一個位置,所以不會出現漏的情況
        // j=0的意思就是先動,取值了再比較
        // 內部有++,此時設置的,是上個位置應指向的點,j=0也就是不等時的指向
        // if中也有判斷,如果有相等,則j的值可後移,next也會記錄新位置
        // 出現新的不等後j還會繼續回溯
        if (j==0 || T[i]==T[j])
        {
            ++i;    // 後綴判斷一直移動
            ++j;    // 前綴和後綴相似個數計算
            next[i] = j;    // k = n+1;
        }
        else
            j = next[j];    // 出現不同就回溯,計算新的後綴的相似度
    }
}

// 獲取位置
int Index_KMP(String S, String T, int pos)
{
    int i = pos;
    int j = 1;    // T中下標,第一個位置放了長度
    int next[255];    // 存放j的設置
    get_next(T, next);
    while (i<=S[0] && j<=T[0])
    {
        // 這邊也是憑藉j=0和相等來推動數組移動
        // j=0是出現了不等,回溯後取值比較,不動就沒法從頭開始
        if (j==0 || S[i]==T[i])    // 如果相似,就繼續走,比樸素方法多了j=0判斷
        {
            i++;
            j++;
        }
        else
        {
            // 此處i不動,等待T的新值來比較
            // 如果此時i已經過了重複的部分,那麼重複的部分就不用比較了,從重複後開始就行,即next[j]返回的位置
            j = next[j];    // 設定j;
        }
    }
    // j>T[0],此時j=length+1,因爲之前有個++的尾巴
    // 也說明T被遍歷完了
    if (j > T[0])
        return i - T[0];
    else
        return 0;
}

時間複雜度分析
設T的長度爲m,前面的獲取next數組的複雜度爲O[m];
設S的長度爲n,由於i沒有回溯,所以while循環的複雜度爲O[n],所以整個算法的複雜度爲O[n+m]。


KMP模式算法改進

KMP算法存在的問題
下圖中的2-4步都是多餘的,所以在這個2,3,4,5位置元素都相同的情況下,可以直接用next[1]的值取代這幾個next的值,所以可以改良next的求解過程

核心思想
當不等的前面有很多重複的時候,直接跳到最前面,而不是一次一次地往前跳。

void get_nextval(String T, int *nextval)
{
    int i, j;
    i = 1;
    j = 0;
    nextval[1] = 0;
    while (i<T[0])
    {
        if (j==0 || T[i]==T[j])
        {
            ++i;
            ++j;
            // 下面是改動的地方
            // 如果當前字符和前綴字符不同的話,不相同就不需要往前跳
            if (T[i] != T[j])
                nextval[i] = j;
            else
                // 如果i和j位置的元素相同,就使用前面那一個的next,其實也就是直接用的最前面的第一個開始相似的next
                // 因爲之前的也用的前面的,所有這裏就一直是最初的那個值
                nextval[i] = nextval[j];
        }
        else
            j = nextval[j];
    }
}

新版案例

3、4位置(ab)因爲和前面(1、2位置的ab)重複,所以使用的前面的值;
5位置的a和3位置的a重複,還是用的3的值,也就是用的1的值;
6位置開始出現不同,6的a和4的b不同,所以nextval中的值和原來的一樣;
因爲6開始中斷了,所以要從頭開始了,7位置和2的b相比較,不同,還是取原版的值;
8位置和2的b相同,所以用2的b的位置;


JAVA實現KMP相關

package String.Base;

import java.util.Scanner;

public class BaseMatch {
    public static void main(String[] args) {
//        Scanner scanner = new Scanner(System.in);
        System.out.println("輸入數組1");
        String str1 = "jojostarstarl";
        System.out.println(str1);
//        String str1 = scanner.next();
        System.out.println("輸入數組2");
//        String str2 = scanner.next();
        String str2 = "starl";
        System.out.println(str1);
        System.out.println("正確結果應爲:");
        System.out.println(EasyMatch(str1.toCharArray(), str2.toCharArray()));
        System.out.println("KMP結果尾:");
        System.out.println(KMP(str1.toCharArray(), str2.toCharArray()));


    }


    private static Object EasyMatch(char[] str1, char[] str2) {
        int len1 = str1.length;
        int len2 = str2.length;
        int count = 0;

        for (int i = 0; i < len1; i++) {
            for (int j = 0; j < len2; j++) {
                if (str1[i + j] == str2[j]) {
                    count++;
                    if (count == len2)
                        return i + 1;
                }
            }
            count = 0;
        }

        return "不匹配";
    }

    private static Object KMP(char[] str1, char[] str2) {
        int len1 = str1.length;
        int len2 = str2.length;
        int[] next = new int[len2];
        // 獲取next數組
        next = getNextVal(str2);
        int i = -1;
        int j = -1;
        // 兩個都不能越界
        while (i < len1 && j < len2) {
            // 通過兩個條件推動數組移動
            if (j == -1 || str1[i] == str2[j]) {
                ++i;
                ++j;
            } else {
                j = next[j];
            }
        }
        // 判斷是否比較了str2的全部
        if (j >= len2) {
            // 如果不加1則輸出的是下標位置,下標位置是比實際位置小1的
            return i - len2 + 1;
        }
        return "沒找到";
    }

    private static int[] getNext(char[] str) {
        int len = str.length;
        int i = 0;  // 後移
        int j = -1;  // 回溯
        int[] next = new int[len];
        next[0] = -1;
        // 即算完最後的i++之後i=len,停止循環
        while (i < len - 1) {
            if (j == -1 || str[i] == str[j]) {
                i++;
                j++;
                // 回溯位置和相同數相關
                next[i] = j;
            } else {
                j = next[j];    // 不相同,要回溯
            }
        }

        return next;
    }

    private static int[] getNextVal(char[] str) {
        int len = str.length;
        int i = 0;
        int j = -1;
        int[] next = new int[len];
        next[0] = -1;
        while (i < len - 1) {
            if (j == -1 || str[i] == str[j]) {
                ++j;
                ++i;
                // 不相等了,就不用管重複問題了
                if (str[i] != str[j])
                    next[i] = j;
                else
                    // 相等就一直回溯到最前的第一個同元素位置
                    next[i] = next[j];
            } else {
                j = next[j];
            }
        }
        return next;
    }
}

思維導圖

在這裏插入圖片描述

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