前言
李柱明部落格:https://www.cnblogs.com/lizhuming/p/15487367.html
串的定義
定義:
- 串(string):由零個或多個字元組成的有限序列,又名叫字串。
相關概念:
-
空格串:只包含空格的串。
- 注意:與空串區別,空格串是有內容有長度的,而且可以不止一個空格。
-
子串:串中任意個數的連續字元組成的子序列,稱為該串的子串。
-
主串:相應地,包含子串的串,稱為主串。
-
子串在主串中的位置:子串的第一個字元在主串中的序號。
串的比較
串的比較:
- 通過組成串的字元之間的編碼來進行的。
- 而字元的編碼:指的是字元在對應字符集中的序號。
ASCII 和 Unicode:
- ASCII 碼:用 8 個二進位制數表示一個字元,總共可以表示 256 個字元。
- Unicode 碼:用 16 位二進位制數表示一個字元,總共有 2 的 16 次方 個字元。
- 為了和 ASCII 碼相容,Unicode 碼的前 256 個字元與 ASCII 碼完全相同。
串相等:
- 長度相等,各個對應位置的字元相等。
串的抽象型別資料
串與線性表的比較
線性表:更關注單個元素的操作,如查詢一個元素,插入或刪除一個元素。
串:更多是查詢子串位置、得到指定位置子串、替換子串等操作。
串的資料
資料:串中元素僅由一個字元組成,相鄰元素具有前驅和後繼關係。
操作:
str_assign(t, *cahrs);
生成一個其值等於字串常量 chars 的串 t。str_copy(t, s);
串 s 存在,由串 s 複製得到串 t 中。str_clear(s);
清空串。str_empty(s);
判斷串是否空。str_length(s);
串的長度。str_compare(s, t);
若 s>t,返回 >0 , 若 s=t ,返回 0 ,若 s<t ,返回 <0。str_concat(t, s1, s2);
合併 s1 和 s2,通過 t 返回。str_get_sub(t, s, pos, len);
在串 s 中從 pos 點開始擷取最大 len 的長度,通過 t 返回。str_index(s, t, pos);
在主串 s 的 pos 位置起查詢子串 t 並返回起始子串起始位置,沒有則返回 0。str_replace(s, t, v);
在主串 s 中查詢子串 t,並用串 v 代替。str_insert(s, pos, t);
在主串 s 的 pos 位置中插入串 t。str_delete(s, pos, len);
在主串 s 中的 pos 位置其刪除長度為 len 的子串。
串的儲存結構
串的儲存結構與線性表類似,分為兩類:順序和鏈式。
串的順序儲存結構
定義:用一組地址連續的儲存單元來儲存串中的字元序列。
按照預定義大小,為每個定義的串分配一個固定長度的儲存區,一般用定長陣列來定義。
一般可以將實際的串長值儲存在陣列的 0 下標位置,或者在陣列的最後一個下標位置。
但有的語言規定在串值後面加一個不計入串長度的結束標記符號“\0”來表示串值的終結(但佔用一個空間)。
由於過於不便,串的順序儲存操作有一些變化:串值的儲存空間可在程式執行過程中動態分配而得
- 比如堆:可由 c 語言動態分配函式 malloc() 和 free()來管理。
串的鏈式儲存結構
定義:用節點儲存串的資料。
若一個結點存放一個字元,會存在很大的空間浪費。
故串的鏈式可以一個結點放多個字元,最後一個結點若不滿,可用#或其他非串值字元補全。(每個節點固定長度)
優點:連線兩串操作方便。
缺點:靈活度、效能都不如順序儲存結構的。
樸素的模式匹配演算法
模式匹配的定義
子串(又稱模式串)的定位操作通常稱做串的模式匹配,是串中最重要的操作之一。
樸素的匹配方法(BRUTE FORCE 演算法,BF 演算法)
邏輯思路:
- 對主串的每個字元作為子串開頭,與要匹配的字串進行匹配。
- 對主串做大迴圈,每個字元開頭做要匹配子串的長度的小迴圈,直到匹配成功或全部遍歷完成為止。
資料結構:
typedef struct{
char *str;
int max_length;
int length;
}data_str_t;
程式碼實現:
int bf_index(data_str_t main_str, int start, data_str_t sub_str)
{
int i = start, j = 0, v;
while ((i < main_str.length)&&(j < sub_str.length))
{
if(main_str.str[i] == sub_str.str[j])
{
i++;
j++;
}
else
{
i = i - j + 1;
j = 0;
}
}
if (j == sub_str.length)
{
v = i-sub_str.length;
}
else
{
v = -1;
}
return v;
}
時間複雜度分析
n:主串長度,m:要匹配子串長度。
時間複雜度分析:
-
最好情況:O(1)
- 第一次比較就找到。
-
平均情況:O(n+m)
- 根據等概率原則,平均是(n+m)/2 次查詢。
-
最壞的情況: O(m×n) (注:(n-m+1)×m)
- 每遍比較都在最後出現不等,即每遍最多比較 m 次,最多比較 n-m+1 遍,總的比較次數最多為 m(n-m+1)。
KMP 模式匹配演算法
KMP 與 BF 演算法
KMP 演算法:
- 由三位前輩發表的一個模式匹配演算法,可以大大避免重複遍歷的情況,稱之為克努特-莫里斯-普拉特演算法,檢查 KMP 演算法。
- 又叫 快速模式匹配演算法。
KMP 演算法相比於 BF 演算法,優勢在於:
- 在保證指標 i 不回溯的前提下,當匹配失敗時,讓模式串向右移動最大的距離;
- 並且可以在
O(n+m)
的時間數量級上完成對串的模式匹配操作。
KMP 演算法原理
參考連結:CSDN
原理:
-
主串 S 與模式串 T 有部分相同子串時,可以簡化樸素匹配演算法中的迴圈流程。
-
KMP 中的關鍵就是求公共最長匹配字首和字尾的長度。
-
從子串最長字首和最長字尾開始求。最長也少於前面字元個數。
-
最長公共字首的後面一個字元(指標 j)和匹配失敗的那個字元(指標 i)進行對比。
- 若匹配相同,則繼續推薦 i 和 j。
- 若匹配不同,則繼續縮短公共最長字首和字尾。就是指標 j 進行參考 next 陣列回溯。
-
-
例子 1,如下圖:跳過主串和子串相同的部分。
- 前提:要先知道模式串 T 中首字元 ‘a’ 與串 T 後面的字元均不相等。
- 前提:要先知道模式串 T 中首字元 ‘a’ 與串 T 後面的字元均不相等。
-
例子二,如下圖:跳過子串中與首字元相同的字元。
模式串向右移動距離的計算
在模式串和主串匹配時,各有一個指標指向當前進行匹配的字元(主串中是指標 i ,模式串中是指標 j )。
在保證 i 指標不回溯的前提下,如果想實現功能,就只能讓 j 指標回溯。
j 指標回溯的距離,就相當於模式串向右移動的距離。 j 指標回溯的越多,說明模式串向右移動的距離越長。
計算模式串向右移動的距離,就可以轉化成:當某字元匹配失敗後, j 指標回溯的位置。
模式串中的每個字元所對應 j 指標回溯的位置,可以通過演算法得出,得到的結果相應地儲存在一個陣列中(預設陣列名為 next )。
- 即是模式串中遇到某個字元匹配失敗,就在 next 陣列中找到對應的回溯位置,取出該位置對應的字元和當前的字元繼續匹配,直至全部匹配失敗再推進主串指標 i 和模式串指標 j。
計算方法:
-
對於模式串中的某一字元來說,提取它前面的字串,分別從字串的兩端檢視連續相同的字串的個數,在其基礎上 +1 ,結果就是該字元對應的值。
-
注意:
- 字元對應 next 是第 0 個字元對應 next 陣列下標為 1 開始的。
- 前面兩個字元對應的回溯值為 0、1。
例子:求模式串 “abcabac” 的 next 。
- 第 1 個字元:‘a’,next 值為 0。
- 第 2 個字元:‘b’,next 值為 1。
- 第 3 個字元:‘c’,提取字串 ‘ab’,連續系統字元為 0 個。0 + 1 = 1。 next 值為 1。
- 第 4 個字元:‘a’,提取字串 ‘abc’,連續系統字元為 0 個。0 + 1 = 1。 next 值為 1。
- 第 5 個字元:‘b’,提取字串 ‘abca’,連續系統字元為 1 個。1 + 1 = 2。 next 值為 2。
- 第 6 個字元:‘a’,提取字串 ‘abcab’,連續系統字元為 2 個。2 + 1 = 3。 next 值為 3。
- 第 7 個字元:‘c’,提取字串 ‘abcaba’,連續系統字元為 1 個。1 + 1 = 2。 next 值為 2。
- 由上的 next 陣列的值為(從下標為 1 開始) [0, 1, 1, 1, 2, 3, 2]。
程式碼實現:
-
理解:下圖為 demo
-
理解字串和 next 陣列的下標都為 1 開始。(可以從 0 開始,自己留意下就可以了,不影響原理)
-
計算 next 陣列的值只需要模式串即可,求出每個字元匹配失敗時指標 j 回溯的長度。
-
在求 next 陣列的值得過程中:
-
不要採用暴力演算法檢索模式串。
- 即是不要從可能最長的公共前字尾開始一個減一個地對比下去。如求圖中 j+1 的 next 值時,暴力演算法就是對比 aabcaabcaa 和 abcaabcaab,如果失敗就減少一個長度繼續重新對比 aabcaabca 和 bcaabcaab。然後迴圈下去。
-
應該採用 KMP 演算法(對,就是在 KMP 演算法中利用 KMP 演算法思維)檢索模式串。
-
即是如求圖中 j+1 的 next 值時,直接取出 j 的 next 值 k 對應的字元 b(j 匹配時的最長公共前字尾,從最長的公共前字尾下手),對比 j 和 k 對應的字元。
- 如匹配相同,則 k+1 (在上一個字元的最長公共前字尾基礎上在加長一個字元)就是 j+1 對應字元的 next 值。
- 若匹配不同,噢,匹配不同,字尾和字首匹配時不同噢,主串和模式串匹配時不同噢,那就找出字尾(主串)和字首(模式串)的公共前字尾,在這裡字首(主串)和字尾(模式串)是一樣的,就是找出字首(模式串)的公共前字尾部分(或者說 k 匹配失敗時怎麼辦,利用 next 回溯啊)。就是找出 k 對應的公共前字尾部分,我們已經求出來了啊,就是
next[k]
k'。k' 對應的字元和 j 對應的字元繼續對比,若匹配相同,就next[j] = k'+1
,若匹配不同就繼續縮短最長公共前字尾,最長就是 k' 對應字元的最長公共前字尾next[k']
。 - 不說了,這樣應該能看出遞迴了吧。直至最長公共前字尾為 0 時,採用特殊處理,因為這裡下標 0 沒有對應的字元,所以就推進 i 和 j,就是模式串的首字元和主串的下一個字元繼續比較。看程式碼吧,騷年!!
-
-
-
利用上一個字元對應的 next 值,取出對應的字元,比較當前字元。
-
如果相等。當前字元的 next 值為上一個 next 值 + 1。結束。
-
如果不等,就拿上一個 next 的值做 next 的下標,繼續取出字元對比。直至字元相等或迴圈到 next[1] = 0 結束。
- 迴圈獲取下標 j 就是不斷回溯 j。
-
-
注意:
- next 陣列使用的下標初始值為 1 ,next[0] 沒有用到(也可以存放 next 陣列的長度)。
- 而串的儲存是從陣列的下標 0 開始的,所以程式中為 str[i-1] 和 str[j-1]。
-
初版(下面程式碼只提供思路,具體程式碼參考最後):
#include <stdio.h>
#include <string.h>
/**
* @name next_creat
* @brief 簡版。時間複雜度O(n)
* @param
* @retval
* @author
*/
void next_creat(char *str, int *next)
{
int i = 1;
next[1] = 0;
int j = 0;
while (i<strlen(str))
{
if (j == 0 || str[j-1] == str[i-1])
{
i++;
j++;
next[i] = j;
}
else
{
j = next[j]; // 指標j,遞迴回溯。 // 演算法重點語句。
}
}
}
-
優化版(下面程式碼只提供思路,具體程式碼參考最後):
-
初版有個弊端,如模式串 caaaaabx。
- 用初版思維推導下,會發現當最後一個 a 匹配失敗時需要回溯,但是前面的最長公共前字尾都是-1,不就是和暴力演算法一樣的效果嗎。
- 另外既然最後一個字元 a 匹配不成功,那前面連續的 a 肯定匹配不成功的,所以就應該直接回溯到前面連續字元的前一個字元 c。參考下面程式碼或者看大話資料結構 P142 頁。
-
根據第 j 個的 next 值找它的 next 值 x 對應的第 x 個字元,並判斷第 j 個和第 x 個字元是否相等。
-
若不相等,保持 val 值等於 next 值;若相等,val 值等於第 x 個值的 val 值。
-
#include <stdio.h>
#include <string.h>
/**
* @name next_creat
* @brief 優化版。時間複雜度O(n)
* @param
* @retval
* @author
*/
void nextval_creat(char *str, int *nextval)
{
int i = 1;
nextval[1] = 0;
int j = 0;
while (i<strlen(str))
{
if (j == 0 || str[j-1] == str[i-1])
{
i++;
j++;
if(str[i] != str[j])
nextval[i] = j;/* 如果當前字元和字首字元不同,則把子串的當前回溯指標j賦值給next */
else
nextval[i] = nextval[j]; /* 如果當前字元和字首字元相同,則自己採用字首字元的回溯值 */
}
else
{
j = nextval[j]; // 指標j,遞迴回溯。
}
}
}
基於 next 的 KMP 演算法的實現
KMP 演算法(下面程式碼只提供思路,具體程式碼參考最後):
/**
* @name kmp_index
* @brief
* @param
* @retval
* @author
*/
int kmp_index(char *str_main, char *str_sub)
{
int i = 1;
int j = 1;
int next[10];
next_creat(str_sub, next); //根據模式串T,初始化next陣列
while (i<=strlen(str_main) && j<=strlen(str_sub))
{
//j==0:代表模式串的第一個字元就和指標i指向的字元不相等;S[i-1]==T[j-1],如果對應位置字元相等,兩種情況下,指向當前測試的兩個指標下標i和j都向後移
if (j==0 || str_main[i-1] == str_sub[j-1])
{
i++;
j++;
}
else
{
j=next[j];//如果測試的兩個字元不相等,i不動,j變為當前測試字串的next值
}
}
if (j > strlen(str_sub))
{
//如果條件為真,說明匹配成功
return i-(int)strlen(str_sub);
}
return -1;
}
KMP 時間複雜度
KMP 演算法的時間複雜度:O(m+n)
- get_next 的時間複雜度:O(m)
- while 迴圈的時間複雜度:O(n)
參考程式碼
串 & KPM 演算法
/** @file string_kmp.c
* @brief 採用kmp演算法
* @details 詳細說明
* @author lzm
* @date 2021-09-11 20:10:56
* @version v1.0
* @copyright Copyright By lizhuming, All Rights Reserved
* @blog https://www.cnblogs.com/lizhuming/
*
**********************************************************
* @LOG 修改日誌:
**********************************************************
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define STRING_SIZE 100
/* status_e是函式的型別,其值是函式結果狀態程式碼,如OK等 */
typedef enum {
LZM_STRING_STATUS_ERR = -1,
LZM_STRING_STATUS_OK,
LZM_STRING_STATUS_FALSE,
}status_e;
typedef int elem_type; /* elem_type型別根據實際情況而定,這裡假設為int */
typedef char string_t[STRING_SIZE+1];
/**
* @name str_assing
* @brief 生成一個其值等於 str_t 的串 str_s
* @param
* @retval
* @author lzm
*/
status_e str_assing(string_t str_s, char *str_t)
{
int i = 0;
if(str_t == NULL || str_s == NULL || strlen(str_s) > STRING_SIZE)
return LZM_STRING_STATUS_ERR;
else
{
str_t[0] = strlen(str_s);
for(i = 1; i <= str_t[0]; i++)
str_t[i] = *(str_s + i-1);
return LZM_STRING_STATUS_OK;
}
}
/**
* @name str_copy
* @brief 複製串 str_t 到串 str_s
* @param
* @retval
* @author lzm
*/
status_e str_copy(string_t str_s, string_t str_t)
{
int i = 0;
if(str_s == NULL || str_t == NULL)
{
return LZM_STRING_STATUS_ERR;
}
for(i = 0; i <= str_t[0]; i++)
{
str_s[i] = str_t[i];
}
return LZM_STRING_STATUS_OK;
}
/**
* @name str_empty
* @brief 判空
* @param
* @retval
* @author lzm
*/
status_e str_empty(string_t str_t)
{
if(str_t == NULL)
{
return LZM_STRING_STATUS_ERR;
}
if(str_t[0] == 0)
return LZM_STRING_STATUS_OK;
return LZM_STRING_STATUS_FALSE;
}
/**
* @name str_compare
* @brief
* @param
* @retval str_s > str_t 則返回大於0,等就0, 小就負
* @author lzm
*/
int str_compare(string_t str_s, string_t str_t)
{
int i = 0;
if(str_t == NULL || str_s == NULL)
{
return LZM_STRING_STATUS_ERR;
}
for(i = 1; i <= str_s[0] && i <= str_t[0]; ++i)
if(str_s[i] != str_t[i])
return str_s[i] - str_t[i];
return str_s[0] - str_t[0];
}
/**
* @name str_length
* @brief 獲取長度
* @param
* @retval
* @author lzm
*/
int str_length(string_t str_s)
{
if(str_s == NULL)
{
return LZM_STRING_STATUS_ERR;
}
return str_s[0];
}
/**
* @name str_clear
* @brief 清空
* @param
* @retval
* @author lzm
*/
int str_clear(string_t str_s)
{
if(str_s == NULL)
return LZM_STRING_STATUS_ERR;
str_s[0] = 0;
return LZM_STRING_STATUS_OK;
}
/**
* @name str_concat
* @brief 拼接
* @param
* @retval
* @author lzm
*/
int str_concat(string_t str_t, string_t str_s1, string_t str_s2)
{
int i = 0;
if(str_t == NULL || str_s1 == NULL || str_s2 == NULL)
return LZM_STRING_STATUS_ERR;
if(str_s1[0] + str_s2[0] <= STRING_SIZE)
{
/* 未截斷 */
for(i=1; i<=str_s1[0]; i++)
str_t[i] = str_s1[i];
for(i=1; i <= str_s2[0]; i++)
str_t[str_s1[0]+i]=str_s2[i];
str_t[0] = str_s1[0] + str_s2[0];
return LZM_STRING_STATUS_OK;
}
else if(str_s1[0] <= STRING_SIZE)
{
/* 截斷S2 */
for(i=1; i <= str_s1[0]; i++)
str_t[i] = str_s1[i];
for(i=1;i<=STRING_SIZE-str_s1[0]; i++)
str_t[str_s1[0]+i] = str_s2[i];
str_t[0] = STRING_SIZE;
return LZM_STRING_STATUS_FALSE;
}
else
{
return LZM_STRING_STATUS_ERR;
}
}
/**
* @name str_insert
* @brief 插串str_t入串str_s
* @param
* @retval
* @author lzm
*/
int str_insert(string_t str_s, string_t str_t, int pos)
{
int i = 0;
if(str_t == NULL || str_s == NULL || pos < 1 || pos > str_t[0]+1)
{
return LZM_STRING_STATUS_ERR;
}
if(str_s[0] + str_t[0] <= STRING_SIZE)
{
/* 完全插入 */
for(i=str_s[0]; i>=pos; i--)
str_s[i+str_t[0]]=str_s[i]; // 先挪動str_s串騰出空間
for(i=pos; i<pos+str_t[0]; i++)
str_s[i] = str_t[i-pos+1];
str_s[0] = str_s[0] + str_t[0];
return LZM_STRING_STATUS_OK;
}
else
{
/* 部分插入 */
for(i = STRING_SIZE; i<=pos; i--)
str_s[i] = str_s[i-str_t[0]];
for(i=pos; i<pos+str_t[0]; i++)
str_s[i] = str_t[i-pos+1];
str_s[0]=STRING_SIZE;
return LZM_STRING_STATUS_FALSE;
}
}
/**
* @name str_delete
* @brief 刪除部分
* @param
* @retval
* @author lzm
*/
int str_delete(string_t str_s, int pos, int len)
{
int i = 0;
if(str_s == NULL || pos < 1 || pos > str_s[0]-len+1)
return LZM_STRING_STATUS_ERR;
for(i=pos+len;i<=str_s[0];i++)
str_s[i-len] = str_s[i];
str_s[0]-=len;
return LZM_STRING_STATUS_ERR;
}
/**
* @name next_creat
* @brief 優化版。時間複雜度O(n)
* @param
* @retval
* @author
*/
int nextval_creat(char *str, int *nextval)
{
int i = 1;
int j = 0;
if(str == NULL || nextval == NULL)
return LZM_STRING_STATUS_ERR;
nextval[1] = 0;
while (i<strlen(str))
{
if (j == 0 || str[j-1] == str[i-1])
{
i++;
j++;
if(str[i] != str[j])
nextval[i] = j;/* 如果當前字元和字首字元不同,則把子串的當前回溯指標j賦值給next */
else
nextval[i] = nextval[j]; /* 如果當前字元和字首字元相同,則自己採用字首字元的回溯值 */
}
else
{
j = nextval[j]; // 指標j,遞迴回溯。
}
}
return LZM_STRING_STATUS_OK;
}
/**
* @name str_kmp_index
* @brief
* @param
* @retval
* @author
*/
int str_kmp_index(string_t str_main, string_t str_sub, int pos)
{
int i = 1;
int j = 1;
int *nextval = NULL;
if(str_main == NULL || str_sub == NULL || pos < 1 || pos > str_main[0] + 1)
return LZM_STRING_STATUS_ERR;
nextval = (int *)malloc(str_sub[0]);
if(nextval == NULL)
return LZM_STRING_STATUS_ERR;
nextval_creat(str_sub, nextval); //根據模式串str_sub,初始化nextval陣列
while (i<=strlen(str_main) && j<=strlen(str_sub))
{
//j==0:代表模式串的第一個字元就和指標i指向的字元不相等;S[i-1]==T[j-1],如果對應位置字元相等,兩種情況下,指向當前測試的兩個指標下標i和j都向後移
if (j==0 || str_main[i-1] == str_sub[j-1])
{
i++;
j++;
}
else
{
j=nextval[j];//如果測試的兩個字元不相等,i不動,j變為當前測試字串的next值
}
}
if (j > strlen(str_sub))
{
//如果條件為真,說明匹配成功
return i-(int)strlen(str_sub);
}
return LZM_STRING_STATUS_ERR;
}
/**
* @name str_replace
* @brief 刪除部分
* @param
* @retval
* @author lzm
*/
int str_replace(string_t str_s, string_t str_t, string_t str_v)
{
int i = 0;
if(str_s == NULL || str_t == NULL || str_empty(str_v) != LZM_STRING_STATUS_OK)
return LZM_STRING_STATUS_ERR;
do
{
i = str_kmp_index(str_s, str_t, i); /* 結果i為從上一個i之後找到的子串T的位置 */
if(i) /* 串str_s中存在串str_t */
{
str_delete(str_s, i, str_length(str_t)); /* 刪除該串str_t */
str_insert(str_s, str_v, i); /* 在原串str_t的位置插入串str_v */
i+=str_length(str_v); /* 在插入的串str_v後面繼續查詢串str_t */
}
}
while(i);
return LZM_STRING_STATUS_OK;
}