串(模式匹配 KMP 算法)

四、串

4.1 串的定义

字符串简称串。

串(String)是由零个或多个字符组成的有限序列,一般记为 S = ′ a 1 a 2 . . . a n ′     ( n ≥ 0 ) S = 'a_1a_2...a_n'\ \ \ (n\geq 0) S=a1a2...an   (n0)

其中,S是串名,单引号括起来的字符序列是串的值; a i a_i ai可以是字母、数字或其他字符,其中字符的个数n称为串的长度。n = 0时的串称为空串( ∅ \emptyset )。

子串:串中任意个连续的字符组成的子序列称为该串的子串。

包含子串的串相应地称为主串。

某个字符在串中的序号称为该字符在串中的位置。子串在主串中的位置以子串的第一个字符在主串中的位置来表示。

两个串的长度相等且每个对应位置的字符都相等时,两个串才是相等的。

空格是特殊字符,由空格组成的串称为空格串。

串的逻辑结构与线性表相似,区别仅在于串的数据对象仅限定为字符集。在基本操作上串和线性表有较大区别。线性表主要以单个元素为操作对象,串的基本操作通常以子串为操作对象。

4.2 串的存储结构
// 预定义最大长度
#define MaxLen 255
typedef struct{
    char ch[MaxLen];
    int length;
}SString;

// 堆分配表示
typedef struct{
    char *ch; // 使用 malloc 和 free 操作
    int length;
}HString;

块链表示:使用类似于线性表的链式存储结构,也可采用链表方式存储串值。在具体实现时,每个结点既可以存放一个字符,也可以存放多个字符。每个结点称为块,整个链表称为块链结构。最后一个结点占不满时通常用’#'补上。

在这里插入图片描述

4.3 串的基本操作
StrAssignn(&T, chars); //串赋值为chars
StrCopy(&T, S); //S复制得到T
StrEmpty(S); //判S空
StrCompare(S, T); //比较S T大小
StrLength(S); //S长度
SubString(&Sub, S, pos, len); //获得S从pos起长为len的子串
Concat(&T, S1, S2); //T返回链接S1和S2的串
Index(S, T); // 定位操作 若S存在与T相同的子串 返回其在主串中的第一个位置
ClearString(&S); // 清空S
DestroyString(&S); //销毁S

其中, S t r A s s i g n ,   S t r L e n g t h ,   S t r C o m p a r e ,   C o n c a t ,   S u b S t r i n g StrAssign,\ StrLength,\ StrCompare,\ Concat,\ SubString StrAssign, StrLength, StrCompare, Concat, SubString 5项构成穿类型的最小操作子集,即这些操作不可能用其它串操作实现。

4.4 串的模式匹配

子串的定位操作通常称为串的模式匹配,它求的是子串(模式串)在主串中的位置。(Index)

这里采用定长顺序存储结构。

4.4.1 简单模式匹配算法

最朴素的实现方式,从主串S的第一个字符起,与模式T的第一个字符比较,若相等,则继续逐个比较,否则从主串的下一个字符起,重新和模式的字符比较。以此类推,直到模式T中的每个字符依次和主串S中的一个连续的字符序列相等,则称匹配成功。

时间复杂度为 O ( n m ) O(nm) O(nm) ,m n分别代表模式串和主串的长度。

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 -= (j - 2);
            j = 1;
            // 重新匹配
        }
    }
    if(j > T.length) return i - T.length;
    return 0;
}

4.4.2 改进的模式匹配算法 - KMP
4.4.2.1 改进方向

暴力较坏情况举例: a a a a a a a a a a a a a b aaaaaaaaaaaaab aaaaaaaaaaaaab a a a a a b aaaaab aaaaab

暴力低效的根源:每趟匹配失败都是模式后移一位再从头开始比较。而某趟已匹配相等的字符序列是模式的某个前缀,这种频繁的重复比较相当于模式串在不断地进行自我比较,这就是低效的根源。

如果模式中,已经匹配相等的前缀序列中有某个后缀正好是模式的前缀,那么就可以将模式向后滑动到与这些相等字符对齐的位置,主串的指针无需回溯,并继续从该位置开始进行比较。而模式向后滑动位数的计算仅与模式本身的结构有关,与主串无关。

4.4.2.2 最长相等前后缀

前缀:除最后一个字符以外,字符串的所有头部子串。

后缀:除第一个字符外,字符串所有的尾部子串。

部分匹配值(PM):字符串的前缀和后缀的最长相等前后缀长度。

【例】对于串 " a b a b a " "ababa" "ababa"

前缀: { a ,   a b ,   a b a ,   a b a b } \{a,\ ab,\ aba,\ abab\} {a, ab, aba, abab}

后缀: { a ,   b a ,   a b a ,   b a b a } \{a,\ ba,\ aba,\ baba\} {a, ba, aba, baba}

公共元素: { a ,   , a b a } \{a,\ ,aba\} {a, ,aba}

