04 – 串
本文最后更新于30 天前,其中的信息可能已经过时,如有错误请留言

线性表
栈和队列:操作受限的线性表
串:内容受限的线性表

4.1 串类型的定义

  • 子串:一个串中任意个连续字符组成的子序列(含空串),任意串s都是其自身的子串
  • 真子串:不包含自身的所有子串
  • 空串:零个字符的串。是任意串的子串
  • 主串:包含子串的串
  • 字符位置:一个字符在序列中的序号
  • 子串位置:子串的第一个字符在主串中的位置
  • 空格串:由一个或多个空格组成的串,与空串不同
  • 串相等:当且仅当两个串的长度相等,并且各对应位置的字符都相等

基本操作中Index(S, T, pos)定位函数的基本思想:
在主串S中取从第i(i的初值为pos)个字符起,长度和串T相等的子串和T比较,若相等,则求得函数值为i,否则i值增1直至串S中不存在和T相等的子串为止

// 函数功能:在字符串S中从pos位置开始查找子串T的位置
int Index(String S, String T, int pos) 
{
    // 检查pos的有效性(pos需大于0)
    if (pos > 0) {
        // 获取主串S的长度n,子串T的长度m
        n = StrLength(S); 
        m = StrLength(T);
        // i用于记录当前在主串S中匹配的起始位置,初始化为pos
        i = pos;
        // 循环条件:i的范围需保证子串T能完整匹配(主串剩余长度≥子串长度)
        while (i <= n - m + 1) {
            // 从主串S的第i个位置开始,截取长度为m的子串sub
            SubString(sub, S, i, m);
            // 比较截取的子串sub和目标子串T
            if (StrCompare(sub, T) != 0) 
                // 不匹配则i后移一位
                ++i;
            else 
                // 匹配则返回当前起始位置i
                return i;
        }
    }
    // 若未找到匹配或pos无效,返回0
    return 0; 
}

4.2 串的表示和实现

串有3种机内表示方法,分别如下:

定长顺序存储表示

在串的定长顺序存储结构中,按照预定义的大小,为每个定义的串变量分配一个固定长度的存储区

#define  MAXSTRLEN 255
typedef  unsigned  char SString [MAXSTRLEN + 1]        //0号单元存放串长度

串联接操作和求子串操作见P73

堆分配存储表示

任以一组地址连续的存储单元存放串值字符串序列,但他们的存储空间是在程序执行过程中动态分配而得,空间大小根据串长度动态分配

typedef  struct  {       
  char   * ch ;       
  int     length ;
} HString ;

串操作:串复制和串插入见P75

4.2 串的块链存储表示

和线性表的链式存储结构相类似,也可采用链表方式存储值,除头节点外还附设一个尾指针指示链表中最后一个点,虽然对某些串操作比如连接操作有一定方便之处,但总的来说不如另外两种存储结构灵活,它占用存储量大且操作复杂,故在此不讨论。

4.3 串的模式匹配算法

子串的定位操作通常称为串的模式匹配(其中T称为模式串),采用定长顺序存储结构,可以写出不依赖其然串操作的匹配算法:求子串位置的定位函数Index(S, T, pos)

int Index(SString S, SString T, int pos) {
    int i = pos;  // 主串指针:从pos位置开始匹配
    int j = 1;    // 子串指针:从子串第1个字符(T[1])开始匹配

    // 循环条件:完全遵循原代码 → i <= S[0](主串未遍历完)且 j <= T[0](子串未匹配完)
    while (i <= S[0] && j <= T[0]) {
        if (S[i] == T[j]) {  // 当前字符匹配成功
            i++;  // 主串指针后移,比较下一个字符
            j++;  // 子串指针后移,比较下一个字符
        } else {  // 匹配失败:回溯指针
            i = i - j + 2;  // 主串回到本次匹配起始位置的下一个位置(原代码公式)
            j = 1;          // 子串指针重置,重新开始匹配
        }
    }

    // 匹配成功的条件:子串指针j超过子串长度(T[0]),说明子串完全匹配
    if (j > T[0]) {
        return i - T[0];  // 计算子串在主串的起始位置
    } else {
        return 0;  // 未找到子串,返回0
    }
}

指针i,j指示主串S和模式串T中当前正待比较的字符位置,从主串Pos的第pos个字符起和模式的第一个字符比较,若相等着继续比较后面的字符,否则从主串的下一个字符起再重新和模的字符比较。以此类推,直至模式T中中的每个字符依次和主串S中的一个连续字符序列相等

