資料結構與演算法--串
串型別的定義
計算機非數值處理的物件基本都是字串資料。
串
由零個或多個字元組成的有限序列
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;
}