QEQuantumEinsteinSearchCtrl/⌘K

第四章 串(String)

5 分钟阅读2,0269 个小节
返回目录

第四章 串(String)

4.1 串的定义和基本操作

串(String):由零个或多个字符组成的有限序列。记作 S=a1a2anS='a_1a_2\ldots a_n',其中 nn 为串长。n=0n=0 时称为空串(用 \varnothing 表示)。

⚠️ 空串 vs 空格串:空串长度为0(无任何字符),空格串 " " 长度为1(包含一个空格字符),二者不同!

子串:串中任意个连续字符组成的子序列。空串是任何串的子串,任何串是自身的子串

子串位置:子串在主串中第一次出现时第一个字符的位置(位序从1开始)。

串 vs 线性表

  • 串是特殊的线性表——数据对象限定为字符集
  • 串的操作对象通常是子串(而非单个元素)——如求子串、模式匹配、串联接等
  • 线性表的操作多针对单个元素——如查找、插入、删除某个元素

串的比较:按字典序逐字符比较。字符的大小由其编码值决定(ASCII码、Unicode等)。若所有对应字符相同但长度不同,则较短串更小。

基本操作

  • StrAssign(&T, chars):赋值(生成串T)
  • StrCopy(&T, S):复制
  • StrEmpty(S):判空
  • StrLength(S):求串长
  • ClearString(&S):清空
  • DestroyString(&S):销毁
  • Concat(&T, S1, S2):串联接
  • SubString(&Sub, S, pos, len):求子串
  • StrCompare(S, T):串比较
  • Index(S, T, pos):定位(模式匹配)

🗣️ 大白话:串就是一串字符,比如 "hello" 就是一个长度为5的串。"ell" 就是 "hello" 的子串。

串的存储结构

1. 定长顺序存储:用固定长度数组存储,有4种方案记录串长:

方案说明特点
ch[0] 存储串长串内容存在 ch[1] ~ ch[MAXLEN]串长受限于 ch[0] 类型(如 char 最大255)
额外变量 length串内容存在 ch[0] ~ ch[length-1]常用方案
\0 结尾类似C语言字符串求串长需遍历 O(n)O(n)
ch[0] 不用 + length串内容存在 ch[1] ~ ch[length]便于与教材位序从1开始对应

2. 堆分配存储:用 malloc 动态分配空间,串长不受限

3. 块链存储:用链表存储,每个结点存多个字符(存储密度 = 实际字符数 / 链表总容量)

⚠️ 块链存储操作不方便、存储密度低,实际中很少使用


4.2 串的模式匹配

一、朴素模式匹配算法(BF算法)

问题:主串 SS 中查找模式串 TT 的位置。

思路:从主串第1个字符开始,依次与模式串对齐比较,失败就回溯到主串的下一个位置重新比较。

c
int Index(SString S, SString T) {
    int i = 1, j = 1;
    while (i <= S.length && j <= T.length) {
        if (S.ch[i] == T.ch[j]) {
            i++; j++;           // 匹配成功,继续比较后续
        } else {
            i = i - j + 2;     // i回溯
            j = 1;              // j归位
        }
    }
    if (j > T.length) return i - T.length;  // 匹配成功
    else return 0;                           // 匹配失败
}
java
// BF朴素模式匹配(Java下标从0开始)
int indexBF(String S, String T) {
    int i = 0, j = 0;
    while (i < S.length() && j < T.length()) {
        if (S.charAt(i) == T.charAt(j)) {
            i++; j++;
        } else {
            i = i - j + 1;  // i回溯
            j = 0;           // j归位
        }
    }
    if (j >= T.length()) return i - T.length();  // 匹配成功
    else return -1;                               // 匹配失败
}