算法分析:若S串长度为n,T串长度为m,该算法在指针回较少的情况下,其时间复杂度为O(n+m);若指针回溯较多,最坏情况下其时间将达到O(n*m)。如:
主串 0000000000000000000000000000000000001
模式串 00000001
主要问题:指针回溯

模式匹配的一种改进算法KMP算法

能否设计出一种指针没有回溯的算法?
此算法可以在O(m+n)而不是O(mn)的时间数量级内完成串的模式匹配操作,其改进在于每当一趟匹配过程中出现字符比较不等时,不需要回溯i指针,而是利用已得到的“部分匹配”结果将模式向右尽可能远的一段距离后继续进行比较

我们来看一个例子理解这个算法省略了

回顾图4.3中的匹配过程示例,在第三趟的匹配中,当\( i=7 \)、\( j=5 \)字符比较不等时,又从\( i=4 \)、\( j=1 \)重新开始比较。然后,经仔细观察可发现,在\( i=4 \)和\( j=1 \),\( i=5 \)和\( j=1 \)以及\( i=6 \)和\( j=1 \)这3次比较都是不必进行的。因为从第三趟部分匹配的结果就可得出,主串中第4、5和6个字符必然是‘b’、‘c’和‘a’(即模式串中第2、3和4个字符)。因为模式中的第一个字符是\( a \),因此它无需再和这3个字符进行比较,而仅需将模式向右滑动3个字符的位置继续进行\( i=7 \)、\( j=2 \)时的字符比较即可。同理,在第一趟匹配中出现字符不等时,仅需将模式向右移动两个字符的位置继续进行\( i=3 \)、\( j=1 \)时的字符比较。由此,在整个匹配的过程中,\( i \)指针没有回溯,如图4.4所示。

匹配的后缀可以变成前缀,所以直接开始匹配前缀后一位开始下一轮匹配
假设主串为“s1 s2 s3…sn” 模式串为“p1 p2 p3…pm”为了改进算法,我们需要知道当主串中第i个字符与模式串中j个字符失配时,主串的第i个字符应与模式串中的哪个字符再比较。可以假设此时应与第K个字符比较应该可以跳过多长,这个值由next[j]=k 表示,其表示与j对应的k值即,当模式串中第j个字符与主串中相应字符失配时,在模式中需重新和主串该字符进行比较的位置

int Index_KMP(SString S, SString T, int pos) {
    int i = pos;    // i 是主串 S 的指针,从 pos 开始
    int j = 1;      // j 是模式串 T 的指针,从第 1 个字符开始

    // 当主串和模式串都还没遍历完时,继续匹配
    while (i <= S[0] && j <= T[0]) {
        // 情况1:j=0 表示模式串从头开始匹配;情况2:当前字符匹配成功
        if (j == 0 || S[i] == T[j]) {
            i++;  // 主串指针后移
            j++;  // 模式串指针后移,继续比较下一个字符
        } else {
            // 字符不匹配,模式串指针 j 跳转到 next[j] 的位置,减少重复比较
            j = next[j];
        }
    }

    // 如果 j 超过了模式串的长度,说明匹配成功
    if (j > T[0]) {
        // 返回主串中 T 的起始位置:i 是主串当前位置,减去模式串长度 T[0]
        return i - T[0];
    } else {
        // 匹配失败,返回 0
        return 0;
    }
}
//注T[0]表示模式串的长度

代码的思路是通过迭代的方式,考试能求出值即可(等待理解),我们用另一种思路计算Next的值

求next函数值的算法:

