數據結構:串

一、串的定義

串(string)是由零個或多個字符組成的有限序列,又名叫字符串

一般記爲s=”a1a2……an”(n≥0),其中s是字符串的名稱,用雙引號括起來的字符序列是串的值,引號不屬於串的內容。ai(1 ≤ i ≤ n)可以是字母、數字或其他字符,i就是該字符在串中位置。串中的字符數目n稱爲串的長度,定義中談到“有限”是指長度n是一個有限的數值。兩個字符的串稱爲空串(null string),它的長度爲零,可以直接用雙引號“”表示。所謂序列,說明串的相鄰字符之間具有前驅後繼的關係。

其他一些概念。
空格串,是隻包含空格的串。注意它和空串的區別,空格串是有內容有長度的,而且可以不止一個空格。

子串與主串,串中任意個數的連續字符組成的子序列稱爲該串的字串,相應地,包含子串的串稱爲主串。

子串在主串中的位置就是子串第一個字符在主串中的序號。

二、串的比較

對於兩個串不相等時,如何判定它們的大小呢。我們這樣定義:
給定兩個串:s = “a1a2…….an”,t = “b1,b2……bm”,當滿足以下條件之一時,s < t。

  1. 存在某個k < min(m,n),使得ai = bi(i = 1, 2 , ….., k -1),ak < bk
    假如當s = “happen” ,t = “happy”,因爲兩串的前4個字符均相同,而兩串的第5個字母(k值),字母e的ASCII碼時101,而字母y的ASCII碼時121,顯然e < y,所以s < t。

  2. n < m,且ai = bi (i = 1, 2, …… , n)
    例如當 s = “hap”,t = “happy”,就有s < t,因爲t比s多處了兩個字母。

三、串的抽象數據類型

串的邏輯結構和線性表很相似,不同之處在於串針對的是字符集,也就是串中的元素都是字符

因此,對於串的基本操作與線性表所有很大差別的。線性表更關注的是單個元素的操作,比如查找一個元素,插入或刪除一個元素,但串中更多的是查找子串的位置、得到指定位置的子串、替換子串等操作

ADT 串(string)

Data
    串中元素僅由一個字符組成,相鄰元素具有前驅和後繼關係。
Operation
    StrAssign(T,*chars):生成一個其值等於字符串常量chars的串T。
    strCopy(T,S):串S存在,由串S複製得串T
    ClearString(S):串S存在,將串清空
    StringEmpty(S):若串S爲空,返回true,否則false
    StrLength(S):返回串S的元素個數,即串的長度
    StrCompare(S,T):若S>T,返回值大於0;若S=T,返回0,若S<T,返回值0
    Concat(T,S1,S2):用串T返回S1和S2連接而成的新串
    SubString(Sub,S,pos,len):串S存在,1≤pos≤StrLength(S)
                且0≤len≤StrLength(S)-pos+1,用Sub
                返回值S的第pos個字符起長度爲len的子串
    Index(S,T,pos):串S和串T存在,T是非空串1≤ pos ≤StrLength(S)
                若主串S中存在和串T相同的子串,則返回它在主串S中
                第pos個字符後第一個出現的位置,否則返回0
    Replace(S,T,V):串S、T和V存在,T是非空串。用V替換主串中出現的
                所有與T相等的不重疊的子串
    StrInset(S,pos,T):串S和T存在,1 ≤ pos ≤StrLength(S) + 1
                在串S的第pos個字符之前插入串T
    StrDelete(S,pos,len):串S存在,1≤pos≤StrLengthS-len+1
                從串S中刪除第pos個字符起長度爲len的子串。

對於不同的高級語言,對串的基本操作會有不同的定義方法,所以同學們用某個語言操作字符串時,需要先查看它的參考手冊關於字符串的基本操作有哪些。
不過還好,不同語言除方法名稱外,操作的實質其實都是類似的。

我們來看一個操作Index的實現算法。

//T爲非空串。若主串S中第pos個字符之後存在於T相等的子串
//則返回第一個這樣的子串在S中的位置,否則返回0
int Index(String S,String T,int pos)
{
    int n,m,i;
    String sub;
    if(pos > 0)
    {
        n = StrLength(S);//得到主串S的長度
        m = StrLength(T);
        i = pos;
        while(i <= n-m+1)
        {
            SubString(sub,S,i,m);//取主串第i個位置,m長度的子串
            if(StrCompare(sub,T) != 0)//如果兩串不相等
                i++;
            else                      //如果相等 
                return i;             //返回i值
        }
    }
    return 0;//若無子串與T相等,返回0
}

