數據結構學習筆記(三)串和數組、BF與KMP算法


學習C語言的時候,老師反復說過一個事情——C語言沒有字符串變量這一說!
那么,我們寫的“hello world”是什么呢?
——是字符串常量
在C語言中如果要用到這種數據類型,就只能用數組來實現。

從這可以看出,串和數組的區別。字符串可以簡稱為串,但是其本質也是只能包含字符類型,數組可以表示不同類型,但同一個的數組的各元素類型都是相同的。可以把串看作數組的一種。

串(string):零個或者多個任意字符組成的有限序列

  • 子串:一個串中連續字符組成的子序列,包含空串
  • 真子串:不包含自身的所有子串
  • 字符位置:字符在序列中的序號,1開頭
  • 子串位置:子串第一個字符在主串的位置
  • 空格串:由一個或者多個空格組成的串,與空串不同
  • 串相等:當且僅當兩個串長度相等,對應位置的字符都相等

串同樣可以采用順序和鏈式兩種表示方法,和定義一般數組一樣。
關於串的相關操作,C語言有一個string.h頭文件包含了大部分。比較復雜的是串的匹配算法,在文件校驗,密碼確認等方面有廣泛應用。有BF和KMP兩種算法。

 

BF算法

 

BF算法全稱Brute-Froce算法,又叫暴力破解法,窮舉法,就是一個個比對

比如這個字符串,就從第一個開始比對,i和j同時進行循環

發現有不一樣的,則主串從第二個開始,即i-j+2的位置,子串又回到第一個開始,即為j置為1,這個過程叫做回溯

以此類推,直到匹配到完全相同的,結束條件就是主串或者子串走到盡頭,主串走到盡頭表示匹配失敗,子串走到盡頭表示匹配成功

//字符串結構
typedef struct String{
    char ch[MaxLen-1];
    int length;
}SString;


int index_BF(SString S,SString T){
    int i=1,j=1;//S為主串,T子串
    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) return i-T.length;
    else return 0;
}

最壞情況下,主串每個元素都要停下來讓子串走完一邊,復雜度為長度乘積。若設主串長度為n,子串長度為m,則O(n*m)

 

KMP算法

 

由於BF算法可能需要回溯的次數太多導致效率很低,事實上,並不是每一次都需要回溯的,比如這個例子

前4個已經匹配了,第5個不一樣,但是之前出現了相等的情況,第二次比較直接挪到主串位置3上才是合理的做法,按照BF算法,顯然太費時間了

所以又提出了KMP算法,減少不必要的回溯,也就是上述的第二種移動方法,復雜度可縮短到O(n+m)。
但是,怎么確定這個移動的位置呢?這里定義一個數組next[j],來存放這個值

這里舉一個例子來計算一個主串的next[j]的值

后面各個位上的值依次計算

next[j]的值等於對應位置上前n-1個數中前綴與后綴相同的最大位數加一

viod get_next(SString T,int &next[]){
    int i = 1;
    next[1] = 0;//next數組的位置0不使用,從1開始
    int j = 0;
    while(i<T,length){
        if(j==0 || T.ch[i]=T.ch[j])
        {
            ++i;++j;//i和j同時向前推進
            next[i]=j;/*i后一位其對應的next數組的值等於j向前推進的值*/
        }
        else
        {
            j=next[j];/*如果不相等,i的位置不變,j后退到所在位置的next值*/
        }
    }
}

求出了next[]的值,然后帶入原來的算法中

int index_KMP(SString S,SString T,int pos){
    int i=pos,j=1;//S為主串,T子串
    while(i<=S.length && j<=T.length){
        if(S.ch[i]==T.ch[j])
        {
            i++;j++;
        }
        else
        {
            j=next[j]
        }
    }
    if(j>=T.length) return i-T.length;
    else return 0;
}