// 函数名:get_next
// 功能:计算模式串 T 的 next 函数值,并存入数组 next
// 参数:
//   - T:输入的模式串(SString 为自定义字符串类型,通常 T[0] 存储字符串长度,T[1..T[0]] 存储实际字符)
//   - next:输出数组,用于保存模式串每个位置的 next 值(next[1..T[0]] 有效,next[0] 未使用)
// 核心逻辑:通过前缀后缀的最长公共元素长度,确定字符不匹配时模式串的回溯位置,避免主串指针 i 回溯
void get_next(SString T, int next[]) {
    // 初始化变量:
    // i:模式串 T 的下标指针(从 1 开始,对应 T[1] 第一个字符)
    // j:用于记录前缀的下标,同时表示当前位置前缀后缀的最长公共元素长度
    i = 1;        
    next[1] = 0;  // 模式串第 1 个字符(T[1])无前缀,next[1] 固定为 0
    j = 0;        

    // 循环条件:i 未遍历完模式串(T[0] 是字符串长度,i < T[0] 表示未到最后一个字符)
    while (i < T[0]) {
        // 两种情况进入匹配/更新逻辑:
        // 1. j == 0:前缀为空(说明当前无公共前后缀),需重新开始匹配
        // 2. T[i] == T[j]:当前模式串的后缀字符(T[i])与前缀字符(T[j])匹配成功
        if (j == 0 || T[i] == T[j]) {
            ++i;  // 后缀指针后移,指向 next 要计算的下一个位置
            ++j;  // 前缀指针后移,公共长度 +1
            next[i] = j;  // 记录当前位置 i 的 next 值:前缀后缀最长公共长度(即 j 的当前值)
        }
        else {
            // 字符不匹配:模式串前缀指针回溯到 next[j] 位置
            // 核心:利用已计算的 next 值,跳过无效比较(避免从头开始)
            j = next[j];
        }
    }
}// get_next:函数结束

去掉maxL串中最后一位,在最前面一位加上-1,得到-1 0 0 0 1 2,在每个上加1得到最后的next值0 1 1 1 2 3
也可以这么想,求pj的next值只需要看1到j-1位的子串中重复前后缀的长度 + 1

进一步改进

前面定义的next函数在某些情况下尚有缺陷。例如模式a a a a b在和主串a a a b a a a a b匹配时,当\(i=4\)、\(j=4\)时\(s.ch[4] \neq t.ch[4]\),由\(next[j]\)的指示还需进行\(i=4\)、\(j=3\),\(i=4\)、\(j=2\),\(i=4\)、\(j=1\)这 3 次比较。

实际上,因为模式中第 1、2、3 个字符和第 4 个字符都相等,因此不需要再和主串中第 4 个字符相比较,而可以将模式一气向右滑动 4 个字符的位置直接进行\(i=5\)、\(j=1\)时的字符比较。这就是说,若按上述定义得到\(next[j]=k\),而模式中\(p_j = p_k\),则当主串中字符\(s_i\)和\(p_j\)比较不等时,不需要再和\(p_k\)进行比较,而直接和\(p_{next[k]}\)进行比较,换句话说,此时的\(next[j]\)应和\(next[k]\)相同。由此可得计算next函数修正值的算法如算法 4.8 所示。此时匹配算法不变

void get_nextval(SString T, int nextval[]) {
    // 求模式串T的next函数修正值并存入数组nextval。
    i = 1;    nextval[1] = 0;    j = 0;
    while (i < T[0]) {
        if (j == 0 || T[i] == T[j]) {
            ++i;  ++j;
            if (T[i] != T[j])  nextval[i] = j;
            else  nextval[i] = nextval[j];
        }
        else  j = nextval[j];
    }
}// get_nextval

若不相等填入对应的next值,由此得到nextval序列

KMP算法主串的指针不需要回溯的原因

我们已经利用next数组将之前走过的路的有用信息记录下来了

举个例子:假设主串是  A B A B C A B A B D ,模式串是  A B A B D 。

  • 当匹配到第 5 个字符(主串是  C ,模式串是  D )时,匹配失败了。
  • 这时候, next  数组告诉我们:模式串前 4 个字符  A B A B  里,最长的“前缀=后缀”是  A B (长度 2)
    • next[5] = 3,下面说如何推导的
      1. 首先要清楚next数组只和模式串有关
      2.[5]:匹配到模式串第5个字符时与主串的某字符不匹配
      3.“3”:与主串上一轮不匹配的字符进行匹配的模式串的字符的位置,需要找模式串中是否存在重复的元素,由于前一组元素必须从p1即模式串第一个元素开始,所以数值为首尾相同子串长度加 1
      本质是:匹配的后缀可以变成前缀,所以直接开始匹配前缀后一位开始下一轮匹配
      (但我有一个疑问如果匹配的后缀并不包含最后的元素怎么办:哦我没看定义,定义中要求后缀也必须包含最后的字符,这也是为什么用前后缀描述,因为这个词的意思就是从头开始,从尾开始的含义)
  • 所以模式串直接滑到“前缀  A B ”的后面,主串指针  i  不用往回退,继续从  C  后面的字符开始匹配就行

原来的复杂度是O(MN)M为主串长度,N为模式串长度

学习笔记如有侵权,请提醒我,我会马上删除
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