當中用到了strLength,SubString,StrCompare等基本操作結構來實現。

四、串的存儲結構

串的存儲結構與線性表相同,分爲兩種。

1.串的順序存儲結構

串的順序存儲結構是用一組地址連續的存儲單元來存儲串中的字符序列。按照預定義的大小,爲每個定義的串變量分配一個固定長度的存儲區。一般是用定長數組來定義

既然是定長數組,就存在一個預定義的最大串長度,一般可以將實際的串長度值保存在數組的0下表位置,有的書中也會定義存儲在數組的最後一個下標位置。但有些編程語言,它規定串值後面加一個不計入串長度的結束標記字符,比如“\0”,這個時候,你想要知道此時的串長度,就需要遍歷計算一下才知道了,起始這還是需要佔用一個空間的。

剛纔將的串的順序存儲結構其實是有問題的,因爲字符串的操作,比如量串的連接Concat,新串的插入StrInsert,以及字符串的替換Replace,都有可能使得串序列的長度超過了數值的長度MAXSIZE。顯然,此時無論是上溢提示報錯,還是對多出來的字符串截尾,都不是什麼好辦法。但字符串操作中,這樣情況比比皆是。

於是對於字符串的順序存儲,有一些變化,串值的存儲空間可在程序執行過程中動態分配而得。比如在計算機中存在一個自由存儲區,叫做“堆”。這個堆可由C語言的動態分配函數malloc()和free()來管理

2.串的鏈式存儲結構

對於串的鏈式存儲結構,與線性表是相似的,但由於串結構的特殊性,結構中的每個元素是一個字符,如果也簡單的應用鏈表存儲串值,一個結點對應一個字符,就會存在很大的空間浪費。因此,一個結點可以存放一個字符,也可以考慮存放多個字符,最後一個結點若是未被佔滿,可以用“#”或其他非串值字符不全。

當然這裏一個結點存放多少個字符變得合適顯得很重要,這會直接影響串的處理的效率

但串的鏈式存儲結構除了在連接字符串與串操作時有一定方便之外總的來說不如順序存儲靈活、性能也不如順序存儲的好

五、樸素的模式匹配算法

子串的定位操作通常稱爲串的模式匹配。應該算是串中最重要的操作之一。

假設我們要從下面的主串S = “goodgoogle”中,找到T=”google”這個子串的位置。

最簡單的樸素想法就是,對主串的每一個字符作爲子串開頭,與要匹配的字符串進行匹配。對主串作大循環,每個字符開頭做T長度的小循環,知道匹配完成爲止。

前面我們已經用串的其他操作實現了模式匹配的算法Index。現在考慮不用其他操作,而是隻用基本的數組來實現同樣的算法。

//返回子串T在主串S中第pos個字符之後的位置。若不存在,則函數返回0
//T非空,0≤pos≤strLength(S)
int Index(String S,String T,int pos)
{
    int i = 0;
    int j = 0;
    int SLen = 0;
    int TLen = 0;
    while(s[i] != '\0')//計算S長度
    {
        SLen++;
    }
    while(T[i] != '\0')//子串T的長度
    {
        TLen++;
    }
    int i = pos;
    while(i <= SLen - TLen && j <= TLen)
    {
        if(S[i] == T[i])
        {
            i++;
            j++;
        }
        else
        {
            i = i - j + 1;//匹配失敗回到開始的地方的下一個位置
            j = 0;//子串回到原來位置
        }
    }
    if(j > TLen)
    {
        return i - TLen;
    }
    else
        return 0;
}

分析一下,最好的情況時什麼?那就是一開始就成功,此時時間複雜度爲O(1)。
稍差一點,如果像”abcdedgood”中查找”good”。那麼時間複雜度就是O(n + m),n爲主串長度,m爲要匹配的子串長度。根據等概原則,平均是(n + m) / 2次查找,時間複雜度爲O(n+m)。

