经典字符串匹配

BF暴力匹配算法

暴力匹配,即Brute Force,简称BF算法。BF算法是一种简单朴素的模式匹配算法,常用于在一个主串S内查找一个子串T的出现位置。

算法步骤

假设有主串S与子串P,主串S的长度为N,子串T的长度为M。

  1. 将S和T左对齐,并比较其第一个元素。
  2. 若匹配,则继续比较下一个元素,一直到第M个元素。
  3. 若不匹配则T向右移动一个位置。
  4. 接着根据步骤3和4进行比较,直到匹配到或者T移动了N-M且仍未匹配到。

代码实现

Python实现

实现1:

def BFMatch(s, p):
    if len(s) < len(p):
        return -1
    i, j = 0, 0
    # 匹配阶段
    while i < len(s) and j < len(p):
        # 匹配,s和p的指针均向前一步
        if s[i] == p[j]:
            i += 1
            j += 1
        # 不匹配,i后退到下一个要匹配的位置,j后头到p开头
        else:
            i = i - j + 1
            j = 0
    if j == len(p):
        return i - j
    return -1

if __name__ == "__main__":
    s = "abcdefghijkl"
    p = "ijk"
    result = BFMatch(s, p)
    if result == -1:
        print("False")
    else:
        print(result)

实现2:

def BFMatch(s, p):
    if len(s) < len(p):
        return -1
    # 最多移动len(s)-len(p)+1次
    # 如果s的前len(s)-len(p)+1个与p均没有匹配
    # 那么直接判断为不匹配,无需比较后len(p)-1个
    for i in range(len(s)-len(p)+1):
        index = i           # 当前比较的字符
        for j in range(len(p)):
            # 相等继续比较下一个
            if s[index] == p[j]:
                index += 1
            # 不相等停止比较
            else:
                break
        # 如果完全相等,返回当前位置
        if index - i == len(p):
            return i
    return -1

if __name__ == "__main__":
    s = "abcdefghijkl"
    p = "ijk"
    result = BFMatch(s, p)
    if result == -1:
        print("False")
    else:
        print(result)

C实现

#include <stdio.h>

int BFMatch(char s[], int len_s, char p[], int len_p) {
    if (len_s < len_p)
        return -1;
    int i = 0, j = 0;
    // 之匹配s的前len_s-len_p+1个
    // 如果s[len(s)-len(p)] != p[0]
    // 那么就不用继续匹配
    // i-j代表s与p对齐的位置
    while (i-j <= len_s-len_p) {
        if (s[i] == p[j]) {
            i++;
            j++;
            if (j == len_p)
                return i - j;
            continue;
        }
        i = i - j + 1;
        j = 0;
    }
    return -1;
}

int main(void) {
#define S "abcdefghijkl"
#define P "ijk"
    char s[sizeof(S)] = S;
    char p[sizeof(P)] = P;
    int result = BFMatch(s, sizeof(S)-1, 
            p, sizeof(P)-1);
    if (result == -1)
        printf("False\n");
    else
        printf("%d\n", result);
    return 0;
}

KMP快速匹配算法

快速模式匹配算法,即Knuth Morris Pratt(简称KMP)算法,是解决字符串匹配问题的经典算法。

KMP算法是在 BF 算法基础上改进得到的算法。BF算法的实现过程是用子串与主串中的字符一一配对,算法执行效率不高。对于主串S和子串P,BF算法如果遇到了不匹配的情况,主串S和子串P的指针都会回退,而且子串会回退到子串首部。KMP算法的实现过程接近人为进行模式匹配的过程。它只需回退子串,并且是根据情况回退,并不一定要回退到子串首部。

算法步骤

假设有主串S与子串P,主串S的长度为N,子串T的长度为M。

  1. 求字串P的部分匹配表。
  2. 将S和T左对齐,并比较其第一个元素。
  3. 若匹配,则继续比较下一个元素,一直到第M个元素。
  4. 若不匹配,根据部分匹配表回退P的指针。
  5. 接着根据步骤3和4进行比较,直到匹配到或者T移动了N-M且仍未匹配到。

失配指针求解

让KMP尽量减少回移的关键在于,用一个部分匹配表(也称失配移动表)记录每次需要回退的位置。部分匹配表是一个与原字符串长度相等的整数数组。表中的元素是字符串中相对于元素的前缀集合和后缀集合的交集中的长度最大的字符串的

假设一个长度为n的模式串为$P=a_0a_1…a_{n-1}$,其中$a_i(0\le i<n)$是单个字符, $Next[\ n+1\ ]$为其部分匹配表。

那么对于$a_{i-1}$:

  • 前缀集合:

    $$ P_1={a_0,a_0a_1,\cdots,a_0…a_{i-1}} $$

  • 后缀集合:

    $$ P_2={a_{i-1},a_{i-2}a_{i-1},\cdots,a_1…a_{i-1}} $$

  • 失配指针:

    $$ Next[i] = \begin{cases} -1 & 当\ i=0 时 \ max & { k|0<k<j 且 “p_0\cdots p_{k-1}” = “p_{j-k}\cdots p_{j-1}” } \ 0 & 其他情况 \end{cases} $$

    即:

    • $Next[0] = -1$;
    • $Next[i] = maxLength(P_1 \cap P_2),\ i \neq 0$。