這就是完整的KMP算法,其實仔細比較發現區別就在於回溯方面上,KMP雖然去除了部分繁瑣的操作,但還是不夠完美。

 

改進的KMP算法

 

可以改進一下,這里直接引用王卓老師的源課件

大概原理就是增加一次將所在位的值與next值位的比較,如果相同的話則進一步取其next位,算法的代碼如下

void get_nextval(SString T,int &nextval[]){
    int i=1;
    int j=0;
    nextval[1] = 0;
    while(i<T.length){
        if(j==0  || T.ch[i]==T.ch[j]){
            ++i;++j;
            if(T.ch[i] != T.ch[j])nextval[i]=j;
            else nextval[i]=nextval[j];
        }
        else j = nextval[j];
    }
}

數組

數組

數組:按一定格式排列起來的,具有相同類型的數據元素的集合

其實數組這種類型用的比字符串多,在處理一組數據的時候,通常都會用數組,而且數組也可以用來表示字符串,所以數組還是比較常見的。

數據類型 變量名稱[長度]

數組的特點:連續順序結構,結構固定,維數和界數不變
基本操作:初始化,銷毀,取數據,修改元素值,一般不做插入和刪除操作

數組可以是多維的,但是存儲結構是一維的,所以這里就有了一個多維關系映射到一維的問題。以二維數組為例,可以采用“一行一行的”存取,即以行序為主(C、JAVA、BASIC);或者“一列一列的”存取,即以列序為主(FORTRAN)。

行序和列序為主其實差別不大,最主要的就是定位元素的位置,假設一個[m][n]的數組,則元素a[i][j]的位置為:
行序為主:LOC( i , j ) = LOC( 0 , 0 ) + ( n * i + j )*L
列序為主:LOC( i , j ) = LOC( 0 , 0 ) + ( m* i + j )*L

多維矩陣的存儲

其實不只是多維數組,在許多結構中,存在多個數據元素相同,或者空值太多的時候,就會有許多空間看起來像是浪費了,重復的元素太多就會很沒有價值。

一般可壓縮的矩陣有:對稱矩陣、對角矩陣、三角矩陣、稀疏矩陣

例如三角矩陣,由於有一半的部分是相同的,只需要占有一個空間就可以了,對於一個n*n的三角矩陣,壓縮后就只需要n(n+1)/2+1個元素空間,節省了幾乎一半。但是要注意的是,不能再用原來的公式來確定元素的位置了,要根據情況而定。

三元組表示法

對於稀疏矩陣,矩陣中的大部分元素都是0,就只需要把其中非零元素取出來,標出位置,例如

左邊是三元組,右邊是稀疏矩陣
typedef struct
{
    int i,j;     //儲存非零元素的行和列信息
    ElementType e; //非零元素的值
} Triple;       //定義三元組類型
typedef struct
{
    int mu,nu,tu; //矩陣的行、列和非零元素的個數
    Triple data[SMAX]; //三元組表
} TSMatrix;

十字鏈表

十字鏈表的特點是能夠靈活方便的插入和刪除元素,比起三元組多了兩個指針,分別指向下一個元素,結構如下

用十字鏈表表示的矩陣是這樣的

這樣的結構在插入和刪除元素的時候,就只需要修改指針就行了。但是對於非稀疏矩陣來說,就比較浪費空間了。

廣義表

廣義表(又稱列表lists)是n>=0個元素a0,a1,a2,……的有限序列,其中的a既可以是原子,也可以是廣義表。記為:

LS= ( a0 , a1 , a2 , a3 , …… , an)

常用定義有:

  • 表頭:LS(n>=1)的第一個元素a1就是表頭
  • 表尾:除表頭之外的其余元素組成的表就是表尾
  • 長度:最外層所包含的元素的個數
  • 深度:廣義表展開后括號的重數(就是套娃層數)
  • 廣義表可以共享和遞歸,遞歸表深度是無窮值,長度是有限值
 
       


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM