1. 程式人生 > 其它 >資料結構與演算法--串

資料結構與演算法--串

目錄

串型別的定義

計算機非數值處理的物件基本都是字串資料。

由零個或多個字元組成的有限序列

s = ‘a1a2……an

其中,s表示串名,a1a2……an代表串值,ai可以是字母、數字或其他字元。|s|表示串長,即串中字元的數目。ai在序列中的序號(串中字元的序號從0開始)稱為該字元在串中的位置。單引號本身不屬於串,只起界定作用。

零個字元的串成為空串

一個或多個空格組成的串成為空格串

子串

串中任意個連續字元組成的子序列

包含字串的串相應的成為主串

特別的,空串是任意串的字串,任意串是其自身的字串。

計算字串時,一定要注意是否包括空串和自身。

字串在主串中的位置:字串在主串中首次出現時的該字串的首字元對應的字串中的序號,也稱字串在主串中的序號。(習慣序號從0開始)

基本操作

StrAssign(&T,chars);//常量賦值
StrCopy(&T,S);//拷貝賦值
StrDestroy(&S);//串的銷燬
StrEmpty(S);//串是否為空
StrCompare(S,T);//串的比較
StrLength(S);//串的長度
StrCat(&T,S1,S2);//串的拼接
StrSub(&Sub,S,pos,len);//串的字串
Index(S,T,pos);//從S的pos位置查詢T第一次出現位置,沒有則返回-1
Replace(&S,T,V);//字串的替換
StrInsert(&S,pos,T);//串的插入
StrDelete(&S,pos,len);//子串的刪除
StrClear(&S);//串的清空

上述定義的13中操作中,StrAssign(串賦值)、StrCopy(串複製)、StrCompare(串比較)、StrLength(求串長)、StrCat(串拼接)、StrSub(求子串)這六種操作構成串型別的最小操作子集

串的表示和實現

串實際上是特殊的線性表,故其儲存結構與線性表的儲存結構類似,只不過串的結點是單個字元。

定長順序儲存表示

用一組地址連續的儲存單元儲存串值的字元序列。

按照預定義的大小,為每個定義的串變數分配一個固定長度的儲存區。串的實際長度可以在此預定義長度內隨意,但超過預定義長度的串值會被捨棄,稱為截斷

兩種順序儲存表示:

  • 下標0的分量存放串的長度,其他存放字元
  • 串值末尾增加一個不計入串長的結束標記字元,例如C和C++C採用‘\n’結尾
//一些操作比較簡單,這裡只是實現部分操作,採用的以'\n'結尾
//串拼接
Status StrCat(&T,S1,S2)
{
    int len1 = StrLength(S1),len2 = StrLength(S2);
    for(int i = 0;i<len1&&i<MAXSIZE;++i)
        T[i] = S1[i];
    for(int i = 0; i < len2 && len1 + i < MAXSIZE; ++i)
        T[i+len1] = s2[i];
    if(len1 + len2 > MAXSIZE)
        T[MAXSIZE-1] = '\n';
    else 
        T[len1+len2-1] = '\n';
    return OK;
}

//求子串
Status StrSub(&Sub,S,pos,len)
{
    int len = StrLength(S);
    if(pos + len > n)
        return ERROR;
    for(int i = 0; i < len; ++i)
        Sub[i] = S[pos+i];
    Sub[len] = '\n';
    return OK;
}

堆分配儲存表示

在定長順序儲存中,雖然實現簡單,但是由於空間一定,很容易產生截斷現象,所以我們自然而然想到動態分配空間。

堆分配儲存仍然採用一組地址連續的儲存單元存放串值字元序列,但是儲存空間是在程式執行過程中動態分配而得,所以也稱為動態儲存分配的順序表。

通常,C語言中提供的串型別就是以這種儲存方式實現的。系統利用函式malloc和freee進行串值空間的動態管理,為每一個新產生的串分配一個儲存區,稱為串值共享的儲存空間為堆,仍是順序儲存。

同定長順序儲存實現,我們仍然採用C語言形式描述堆分配儲存表示。我們沒有顯式的T[0]去描述串長,串長是一個隱含值(串以特定字元結尾)。所以,約定串長也作為儲存結構的一部分。

//以下的實現中,如果不對HString進行StrAssign會導致ch指標沒有分配空間。除了使用StrAssign外,可以在struct內使用一個建構函式,對ch指標進行初始化。
//線上性表實現中,我們就知道,如果在一個順序儲存區進行不斷插入元素等操作,可能分配空間不足。所以當前堆分配儲存,我們為了避免儲存空間不夠,每次我們都為新生成的串分配一個儲存空間,然後進行串值的複製。
//程式碼中OK,ERROR,OVERFLOW需要define;
typedef struct 
{
    char* ch;//串
    int length;//長度
}HString;

Status StrAssign(HString &S, char* chars)//字串常量應該以'\n'結尾
{
    if(T.ch)
        free(T.ch);
    int cnt = 0;//記錄chars的串長
    char *p = chars;
    while(*p != '\n')
    {
        ++p;
        ++cnt;
    }
    if(!cnt)
    {
        T.ch = NULL;
        T.length = 0;
    }
    else
    {
        T.ch = (char*)malloc((cnt+1)*sizeof(char));
        if(!T.ch)
            exit(OVERFLOW);
    	for(int i =0;i<cnt;++i)
            T.ch[i] = chars[i];
        T.ch[cnt] = '\0';
        T.length = cnt;
    }
    return OK;
}

//串長
int Strlength(HString S)
{
    return S.length;
}

//若S>T返回值>0,S<T返回值<0,S=T返回值=0
int StrCompare(HString S, HString T)
{
    for (int i = 0; i < S.length && i < T.length; ++i)
    {
        if (S.ch[i] != T.ch[i])
            return S.ch[i] - T.ch[i];
    }
    return S.length - T.length;
}

//清空字串
Status ClearString(HString& S)
{
    if (S.ch)
    {
        free(S.ch);
        S.ch = 0;
    }
    S.length = 0;
    return OK;
}

//串的拼接
Status StrCat(HString& T, HString S1, HString S2)
{
    if (T.ch)
    {
        free(T.ch);
    }
    T.ch = (char*)malloc((S1.length + S2.length + 1) * sizeof(char));
    if (!T.ch)
        exit(OVERFLOW);
    for (int i = 0; i < S1.length; ++i)
        T.ch[i] = S1.ch[i];
    for (int i = 0; i < S2.length; ++i)
        T.ch[S1.length + i] = S2.ch[i];
    T.ch[S1.length+S2.length] = '\0';
    T.length = S1.length + S2.length;
    return OK;
}

//獲取子串
Status StrSub(HString& Sub, HString S, int pos, int len)
{
    if (pos < 0 || pos >= S.length || len < 0 || pos + len > S.length)
        return ERROR;
    if (Sub.ch)
        free(Sub.ch);
    if (!len)
    {
        Sub.ch = NULL;
        Sub.length = 0;
    }
    else
    {
        Sub.ch = (char*)malloc((len+1) * sizeof(char));
        if(!Sub.ch)
            exit(OVERFLOW);
        for (int i = 0; i < len; ++i)
            Sub.ch[i] = S.ch[pos + i];
        Sub.ch[len] = '\0';
        Sub.length = len;
    }
    return OK;
}

//串插入
Status StrInsert(HString &S, int pos, HString &T)
{
    if(pos < 0 || pos > S.length)
    	return ERROR;
    char* temp = (char*)malloc((S.length+T.length+1)*(sizeof(char)));
    if(!temp)
        exit(ERROR);
    for(int i = 0;i < pos;++i)
        temp[i] = S.ch[i];
    for(int i = 0;i < T.length; ++i)
        temp[i+pos] = T.ch[i];
    for(int i = pos;i < S.length;++i)
        temp[i+T.length] = S.ch[i];
    temp[S.length+T.length] = '\0';
    if(!S.ch)
        free(S.ch);
    S.ch = temp;
    return OK;
}

串的塊鏈儲存表示

同線性表,串的順序表示插入和刪除也很不方便,需要移動大量字元,所以我們可以採用單鏈表的方式儲存串值,串的這種鏈式儲存結構簡稱為鏈串。

typedef struct node 
{
    char ch;
    struct node *next;
}LinkStr;

這種結構便於進行插入和刪除,但是儲存空間利用率太低。每個節點只儲存了一個字元,所以我們通常在一個節點中儲存一個子串。將串的連結串列儲存和串的定長結構結合使用。(為什麼不結合堆儲存呢?笑死,每個結點到底存多少呢?這不徒增煩惱)

儲存密度:(串值所佔儲存位)/(實際分配儲存位)

節點大小的選擇直接影響到串處理的效率。所以要儘可能增大儲存密度。當然也不是越大越好。

在下方結點結構中,next指標大小為4,而data為CHUNKSIZE,故儲存密度為 CHUNKSIZE / CHUNKSIZE+4

實際應用中可以根據問題所需來設定結點大小,例如,在編輯系統中,整個文字編輯區可以看作是一個串,每一行是一個子串,構成一個結點。即:同一行的串用定長結構(80字元),行與行之間用指標相聯結。

這裡只給出相關結構定義,其餘操作函式易實現。

#define CHUNKSIZE 80//自定義塊大小
//一個鏈串由頭指標唯一確認
typedef struct Chunk//結點結構
{
    char data[CHUNKSIZE];
    struct Chunk *next;
}Chunk;
typedef struct//塊鏈結構
{
    Chunk *head, *tail;//串的頭尾指標
    int curlen;//串的當前長度
}LString;

模式匹配演算法

字串的定位運算通常稱為串的模式匹配,是串處理中最重要的運算之一。通常把主串S稱為目標,把字串T成為模式,把從目標S中查詢模式為T的字串的過程稱為“模式匹配”。

以下程式碼重點在於演算法講解,為了簡便,不再使用自定義串,而是統一使用string

簡單演算法