过程代码演示:

def get_next_process(p, i, j, next_val):
    print("第", i + 1, "趟:")
    print(p)
    if j != -1:
        for k in range(i - j):
            print(" ", end="")
        print(p)
        for k in range(i):
            print(" ", end="")
        print("^")
    if i == 0:
        print("初始化")
    print(next_val)

def get_next(p):
    """求部分匹配表(失配指针)"""
    i = 0    # 指向主串的指针
    j = -1   # 指向模式串的指针,一开始
    next_val = [-1] * len(p)    # 要返回的next数组
    get_next_process(p, i, j, next_val)
    # next[0]==-1,只需要求后面的len(p)-1个值即可
    while i < len(p)-1:
        # 匹配成功,相同前缀长度增加1
        if j == -1 or p[i] == p[j]:    
            i += 1
            j += 1
            next_val[i] = j
            get_next_process(p, i, j, next_val)
        # 匹配不成功则在前面的子串中继续搜索,直至找不到
        else:
            j = next_val[j]
    return next_val

if __name__ == "__main__":
    p = "abababc"
    get_next(p)

输出:

第 1 趟:
abababc
初始化
[-1, -1, -1, -1, -1, -1, -1]
第 2 趟:
abababc
 abababc
 ^
[-1, 0, -1, -1, -1, -1, -1]
第 3 趟:
abababc
  abababc
  ^
[-1, 0, 0, -1, -1, -1, -1]
第 4 趟:
abababc
  abababc
   ^
[-1, 0, 0, 1, -1, -1, -1]
第 5 趟:
abababc
  abababc
    ^
[-1, 0, 0, 1, 2, -1, -1]
第 6 趟:
abababc
  abababc
     ^
[-1, 0, 0, 1, 2, 3, -1]
第 7 趟:
abababc
  abababc
      ^
[-1, 0, 0, 1, 2, 3, 4]

代码实现

Python实现

def get_next(p):
    """求部分匹配表(失配指针)"""
    i = 0    # 指向主串的指针
    j = -1   # 指向模式串的指针
    next_val = [-1] * len(p)    # 要返回的next数组
    # next[0]==-1,只需要求后面的len(p)-1个值即可
    while i < len(p)-1 and j < len(p)-1:
        # 匹配成功,相同前缀长度增加1
        if j == -1 or p[i] == p[j]:    
            i += 1
            j += 1
            next_val[i] = j
        # 匹配不成功则在前面的子串中继续搜索,直至找不到
        else:
            j = next_val[j]
    return next_val

def KMP(s, p):
    if len(s) < len(p):
        return -1
    i, j = 0, 0
    # 求next数组
    next = get_next(p)
    # 匹配阶段
    while i < len(s) and j < len(p):
        if j == -1 or s[i] == p[j]:
            i += 1
            j += 1
        else:
            j = next[j]
    if j == len(p):
        return i - j
    return -1

if __name__ == "__main__":
    s = "ababababca"
    p = "abababc"
    result = KMP(s, p)
    if result == -1:
        print("False")
    else:
        print(result)

优化代码:去除get_next(),边匹配边计算部分匹配表

def KMP(s, p):
    if len(s) < len(p):
        return -1
    i, j = 0, 0
    p_i, p_j = 0, -1
    next = [-1] * len(p)
    # 边计算部分匹配表,边匹配
    while i < len(s) and j < len(p):
        # 求部分匹配表
        if p_i < len(p)-1:
            if p_j == -1 or p[p_i] == p[p_j]:
                p_i += 1
                p_j += 1
                next[p_i] = p_j
            else:
                p_j = next[p_j]
        # 匹配阶段
        if j == -1 or s[i] == p[j]:
            i += 1
            j += 1
        else:
            j = next[j]
    if j == len(p):
        return i - j
    return -1

if __name__ == "__main__":
    s = "ababababca"
    p = "abababc"
    result = KMP(s, p)
    if result == -1:
        print("False")
    else:
        print(result)

C语言

#include <stdio.h>
#include <stdlib.h>

int KMP(char s[], int len_s, char p[], int len_p) {
    if (len_s < len_p)
        return -1;
    // 创建和初始化部分匹配表
    int* next = (int*)malloc(len_p*sizeof(int));
    for (int i = 0; i < len_p; i++)
        next[i] = -1;
    // 边计算部分匹配表,边匹配
    int i = 0, j = 0, p_i = 0, p_j = -1;
    while (i < len_s && j < len_p) {
        // 求部分匹配表
        if (p_i < len_p-1) {
            if (p_j == -1 || p[p_i] == p[p_j]) {
                p_i++;
                p_j++;
                next[p_i] = p_j;
            }
            else
                p_j = next[p_j];
        }
        // 匹配阶段
        if (j == -1 || s[i] == p[j]) {
            i++;
            j++;
        }
        else
            j = next[j];
    }
    free(next);
    if (j == len_p)
        return i - j;
    return -1;
}

#define LEN(S) sizeof(S)-1

int main(void)
{
#define S "ababababca"
#define P "abababc"
    char s[sizeof(S)] = S;
    char p[sizeof(P)] = P;
    int result = KMP(s, LEN(s), p, LEN(p));
    if (result == -1)
        printf("False\n");
    else
        printf("%d\n", result);
    return 0;
}