时间复杂度

  • 最好情况:O(m)O(m)(第一次就匹配成功,mm 为模式串长度)
  • 最坏情况O(nm)O(nm)nn 为主串长度,mm 为模式串长度)
  • 最坏情况示例:SS = "aaa...aab",TT = "aab" → 每次前 m1m-1 个字符都匹配,到最后一个才失配
  • 实际应用中平均接近 O(n+m)O(n+m)(因为自然语言中连续部分匹配的概率很低)

🗣️ 大白话:BF算法就像笨办法找人——你拿着一张照片从人群第一个人开始一个个比对。如果发现"不是",你就回到下一个人重新开始比对。虽然简单但是慢。

二、KMP算法

核心思想:当发生不匹配时,主串指针 ii 不回溯,只让模式串指针 jj 回退到一个合适的位置。这个"合适的位置"由 next数组 决定。

next数组的含义next[j] 表示当模式串第 jj 个字符与主串发生不匹配时,模式串需要回退到的位置。也就是模式串 T[1j1]T[1 \ldots j-1]最长相等真前后缀的长度加1。

next数组的手工求法

对模式串的每个位置 jj,求 T[1j1]T[1 \ldots j-1] 的最长相等真前后缀长度,加1。

:模式串 T="abaabcac"T = "abaabcac"

jj12345678
模式串abaabcac
T[1..j1]T[1..j-1]"a""ab""aba""abaa""abaab""abaabc""abaabca"
最长相等前后缀长度00011201
next[j]01122312

📏 规则:next[1] 固定为 0。

【KMP 匹配过程完整手工模拟】

以主串 SS = "abaabcac",模式串 TT = "abaabc" 为例,next = [0, 1, 1, 2, 2, 3]。

步骤iijj比较结果动作
111S[1]='a' vs T[1]='a'i++, j++
222S[2]='b' vs T[2]='b'i++, j++
333S[3]='a' vs T[3]='a'i++, j++
444S[4]='a' vs T[4]='a'i++, j++
555S[5]='b' vs T[5]='b'i++, j++
666S[6]='c' vs T[6]='c'i++, j++, j=7 > T.length=6 → 匹配成功!

匹配位置 = iT.length=76=1i - T.length = 7 - 6 = 1

再举一个需要回退的例子SS = "abaacababc",TT = "ababc",next=[0,1,1,2,1]\text{next} = [0, 1, 1, 2, 1]

步骤iijj比较结果动作
111a = ai=2, j=2
222b = bi=3, j=3
333a = ai=4, j=4
444a ≠ bj = next[4] = 2(i 不动!利用已知 "a" 匹配信息)
542a ≠ bj = next[2] = 1
641a = ai=5, j=2
752c ≠ bj = next[2] = 1
851c ≠ aj = next[1] = 0,故 j==0 → i++, j++
961a = ai=7, j=2
1072b = bi=8, j=3
1183a = ai=9, j=4
1294b = bi=10, j=5
13105c = cj=6 > 5 → 匹配成功!位置 = 10 - 5 = 6

💡 手算要诀

  1. 单独求出 next 数组(对模式串 TT 逐位求最长相等真前后缀 + 1)
  2. 模拟匹配时,ii 永远只增不减,失配时只动 jjj=next[j]j = \text{next}[j]
  3. j=0j = 0 时,说明模式串第1个字符就不匹配,此时 iijj 都加1(开始新一轮比较)
  4. 考试中常考比较次数:上例中匹配成功共比较了 13 次

⚠️ 2019年真题:KMP 匹配主串 S 和模式串 T,问比较次数。答案是 10 次——关键是准确执行上述步骤表。

KMP算法代码