//遍歷每一個位置進行匹配,以string型別為例
int INDEX(string S, string T, int pos)
{
    int i = pos,j = 0;
    while(i < S.size() && j < T.size())
    {
        if(S[i]==T[j])
        {
            ++i;
            ++j;
        }
        else 
        {
            i = i - j + 1;
            j = 0;
		}   
    }
    if(j == T.size())
    	return i-T.size();
    else 
        return -1;
}

時間複雜度分析:

  • 最好情況:每次不成功匹配第一個字元就能比較出結果,則總的比較次數為 i-1+m(i代表匹配成功位置)

    可算出時間複雜度為O(n+m)

  • 最壞情況:每次不成功匹配最後一個字元才能比較出結果,則總的比較次數為i*m

    可算出時間複雜度為*O()

首尾匹配演算法

首先比較模式串的第一個字元,再比較模式串的最後一個字元,最後比較模式串中從第二個到第n-1個字元。

int Index_FL(string S, string T, int pos)
{
    int i = pos;
    while (i <= S.size() - T.size())
    {
        if (S[i] != T[0])
        {
            ++i;
            continue;
        }
        if (S[i + T.size() - 1] != T[T.size() - 1])
        {
            ++i;
            continue;
        }
        else
        {
            int j = 1;
            while (j < T.size() - 1&&S[i+j]==T[j])
                ++j;
            if (j == T.size() - 1)
            {
                return i;
            }
            else
                ++i;
        }
    }
    return -1;
}

KMP演算法

演算法思想:在主串的第i個字元匹配失敗後,不回溯主串當前的位置,而是根據已經得到的部分匹配結構,將模式串向右滑動儘可能遠的一段距離後,繼續進行比較。

我們已知主串S第i個元素和模式串T第j個元素不匹配,這時我們並不進行回溯,而是進行模式串的匹配分析,對S第i個元素和模式串第k和元素進行匹配。

也就是說,我們對模式串進行處理,對於前j-1個元素,我們找到前k-1個元素和後k-1個元素相同的情況(需要滿足最長前後綴匹配),然後返回k,將S的第i個元素和模式串第k個元素進行比較。

1.為什麼能從模式串第k個元素進行比較?

我們將模式串T字串(前j個元素)的前k-1個元素(記作字首)和後k-1個元素(記作字尾)進行了匹配,同時主串S第i個元素前的k-1個元素和模式串T第j個元素前的k-1個元素(也就是上面所說字尾)也是匹配的。所以,模式串的字首也是和主串T的第j個元素前的k-1個元素是匹配的。

2.為什麼能保證前後綴之間不存在匹配可能性?

首先明確我們選取的是最長前後綴匹配串,也就是說,如果在中間存在一個和字首匹配的字串,甚至長度允許大於匹配的字尾的長度,那麼如果從中間這個位置能夠和主串S匹配,就有模式串從頭開始和模式串從中間部分開始,能夠一直到模式串結尾匹配,那麼這個從中間開始的字串才是最長前後綴匹配串,與假設矛盾了。

//next函式求解
/*
*next函式實質上就是找到字首和字尾最大匹配長度,next[i]就表示前i-1
*字元中的前後綴匹配。這樣如果在匹配時,如果模式串i與主串不同,我不
*需要回到0,而是隻需要回到next[i],因為前i-1前後綴匹配。即這段字元
*前next[i] - 1個字元是和字尾匹配的,也就是說我們只需要從next[i]
*重新開始就行
*/
vector<int> KMP_next(string str)
{
    vector<int>next(str.size());
    next[0] = -1;
    int j = 1;
    int k = next[0];
    while (j < str.size())
    {
        if (k == -1 || str[j - 1] == str[k])
            next[j++] = ++k;
        else
            k = next[k];
    }
    return next;
}

//實際上,我們可以發現,如果str[j] == str[next[j]],那我們就沒必要從next[j]這個位置開始回溯,而應該再在前next[j]中進行匹配。於是我們可以得到nextval
vector<int> KMP_nextval(string str)
{
    vector<int> next = KMP_next(str);
    vector<int> nextval(str.size());
    nextval[0] = -1;
    int j = 1;
    int k = nextval[0];
    while (j < str.size())
    {
        if (k == -1 || str[j - 1] == str[k])
        {
            if (str[j] != str[++k])
                nextval[j] = k;
            else
                nextval[j] = nextval[k];
            ++j;
        }
        else
            k = nextval[k];
    }
    return nextval;
}

int KMP(string S, string T)
{
    int len1 = S.size();
    int len2 = T.size();
    vector<int>nextval = KMP_nextval(T);
    int i = 0, j = 0;
    //warning:這裡不能用S.size代替len1,size返回值為unsigned而i為signed
    //所以如果j為-1,那麼j和len2比較會視為unsigned比較,即j變成了最大值
    while (i < len1 && j < len2)
    {
        //j=-1就說明要從模式串開頭重新開始
        if (j == -1 || S[i] == T[j])
        {
            ++i;
            ++j;
        }
        else
            j = nextval[j];
    }
    if (j == T.size())
        return  i - T.size();
    return -1;
}