那麼最壞的情況又是什麼?就是每次不成功的匹配都發生在串T的最後一個字符,比如主串S = 0000000000000000000000000000000000001,而要匹配的子串爲T = 0000001。前者是有49個“0”和1個“1”,後者是9個“0”和1個“1”。這樣等於前40個位置要循環40 * 10次。知道第41個位置,這裏也要匹配10次。
因此最壞時間複雜度爲O[(n-m+1)*m]

但是這種情況在計算機中很常見,因爲在計算機中,處理的都是二進制位的0和1的串,一個字符ASCII碼也可以看成是8位的二進制01串。所以在計算機中,模式匹配操作使用剛纔的算法顯得太低效了。

六、KMP模式匹配算法

D.E.Knumth、J.H.Morris和V.R.Pratt發現一個模式匹配算法,可以大大簡化避免重複遍歷的情況,我們把它稱爲克努特—莫里斯—普拉特算法,簡稱KMP算法。

1.KMP模式算法原理

假設主串S = “abcdefgab”,其實還可以更長一些,我們要匹配的T = “abcdex”,那麼如果用前面的樸素算法的話,前5個字母,兩個串完全相等。直到第6個字母,“f”與“x”不等。

如果按照之前的樸素匹配模式算法,應該繼續比較主串S中當2,3,4,5,6時,首字母與子串T的首字符均不等。

可是仔細觀察發現。對於要匹配的子串T來說,“abcdex”首字母“a”與後面的串“bcdex”中任意一個字符都不相等。也就是說,既然“a”不與後面的子串中任意一字符相等,那麼也可能與主串的後面第2到第5位相等。因此樸素模式匹配算法的接下來的2~5步都是多餘。

注意這裏是KMP算法的關鍵。如果我們知道子串中首字母“a”與子中後面的字符均不相等。而子串的第二位“b”與主串的第二位“b”相等,那麼就意味着,子串中的“a”不需要判斷也知道他們是不可能相等的了。

同理,後面的“c”、“d”、“e”也確定是不相等的。

接下來下面一個例子,假設S = “abcababca”,T = “abcabx”。對於開始判斷“abcabx”。對於開始的判斷,前5個字符完全相等,第6個字符不等。此時,根據剛纔的經驗,T的首字符“a”和T的第二位字符“b”、第三位字符“c”均不等,所以不需要做判斷。

因爲T的首位“a”與第四位“a”相等,第二位“b”與第五位“b”相等。而在①時,第四位的“a”與第五位的“b”已經與主串S中相應位置比較過了,是相等,因此可斷定,T的首字符“a”、第二位的字符“b”與S的第四位字符、第五位字符也不需要比較了,肯定也是相等的。

也就是說,對於在子串中有與首字符相等的字符,也可以省略一部分不必要的判斷步驟。

對比上面兩個步驟,我們發現,在第一步比較時候,我們的i值,也就是主串當前位置的下標是6。即我們在樸素的匹配算法中,主串i值是不斷地回溯來完成的,而我們發現,這種回溯其實是可以不需要的——KMP算法讓這沒必要的回溯發生。

既然i可以不會素,那麼只需要考慮j就行了。通過我們觀察也發現,我們提到了T串的首字符與自身後面字符的比較,發現如果有相等字符,j值的變化就會不相同,也就是說,這個j值的變化與主串其實沒什麼關係,關鍵在於T串結構是否有重複的問題。

比如T = “abcdex”,當前串中沒有任何重複的字符,所以j就由6變成了1。而T= “abcabx”,前綴的“ab”與最後的“x”之前串的後綴“ab”是相等的。因此j就由6變成了3,因此,我們可以得出規律,j的值的多少取決於當前字符之間的串的前後綴的相似度。

我們把T串的各個位置的j值變化定義成一個數組next,那麼netx長度就是T的長度,我們可以得到下面的函數定義:
next[j] = 0 ,當j = 1時
next[j] = Max {k | 1 < k < j,且’p1… pk-1’=’pj-k+1…pj-1’}當此集合非空
next[j] = 1,其他情況

2.next數組值推導

1.T = “abcdex”

j 123456
模式串T abcdex
next[j] 011111

1). j =1,next[i] = 0
2).j = 2, “a”,屬於 next[2] =1
3)j = 3,j由1到j- 1就是”ab”,next[3] = 1
4)同理,沒有重複元素,T 串的next[j] = 011111

