6.字符串匹配算法

字符串匹配

字符串匹配问题是计算机科学中研究最为广泛的问题之一,字符串匹配算法发展了十几年,广泛应用于生物信息学、信息检索等领域。我们先来看一个比较常见的字符串匹配问题。

相关概念:

  • 模式串(子串):被查找的短串
  • 文本串(母串):待查找的长串

字符串匹配原则:不重不漏

BF(Brute Force)算法

朴素的模式匹配算法:

  1. 母串S的第一个字符和模式串T的第一个字符进行比较;
  2. 如果相等,则继续比较下一关字符是否相等;
  3. 如果不相等则选择母串S的第二个字符和模式串T第一个字符进行比较
  4. 重复步骤2和3,直至在母串S中匹配到模式串T或者枚举到母串末尾。

BF算法嵌套了两重循环,所以时间复杂度为O(n*m)。BF算法浅显易懂,易于实现。

算法优化思路:可以用之前的匹配信息避免后继一些无用的匹配动作

  1. 借助什么样的匹配信息
  2. 避免哪些无用匹配动作
BF算法代码实现
int brute_force(const char *s, const char *t) {
    for(int i = 0; s[i]; i++) {
        int flag = 1;
        for(int j = 0; t[j]; j++) {
            if(s[i + j] == t[j]) continue;
            flag = 0;
            break;
        }
        if(flag) return i;
    }
    return -1;
}

KMP算法

算法思路:状态机:一个状态根据一个输入跳到另一个状态,KMP非常适合在信息流中匹配字符。

实现步骤:

  1. KMP 算法中,模式串中的第三部分的重要性
  2. 第三部分是可以帮助我们加快匹配速度的,避免掉大量无用的匹配尝试
  3. KMP 算法保证不漏:第三部分匹配到的是模式串的最长前缀
  4. 普通编码:获得 NEXT 数组,使用 NEXT 数组
  5. 高级编码:抽象化了一个状态机模型,j 所指向的就是状态机中的位置
  6. getNext 方法相当于根据输入字符,进行状态跳转,实际上就是改变 j 的值
KMP算法代码实现
int kmp(const char *s, const char *t) {
    int next[100] = {-1};
    for(int i = 1, j = -1; t[i]; i++) {
        while(j != -1 && t[i] != t[j + 1]) j = next[j];
        if(t[i] == t[j + 1]) j++;
        next[i] = j;
    }
    for(int i = 0, j = -1; s[i]; i++) {
        while(j != -1 && s[i] != t[j + 1]) j = next[j];
        if(s[i] == t[j + 1]) j++;
        if(t[j + 1] == 0) return i - j;
    }
    return -1;
}

Sunday算法

SUNDAY 算法理解的核心,在于理解黄金对齐点位,是文本串的匹配尾部,一定会出现在模式串中的字符应该和模式串中最后一位出现该字符的位置对齐,本质就是利用后缀信息进行加锁。最优时间复杂度为n/m,应用场景单词查找(ctrl + F)

实现步骤:

  1. 预处理每一个字符在模式串中最后一次出现的位置
  2. 模拟暴力匹配算法过程,失配的时候,文本串指针根据预处理信息向后移动若干位
sunday算法代码实现
int sunday(const char *s, const char *t) {
    int ind[256], len_t = strlen(t), len_s = strlen(s);
    for(int i = 0; i < 256; i++) ind[i] = len_t + 1;
    for(int i = 0; t[i]; i++) ind[t[i]] = len_t - i;
    for(int i = 0; i + len_t <= len_s; i+= ind[s[len_t + i]]) {
        int flag = 1;
        for(int j = 0; t[j]; j++) {
            if(s[i + j] == t[j]) continue;
            flag = 0;
            break;
        }
        if(flag) return i;
    }
    return -1;
}

shift-and算法

shift-and算法思路:模式串的预处理->字符编码表(二维数组)。相应字符在哪个位置上,哪个位置上的二进制数字就会变为1。p就是一个数字,代表当前的匹配状态 ,以文本串当前位置作为结尾,向前可以匹配成功模式串的前几位,可以匹配成功几位对应的位置上就是1。最后通过P判断是否匹配成功。

实现步骤:

  1. 第一步对模式串做特殊处理,把每一种字符出现的位置,转换成相应的二进制编码。相应字符在哪个位置上,哪个位置上的二进制数字就会变为1。
  2. 后续匹配的过程中跟模式串一毛钱关系都没有
  3. p_i = (p_{i-1}<<1 | 1) \& d[s_i]
  4. p_i第 j 位二进制为1,代表当前位置为结尾,可以匹配成功模式串的第 j 位
shift_and算法实现
int shift_and(const char *s, const char *t) {
    int code[256] = {0}, n = 0, p = 0;
    for(int i = 0; t[i]; i++, n++) code[t[i]] |= (1 << i);
    for(int i = 0; s[i]; i++) {
        p = (p << 1 | 1) & code[s[i]];
        if(p & (1 << (n - 1))) return i - n + 1;
    }
    return -1;
}

Manacher算法

Manacher算法主要用于字符串中最长回文字符的查找。时间复杂度接近线性。

实现步骤:

  1. 首先我们把字符串S改造一下,改造方法是:在S的每个字符之间和S首尾都插入一个"#"。得到的字符串变为确定的奇数个字符。
  2. 定义一个与新串等长的数组统计每一位的回文串(左右相同字符的)长度,根据前面的字符串对称性质减少遍历。
  3. 遍历结果数组,找到左右对称长度最长的一位。
  4. 以该位为终点,左右延伸最长值,去掉其中#字符,得到结果串
Manacher算法实现
/*************************************************************************
	> File Name: 005.最长回文子串.c
	> Author: 陈杰
	> Mail: 15193162746@163.com
	> Created Time: 2021年05月21日 星期五 16时52分50秒
    > 题目描述
    给你一个字符串`s`,找到`s`中最长的回文子串
    > 马拉车算法
 ************************************************************************/
#include<stdio.h>
#include<stdlib.h>
char *longestPalindrome(char * s){
    char ns[2002] = {0};
    int dist[2005]; // 第i为的回文串长度
    ns[0] = '#';
    for(int i = 0; s[i]; i++) {
        ns[2*i + 1] = s[i];
        ns[2*i + 2] = '#';
    }
    // 马拉车算法核心
    int l = 0, r = -1;
    for(int i = 0; ns[i]; i++) {
        // dist[i] 赋初始值
        if(i > r) dist[i] = 1;
        else dist[i] = dist[l + r - i] > (r - i) ? (r - i) : dist[l + r - i];
        // 朴素的回文串判断办法
        while(i - dist[i] >= 0 && ns[i - dist[i]] == ns[i + dist[i]]) dist[i]++;
        // 更新l r
        if(i + dist[i] > r && i - dist[i] > 0) {
            l = i - dist[i];
            r = i + dist[i];
        }
    }
    // 马拉车算法
    char *res = (char *)malloc(sizeof(s));
    int tmp = 0, ind = 0;
    for(int i = 0; ns[i]; i++) {
        if(tmp >= dist[i]) continue;
        tmp = dist[i];
        ind = 0;
        for(int j = i -dist[i] + 1; j < i + dist[i]; j++) {
            if(ns[j] != '#') res[ind] = ns[j], ind++;
        }
    }
    res[ind] = 0;
    return res;
}
int main() {
    char s[1001];
    scanf("%s", s);
    char *res = longestPalindrome(s);
    printf("%s\n", res);
    free(res);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值