第五章 串
定義:串是由零個或多個字符串組成的有限序列,又叫字符串。
子串和主串:串中任意個數的連續字符組成的子序列稱爲該串的子串,相應的包含子串的串稱爲主串。
字串在主串中的位置就是子串的第一個字符在主串中的序號。
串的比較:串的比較是通過串的字符之間的編碼來進行的。
串的抽象數據類型:
串的存儲結構
串的順序存儲結構:用一組地址連續的存儲單元來存儲串中的字符序列。
可能會在第一個或最後一個元素中存放長度,也可能在最後添加\0
固定長度的缺陷:固定後可能出現上溢提示或者截斷,這時用"堆"來處理更好,堆可以用C語言的malloc()和free()來管理。
串的鏈式存儲結構:結構中的每個數據元素都是一個字符,但是隻放一個會很浪費,因此一個系欸但也可以存放多個字符,如果最後一個結點沒有被佔滿的話,可以用#補全。
鏈式缺陷:一個結點存放多少個字符才合適變得很重要,這會直接影響串的處理效率,需要根據實際情況做出選擇。
兩種對比:鏈式除了連接串與串操作時有一定方便之外,總的來說不如順序存儲靈活,性能也不如順序存儲結構好。
樸素的模式匹配算法
串的模式匹配:像尋找一個單詞在一篇文章中的定位問題,這種子串的定位操作通常稱爲串的模式匹配。
子串尋找例題:
// 返回子串T在主串S中第pos個字符之後的位置,不存在則返回0
// T非空,1 <= pos <= StrLength(S)
// 設字符串的長度存放在第一個位置中
int Index(String S, String T, int pos)
{
int i = pos; // 匹配開始的位置
int j = 1; // 用來記錄子串中的下標
while (i<=S[0] && j<=T[0])
{
if (S[i] == T[j])
{
i++;
j++;
}
else // 出現不相等的情況了
{
i = i - j + 2; // 回到剛纔的位置的後面
j = 1; // 重置T的位置
}
}
if (j > T[0]) // 說明走完了T全部,匹配
return i - T[0];
else
return 0;
}
時間複雜度分析:
- 最好情況下,每次子串第一個就能檢測出是否匹配,然後根據等概率原則,平均是(n+m)/2次查找,時間複雜度爲O[n+m]。
- 最壞情況下,每次要找到子串最後一個才知道是否匹配,要執行次。時間複雜度爲O[]
性能:低效。
KMP模式匹配算法
算法原理:
如果串中元素都不相同,那麼可以跳過比較的長度,如下圖可省略2-5步驟:
如果有相同的,那麼相同的,可以在斷掉的前面的那個相同除,如在45處直接取結果,省了一部分比較,其中2-5步驟是多餘的:
優點:KMP可以避免不必要的回溯
子串中的新位置選擇問題:
新建一個數組next,每個元素的下標 j 是當判斷出現不同時,子串結束的位置,存放的內容爲子串下次開始的位置;j的值主要取決於子串中是否有重複的問題。
根據經驗如果前後綴一個字符相等,k值是2,兩個字符k值是3,n個相等k就是n+1
思路:
應該是先分析情況,得出上面的規律,然後根據規律寫代碼。
標記重複位置,主數組的位置是一直往後移的,動的是被匹配的數組,不用每次都從頭開始,根據被匹配數組的重複度和當前比較的位置,來確定被匹配數組下次從哪裏開始
計算規則:
只管首尾,中間不管:
前綴和後綴可重疊,但是不可完全相同:
案例:
KMP代碼實現:
思路:j代表前綴,通過next[j]來回溯;i表示後綴,一直在往後加,不會考慮後綴和中間內容的關係。
// 計算子串T的next數組
void get_next(String T, int *next)
{
int i, j;
i = 1;
j = 0;
next[1] = 0;
while (i < T[0]) // T[0]存放的是長度
{
// j=0的時候,就是連續不等回溯重置的時候
// 利用j=0來推動i和j的移動,因爲j=1表示串的第一個位置,所以不會出現漏的情況
// j=0的意思就是先動,取值了再比較
// 內部有++,此時設置的,是上個位置應指向的點,j=0也就是不等時的指向
// if中也有判斷,如果有相等,則j的值可後移,next也會記錄新位置
// 出現新的不等後j還會繼續回溯
if (j==0 || T[i]==T[j])
{
++i; // 後綴判斷一直移動
++j; // 前綴和後綴相似個數計算
next[i] = j; // k = n+1;
}
else
j = next[j]; // 出現不同就回溯,計算新的後綴的相似度
}
}
// 獲取位置
int Index_KMP(String S, String T, int pos)
{
int i = pos;
int j = 1; // T中下標,第一個位置放了長度
int next[255]; // 存放j的設置
get_next(T, next);
while (i<=S[0] && j<=T[0])
{
// 這邊也是憑藉j=0和相等來推動數組移動
// j=0是出現了不等,回溯後取值比較,不動就沒法從頭開始
if (j==0 || S[i]==T[i]) // 如果相似,就繼續走,比樸素方法多了j=0判斷
{
i++;
j++;
}
else
{
// 此處i不動,等待T的新值來比較
// 如果此時i已經過了重複的部分,那麼重複的部分就不用比較了,從重複後開始就行,即next[j]返回的位置
j = next[j]; // 設定j;
}
}
// j>T[0],此時j=length+1,因爲之前有個++的尾巴
// 也說明T被遍歷完了
if (j > T[0])
return i - T[0];
else
return 0;
}
時間複雜度分析:
設T的長度爲m,前面的獲取next數組的複雜度爲O[m];
設S的長度爲n,由於i沒有回溯,所以while循環的複雜度爲O[n],所以整個算法的複雜度爲O[n+m]。
KMP模式算法改進
KMP算法存在的問題:
下圖中的2-4步都是多餘的,所以在這個2,3,4,5位置元素都相同的情況下,可以直接用next[1]的值取代這幾個next的值,所以可以改良next的求解過程
核心思想:
當不等的前面有很多重複的時候,直接跳到最前面,而不是一次一次地往前跳。
void get_nextval(String T, int *nextval)
{
int i, j;
i = 1;
j = 0;
nextval[1] = 0;
while (i<T[0])
{
if (j==0 || T[i]==T[j])
{
++i;
++j;
// 下面是改動的地方
// 如果當前字符和前綴字符不同的話,不相同就不需要往前跳
if (T[i] != T[j])
nextval[i] = j;
else
// 如果i和j位置的元素相同,就使用前面那一個的next,其實也就是直接用的最前面的第一個開始相似的next
// 因爲之前的也用的前面的,所有這裏就一直是最初的那個值
nextval[i] = nextval[j];
}
else
j = nextval[j];
}
}
新版案例:
3、4位置(ab)因爲和前面(1、2位置的ab)重複,所以使用的前面的值;
5位置的a和3位置的a重複,還是用的3的值,也就是用的1的值;
6位置開始出現不同,6的a和4的b不同,所以nextval中的值和原來的一樣;
因爲6開始中斷了,所以要從頭開始了,7位置和2的b相比較,不同,還是取原版的值;
8位置和2的b相同,所以用2的b的位置;
JAVA實現KMP相關
package String.Base;
import java.util.Scanner;
public class BaseMatch {
public static void main(String[] args) {
// Scanner scanner = new Scanner(System.in);
System.out.println("輸入數組1");
String str1 = "jojostarstarl";
System.out.println(str1);
// String str1 = scanner.next();
System.out.println("輸入數組2");
// String str2 = scanner.next();
String str2 = "starl";
System.out.println(str1);
System.out.println("正確結果應爲:");
System.out.println(EasyMatch(str1.toCharArray(), str2.toCharArray()));
System.out.println("KMP結果尾:");
System.out.println(KMP(str1.toCharArray(), str2.toCharArray()));
}
private static Object EasyMatch(char[] str1, char[] str2) {
int len1 = str1.length;
int len2 = str2.length;
int count = 0;
for (int i = 0; i < len1; i++) {
for (int j = 0; j < len2; j++) {
if (str1[i + j] == str2[j]) {
count++;
if (count == len2)
return i + 1;
}
}
count = 0;
}
return "不匹配";
}
private static Object KMP(char[] str1, char[] str2) {
int len1 = str1.length;
int len2 = str2.length;
int[] next = new int[len2];
// 獲取next數組
next = getNextVal(str2);
int i = -1;
int j = -1;
// 兩個都不能越界
while (i < len1 && j < len2) {
// 通過兩個條件推動數組移動
if (j == -1 || str1[i] == str2[j]) {
++i;
++j;
} else {
j = next[j];
}
}
// 判斷是否比較了str2的全部
if (j >= len2) {
// 如果不加1則輸出的是下標位置,下標位置是比實際位置小1的
return i - len2 + 1;
}
return "沒找到";
}
private static int[] getNext(char[] str) {
int len = str.length;
int i = 0; // 後移
int j = -1; // 回溯
int[] next = new int[len];
next[0] = -1;
// 即算完最後的i++之後i=len,停止循環
while (i < len - 1) {
if (j == -1 || str[i] == str[j]) {
i++;
j++;
// 回溯位置和相同數相關
next[i] = j;
} else {
j = next[j]; // 不相同,要回溯
}
}
return next;
}
private static int[] getNextVal(char[] str) {
int len = str.length;
int i = 0;
int j = -1;
int[] next = new int[len];
next[0] = -1;
while (i < len - 1) {
if (j == -1 || str[i] == str[j]) {
++j;
++i;
// 不相等了,就不用管重複問題了
if (str[i] != str[j])
next[i] = j;
else
// 相等就一直回溯到最前的第一個同元素位置
next[i] = next[j];
} else {
j = next[j];
}
}
return next;
}
}