2.T = “abcabx”

j 123456
模式串T abcabx
next[j] 011123

1) j = 1,next[1] = 0
2) j = 2,next[2] = 1
3) j = 3,next[3] = 1
4) j = 4,next[4] = 1
5) j = 5, abca,此時前綴字符串“a”與後綴字符“a”相等,由公式’p1…pk-1’ = ‘pj-k+1…pj-1’,由p1 = p4,推出k = 2,因此next[5] = 2。
6) j = 6,abcab,由前綴字符“ab”與後綴字符“ab”相等,next[6] = 3

所以根據經驗得到如果前後綴一個字符相等,k = 2,兩這個字符k= 3,n個相等就是n + 1。

3.KMP模式匹配算法是現

//通過計算返回子串Tnext數組
void get_next(String T,int *next)
{
    int i,j;
    i = 1;
    j = 0;
    next[1] = 0;
    while(i < T[0])//T[0]表示串T的長度
    {
        if(j == 0|| T[i] == T[j])//T[i]表示後綴的單個字符
        {                        //T[j]表示前綴的單個字符
            i++;
            j++;
            next[i] = j;
        }
        else
            j = next[j];//若字符不相同,則j值回溯
    }
}

這段代碼的目的就是爲了計算出當前要匹配的T的next時更要注意

//返回子串T在主串S中第pos字符之後的位置,若不存在,則函數返回0
//T非空,1≤pos≤StrLength(S)
int Index_KMP(String S,String T,intpos)
{
    int i = pos;//i用於主串S當前位置下標值,若pos不爲1
                //則從pos位置開始匹配
    int j = 1;  //j用於子串T中當前位置下標值
    int next[255];//定義next數組
    get_next(T,next);//對T作分析,得到next數組
    while(i <= S[0] && j <= T[0])//若i小於S的長度且j小於T長度
                                 //循環繼續
    {
        if(j == 0 || S[i] == T[j])//兩字母相等則繼續
                        //相對於樸素算法增加了j = 0判斷
        {
            i++;
            j++;
        }
        else
        {
            j = next[i];
        }
    }
    if(j > T[0])
        return i - T[0];
    else
        return 0;
}

相對於樸素匹配算法增加的代碼,改動不算大,關鍵去掉了i值回溯的部分。對於get_next函數來說,若T的長度爲m,因只涉及到簡單的單循環,其時間複雜度O(m),而由於i的值的不回溯,使得index_KMP算法效率得到了提供,while循環的時間複雜度爲O(n)。因此整體複雜度O(m+n)。相較於樸素匹配算法的O((n-m + 1) * m)來說,是要好一些。

這裏也需要強調,KMP算法僅當模式與主串之間存在許多“部分匹配”的情況下才體現出它的優勢。

4.KMP模式匹配算法的改進

後來有人發現,KMP算法還是有缺陷的,如果主串S= “aaaabcde”,子串T = “aaaaax”,next數組值會爲012345,在開始時,”b”與“a”不相等,j = next[5] = 4。此時“b”與第四位的”a”也不相等,同理與第3、2、1的“a”都不相等。直到next[1] =0時。

我們發現b與第2、3、4、5位“a”比較完全是多餘的,因爲2、3、4、5位字符都與首位的“a”相等。因此我們對next函數進行改良。

void get_nextval(String T,int *nextval)
{
    int i , j;
    i = 1;
    j = 0;
    nextval[1] = 0;
    while(i < T[0])//此處T[0]表示T的長度
    {
        if(j == 0 || S[i] == T[j])//T[i]表示後綴的單個字符
                                 //T[j]表示前綴單個字符
        {
            i++;
            j++;
            if(T[i] != T[j])//若當前字符與前綴組不同
                nextval[i] = j;//則當前j爲nextval在i位置的值
            else
                nextval[i] = next[j];//如果前綴字符相同
                //則將前綴字符nextval值賦給nextval在i位置的值
        }
        else
            j = nextval[j];
    }
}

總結改進的KMP算法,它是在計算next值的同時,如果a位字符與它next值指向的b位字符相等,則該a爲的nextval就指向b位的nextval。如果不等,則該a位的nextval值就是它自己a位的next的值。

發佈了72 篇原創文章 · 獲贊 34 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章