部分匹配值: 3 3 3

" a b a b a " "ababa" "ababa" 的部分匹配值的表为

编号12345
Sababa
PM00123

00123分别代表串:

a a a

a b ab ab

a b a aba aba

a b a b abab abab

a b a b a ababa ababa

的部分匹配值。

得到PM表后进行匹配,如果出现不匹配的情况,那么相应的移动位数的计算方法为

移动位数 = 已匹配的字符数 - 对应的部分匹配值

整个匹配过程,主串不回退。

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

4.4.2.3 next 数组

移动位数 = 已匹配的字符数 - 对应的部分匹配值

可写成: M o v e = ( j − 1 ) − P M [ j − 1 ] Move=(j-1) - PM[j-1] Move=(j1)PM[j1]

用起来可能会不方便,因此将PM表整体右移一位,得到next数组。

编号12345
Sababa
next-10012

第一个位置用-1来填充,即 ( j − 1 ) − ( − 1 ) (j-1) - (-1) (j1)(1),若第一个元素匹配失败,需要将子串向右移动一位,而不需要计算子串移动的位数。此时主串指针右移,子串指针不变。

最后一个PM值没有用处,因此可以舍去。

因而改写为: M o v e = ( j − 1 ) − n e x t [ j ] Move = (j-1) - next[j] Move=(j1)next[j]

这样 j j j就回退到: j = n e x t [ j ] + 1 j = next[j] + 1 j=next[j]+1

有时为了公式更加简洁、计算更简单,将next数组整体+1。

编号12345
Sababa
next01123

此时指针变化为 j = n e x t [ j ] j = next[j] j=next[j]

next[j]的含义为:在子串的第j个字符发生失配时,则跳转到子串的next[j]位置重新与主串当前位置进行比较。

next函数公式:

n e x t [ j ] = { 0 , j = 1 m a x { k ∣ 1 < k < j   &   ′ p 1 . . . p k − 1 ′ = ′ p j − k + 1 . . . p j − 1 ′ } , 集 合 非 空 1 , 其 他 情 况 next[j] = \left\{ \begin{aligned} &0,&j=1\\ &max\{k|1<k<j\ \&\ 'p_1...p_{k-1}' = 'p_{j-k+1}...p_{j-1}'\}, & 集合非空\\&1,&其他情况 \end{aligned} \right. next[j]=0,max{k1<k<j & p1...pk1=pjk+1...pj1},1,j=1

求解步骤:

  1. next[1] = 0
  2. 设next[j] = k。则
    1. p k = p j p_k = p_j pk=pj,则此时next[j + 1] = k + 1。
    2. p k ≠ p j p_k\neq p_j pk=pj,则next[j+1] = next[next…[k]] + 1。

p k ≠ p j p_k\neq p_j pk=pj时,即序列 ′ p 1 . . . p k − 1 p k ′ 'p_1...p_{k-1}p_k' p1...pk1pk 与序列 ′ p j − k + 1 . . . p j − 1 p j ′ 'p_{j-k+1}...p_{j-1}p_j' pjk+1...pj1pj 在匹配的过程中,在 p k p_k pk位置发生失配。

发生失配意味着要向前移动next[k]个单位继续匹配,如果还不可的话继续移动。

next数组计算函数:

void get_next(SString T, int next[]){
    int i = 1, j = 0;
    next[1] = 0;
    while(i < T.length){
        if(j == 0 || T.ch[i] == T.ch[j]){
            ++i;
            ++j;
            next[i] = j; // pi == pj 的情况
        } else {
            j = next[j]; // pi != pj 的情况 不断循环 直到j降到不再失配为止
        }
    }
}

4.4.2.5 KMP代码部分
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];
        }
    }
    if(j > T.length)return i - T.length;
    return 0;
}

4.4.2.6 next函数进一步优化 - nextval

原本KMP算法不优的原因。

′ a a a a b ′ 'aaaab' aaaab 进行匹配时 ′ a a a b a a a a a b ′ 'aaabaaaaab' aaabaaaaab

/*
aaabaaaaab
aaaab!!
 aaaab!!
  aaaab!!
   aaaab!!
    aaaab
多余4次
*/

根源在于出现了 p j = p n e x t [ j ] p_j = p_{next[j]} pj=pnext[j]

因此需要将出现这种情况的next[j]修正为next[next[j]]。

优化后的next数组常更新命名为nextval。

void get_next(SString T, int nextval[]){
    int i = 1, j = 0;
    nextval[1] = 0;
    while(i < T.length){
        if(j == 0 || T.ch[i] == T.ch[j]){
            ++i;
            ++j;
            // pi == pj 的情况
            if(T.ch[i] != T.ch[j]) nextval[i] = j; // 改动
            else nextval[i] = nextval[j]; // 改动
        } else {
            // pi != pj 的情况 不断循环 直到j降到不再失配为止
            j = nextval[j]; 
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值