c
int Index_KMP(SString S, SString T, int next[]) {
    int i = 1, j = 1;
    while (i <= S.length && j <= T.length) {
        if (j == 0 || S.ch[i] == T.ch[j]) {
            i++; j++;
        } else {
            j = next[j];   // 模式串向右滑动,i不回溯
        }
    }
    if (j > T.length) return i - T.length;
    else return 0;
}
java
// KMP算法(Java下标从0开始)
// ⚠️ 考研代码陷阱⚠️:Java/C++代码题目中,字符串和next数组下标通常从0开始(next[0]=-1)。
// 而在考研理论选择题/填空题中,默认下标从1开始(next[1]=0)。
// 两者next值恰好差1。务必看清题目要求!
int indexKMP(String S, String T, int[] next) {
    int i = 0, j = 0;
    while (i < S.length() && j < T.length()) {
        if (j == -1 || S.charAt(i) == T.charAt(j)) {
            i++; j++;
        } else {
            j = next[j];  // 模式串向右滑动,i不回溯
        }
    }
    if (j >= T.length()) return i - T.length();
    else return -1;
}

// 求next数组(Java版,下标从0开始)
int[] getNext(String T) {
    int[] next = new int[T.length()];
    next[0] = -1;
    int i = 0, j = -1;
    while (i < T.length() - 1) {
        if (j == -1 || T.charAt(i) == T.charAt(j)) {
            i++; j++;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
    return next;
}

时间复杂度O(n+m)O(n+m)

三、KMP的进一步优化——nextval数组

T[j]=T[next[j]]T[j] = T[next[j]] 时,回退后仍然会失配,造成不必要的比较。优化后使用 nextval数组

nextval 手工求法(先求 next,再优化)

:模式串 TT = "abaabcac",next = [0, 1, 1, 2, 2, 3, 1, 2]

jj12345678
T[j]abaabcac
next[j]01122312
T[next[j]]aabaaab
T[j]==T[next[j]]?b≠aa=aa≠bb≠ac≠aa=ac≠b
nextval[j]01021302

💡 手算规则:逐列检查 T[j]T[j] 是否等于 T[next[j]]T[\text{next}[j]]

  • 不等nextval[j]=next[j]\text{nextval}[j] = \text{next}[j](不优化)
  • 相等nextval[j]=nextval[next[j]]\text{nextval}[j] = \text{nextval}[\text{next}[j]](递归取值,因为回退后一定还会失配)
  • nextval[1]\text{nextval}[1] 固定为 0
java
// KMP nextval数组优化(Java版,下标从0开始)
int[] getNextVal(String T) {
    int[] next = getNext(T);
    int[] nextval = new int[T.length()];
    nextval[0] = -1;
    for (int j = 1; j < T.length(); j++) {
        if (T.charAt(j) == T.charAt(next[j]))
            nextval[j] = nextval[next[j]];
        else
            nextval[j] = next[j];
    }
    return nextval;
}

🗣️ 大白话:KMP算法的精髓是"别做无用功"。当你发现不匹配时,你已经知道前面一段是匹配的,所以不用从头来过,只需要利用模式串自身的重复结构"滑动"到合适位置继续比较。

📝 真题剖析

【2015年408真题】 已知字符串 SS 为 "abaabaabcabaabc",模式串 TT 为 "abaabc",采用KMP算法进行匹配,第一次出现"失配"(S[i]T[j]S[i] \neq T[j])时,i=j=i = j = ();

A. 3, 3   B. 5, 5   C. 6, 6   D. 4, 4

解析

SS: a b a a b a a b c a b a a b c TT: a b a a b c

i=1,j=1i=1, j=1 开始逐字符比对:

  • i=1,j=1i=1,j=1: a=a ✓
  • i=2,j=2i=2,j=2: b=b ✓
  • i=3,j=3i=3,j=3: a=a ✓
  • i=4,j=4i=4,j=4: a=a ✓
  • i=5,j=5i=5,j=5: b=b ✓
  • i=6,j=6i=6,j=6: a≠c ✗ → 第一次失配,i=6,j=6i=6, j=6

答案选C。

📌 第四章总结

核心知识点考研要求
串的定义、子串概念理解概念
朴素模式匹配(BF算法)理解原理和复杂度
KMP算法原理重中之重,必须完全掌握
next数组的手工求法每年必考(选择题或大题)
nextval数组的求法高频考点
KMP的时间复杂度 O(n+m)O(n+m)牢记