• 数据结构篇——KMP算法


    本次我们介绍数据结构中的KMP算法,我们会从下面几个角度来介绍:

    • 问题介绍
    • 暴力求解
    • 知识补充
    • Next示例
    • Next代码
    • 匹配示例
    • 匹配代码
    • 完整代码

    问题介绍

    首先我们先介绍适用于KMP算法的问题:

    • 给定一个字符串S,以及一个模式串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
    • 模式串P在字符串S中多次作为子串出现。
    • 求出模式串P在字符串S中所有出现的位置的起始下标。

    我们给出一个问题的简单示例:

    1. // 输入 p长度 p s长度 s
    2. 3
    3. aba
    4. 5
    5. ababa
    6. // 输出结果
    7. 0 2

    暴力求解

    所有问题我们都是在暴力求解的基础上进行更新迭代的,所以我们首先给出暴力求解:

    1. // 下面为伪代码,只是起到思路作用
    2. // 首先我们需要创造s[],p[],并赋值
    3. S[N],P[N]
    4. // 然后我们开始匹配,我们会从S的第一个字符开始匹配,设置一个flag判断该字符开始的字符串是否与P字符匹配
    5. // 该算法从每个i开始,全部进行匹配
    6. for(int i = 1;i <= n;i++ ){
    7. boolean flag = true;
    8. for(int j = 1;j <= m;j++){
    9. if(s[i+j-1] != p[j]){
    10. flag = false;
    11. break;
    12. }
    13. }
    14. }
    15. // 我们给出一套完整的暴力求解方法
    16. /**
    17. * 暴力破解法
    18. * @param ts 主串
    19. * @param ps 模式串
    20. * @return 如果找到,返回在主串中第一个字符出现的下标,否则为-1
    21. */
    22. public static int bf(String ts, String ps) {
    23. char[] t = ts.toCharArray();
    24. char[] p = ps.toCharArray();
    25. int i = 0; // 主串的位置
    26. int j = 0; // 模式串的位置
    27. while (i < t.length && j < p.length) {
    28. if (t[i] == p[j]) {
    29. // 当两个字符相同,就比较下一个
    30. i++;
    31. j++;
    32. } else {
    33. i = i - j + 1; // 一旦不匹配,i后退(从之前i的下一位开始,也是遍历所有i)
    34. j = 0; // j归0
    35. }
    36. }
    37. // 当上面循环结束,必定是i到头或者j到头,如果是j到头,则说明存在子串符合父串,我们就将头位置i返回
    38. if (j == p.length) {
    39. return i - j;
    40. } else {
    41. return -1;
    42. }
    43. }
    44. // 但是我们会发现:我们可以不让i回退而是让j回退,使j回退到能够与当前i相匹配的点位,然后继续进行主串和模式串的匹配

    首先我们会发现这个算法的时间复杂度为O(n^2)

    我们其中可以优化的点就是i的位置更新,我们可以根据p字符串的特性来判断i在失败后最近可以移动到哪个点位!

    知识补充

    我们为了学习KMP算法,我们需要补充一些下面会用到的知识:

    • s[ ]是模式串,即比较长的字符串。
    • p[ ]是模板串,即比较短的字符串。(这样可能不严谨。。。)
    • “非平凡前缀”:指除了最后一个字符以外,一个字符串的全部头部组合。
    • “非平凡后缀”:指除了第一个字符以外,一个字符串的全部尾部组合。(后面会有例子,均简称为前/后缀)
    • “部分匹配值”:前缀和后缀的最长共有元素的长度。
    • next[ ]是“部分匹配值表”,即next数组,它存储的是每一个下标对应的“部分匹配值”,是KMP算法的核心。(后面作详细讲解)。

    我们所用到的思想是:

    • 在每次失配时,不是把p串往后移一位,而是把p串往后移动至下一次可以和前面部分匹配的位置,这样就可以跳过大多数的失配步骤
    • 而每次p串移动的步数就是通过查找next[ ]数组确定的

    Next示例

    我们给出一个简单的Next示例:

    1. // 首先我们给出一个next手写实例
    2. /*
    3. 模板串为:ABABAA
    4. next[0]代表t[0]-t[0],即"A" , "A"的前缀和后缀都为空集,共有元素的长度为0.
    5. next[1]代表t[0]-t[1],即"AB",前缀为“A”,后缀为“B”,共有元素的长度为0..
    6. next[2]代表t[0]~t[2],即"ABA",前缀为“AB",后缀为"BA",最大前后缀即"A",长度为1.
    7. next[3]代表t[0]~t[3],即"ABAB",前缀为"ABA"后缀为"BAB”,最大前后缀即"AB ",长度为2.
    8. next[4]代表t[0]~t[4],即"ABABA",前缀为"ABAB",后缀为"BABA",最大前后缀即" ABA",长度为3.
    9. next[5]代表t[0]-t[5],即" ABABAA",前缀为“ABABA",T后缀为“BABAA";最大前后缀即"A",长度为1.
    10. */
    11. // 我们next的作用是使next[j]=k使 P[0 ~ k-1] == P[j-k ~ j-1]、
    12. // 当第n个数不匹配时,我们让j回退到k,这时我们的主串和模式串的前缀还属于匹配状态,我们继续进行匹配
    13. 例如 ababc
    14. 我们如果匹配到c不符合时,我们可以使j回退到k(这里的k是2,即a)再继续进行匹配
    15. 因为我们的c前面的ab和开头的ab是匹配的,我们主串中的i前面肯定也是ab,我们的l前面也是ab,所以两者匹配,我们可以继续后面的匹配
    16. 相当于我们的x不变,我们将j放在2的位置,前面的ab已完成匹配,我们只需要匹配abc即可
    17. // 公式书写就是下述:
    18. 当T[i] != P[j]时
    19. 有T[i-j ~ i-1] == P[0 ~ j-1]
    20. 由P[0 ~ k-1] == P[j-k ~ j-1]
    21. 必然:T[i-k ~ i-1] == P[0 ~ k-1]

    Next代码

    我们给出求解Next的代码展示:

    1. public static int[] getNext(String ps) {
    2. char[] p = ps.toCharArray();
    3. int[] next = new int[p.length];
    4. // 这里的next[0]需要等于-1
    5. // 因为j在最左边时,不可能再移动j了,这时候要应该是i指针后移。所以在代码中才会有next[0] = -1;这个初始化。
    6. next[0] = -1;
    7. // 这里设置j的初始值从第一个开始(我们需要得到全部next数组)
    8. int j = 0;
    9. // 这里设置k,k就是应该返回的位置,也就是我们常说的前缀和后缀匹配区域的前缀的后一个位置
    10. int k = -1;
    11. // 进行循环,得到next数组
    12. while (j < p.length - 1) {
    13. // 首先是k==-1时,说明前面已无匹配状态,我们重新开始
    14. // 然后是p[j] == p[k],说明循环时新添加的值,与我们应该返回比对的位置相同
    15. // 同时由于我们之前的部分都是已经匹配成功的,所以加上这个数使我们的匹配长度又增加一位
    16. if (k == -1 || p[j] == p[k]) {
    17. // 当两个字符相等时要跳过(因为p[k]与S[i]不符合的话,由于我们的p[j]=p[k],所以肯定也不符合,我们直接跳下一步)
    18. if (p[++j] == p[++k]) {
    19. next[j] = next[k];
    20. } else {
    21. // 因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
    22. // 这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
    23. // 即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1
    24. // 前面我们已经进行了j++和k++,所以这里直接赋值即可
    25. next[j] = k;
    26. }
    27. } else {
    28. // 如果当前状态无法匹配,我们就跳回上一个前缀后缀相同部分再来判断是否前后缀相同
    29. k = next[k];
    30. }
    31. }
    32. return next;
    33. }

    匹配示例

    我们给出简单的匹配示例:

    1. // 匹配相对而言就比较简单了
    2. 主串:abababc
    3. 模式串:abc
    4. 我们首先进行i++,j++范围的匹配,当第三位,即a和c匹配不成功时,我们不移动i,而是移动j
    5. 我们将j=2,移动到j=0,即next[2]的位置,在之后一直匹配并再对j进行一次移动,到最后匹配成功为止

    匹配代码

    我们给出对应的匹配代码:

    1. /*该代码实际上是由暴力求解代码改造过来的*/
    2. public static int KMP(String ts, String ps) {
    3. char[] t = ts.toCharArray();
    4. char[] p = ps.toCharArray();
    5. int i = 0; // 主串的位置
    6. int j = 0; // 模式串的位置
    7. int[] next = getNext(ps);
    8. // 开始判断(设置边界值)
    9. while (i < t.length && j < p.length) {
    10. // 当j为-1时,要移动的是i,当然j也要归0
    11. // 如果匹配成功,两者都进行移动,开始下一位比对
    12. if (j == -1 || t[i] == p[j]) {
    13. i++;
    14. j++;
    15. } else {
    16. // 如果比对失败,我们将 j 返回next数组指定位置继续匹配
    17. // i不需要回溯了
    18. // i = i - j + 1;
    19. j = next[j]; // j回到指定位置
    20. }
    21. }
    22. // 最后同样进行判断,是否符合条件
    23. if (j == p.length) {
    24. return i - j;
    25. } else {
    26. return -1;
    27. }
    28. }

    完整代码

    最后为大家展示一下完整代码:

    1. import java.util.Scanner;
    2. class ppp {
    3. /**
    4. * 主代码
    5. * @param args
    6. */
    7. public static void main(String[] args) {
    8. Scanner scanner = new Scanner(System.in);
    9. String ts = scanner.nextLine();
    10. String ps = scanner.nextLine();
    11. int kmp = KMP(ts, ps);
    12. System.out.println(kmp);
    13. }
    14. /**
    15. * kmp算法
    16. * @param ts
    17. * @param ps
    18. * @return
    19. */
    20. public static int KMP(String ts, String ps) {
    21. char[] t = ts.toCharArray();
    22. char[] p = ps.toCharArray();
    23. int i = 0; // 主串的位置
    24. int j = 0; // 模式串的位置
    25. int[] next = getNext(ps);
    26. // 开始判断(设置边界值)
    27. while (i < t.length && j < p.length) {
    28. // 当j为-1时,要移动的是i,当然j也要归0
    29. // 如果匹配成功,两者都进行移动,开始下一位比对
    30. if (j == -1 || t[i] == p[j]) {
    31. i++;
    32. j++;
    33. } else {
    34. // 如果比对失败,我们将 j 返回next数组指定位置继续匹配
    35. // i不需要回溯了
    36. // i = i - j + 1;
    37. j = next[j]; // j回到指定位置
    38. }
    39. }
    40. // 最后同样进行判断,是否符合条件
    41. if (j == p.length) {
    42. return i - j;
    43. } else {
    44. return -1;
    45. }
    46. }
    47. /**
    48. * next数组求解
    49. * @param ps
    50. * @return
    51. */
    52. public static int[] getNext(String ps) {
    53. char[] p = ps.toCharArray();
    54. int[] next = new int[p.length];
    55. // 这里的next[0]需要等于-1
    56. // 因为j在最左边时,不可能再移动j了,这时候要应该是i指针后移。所以在代码中才会有next[0] = -1;这个初始化。
    57. next[0] = -1;
    58. // 这里设置j的初始值从第一个开始(我们需要得到全部next数组)
    59. int j = 0;
    60. // 这里设置k,k就是应该返回的位置,也就是我们常说的前缀和后缀匹配区域的前缀的后一个位置
    61. int k = -1;
    62. // 进行循环,得到next数组
    63. while (j < p.length - 1) {
    64. // 首先是k==-1时,说明前面已无匹配状态,我们重新开始
    65. // 然后是p[j] == p[k],说明循环时新添加的值,与我们应该返回比对的位置相同
    66. // 同时由于我们之前的部分都是已经匹配成功的,所以加上这个数使我们的匹配长度又增加一位
    67. if (k == -1 || p[j] == p[k]) {
    68. // 当两个字符相等时要跳过
    69. //(因为p[k]与S[i]不符合的话,由于我们的p[j]=p[k],所以肯定也不符合,我们直接跳下一步)
    70. if (p[++j] == p[++k]) {
    71. next[j] = next[k];
    72. } else {
    73. // 因为在P[j]之前已经有P[0 ~ k-1] == p[j-k ~ j-1]。(next[j] == k)
    74. // 这时候现有P[k] == P[j],我们是不是可以得到P[0 ~ k-1] + P[k] == p[j-k ~ j-1] + P[j]。
    75. // 即:P[0 ~ k] == P[j-k ~ j],即next[j+1] == k + 1 == next[j] + 1
    76. // 前面我们已经进行了j++和k++,所以这里直接赋值即可
    77. next[j] = k;
    78. }
    79. } else {
    80. // 如果当前状态无法匹配,我们就跳回上一个前缀后缀相同部分再来判断是否前后缀相同
    81. k = next[k];
    82. }
    83. }
    84. return next;
    85. }
    86. }
  • 相关阅读:
    【Linux】Linux编译器-gcc/g++使用
    《深度学习在医学图像分析中的应用(第二版)》
    【愚公系列】2022年12月 使用win11系统自带SSH,远程控制VMware中Liunx虚拟机系统
    为PDF创建目录(侧边栏目录)
    设计模式---工厂模式(创建型)
    [Spring cloud alibaba][Sentinel][Gateway] 微服务整合sentinel流控未发现注册服务(监控空白问题)
    金仓数据库KingbaseES客户端编程接口指南-ado.net(7. Kdbnpg支持的类型和类型映射)
    2022一文了解科技特长生
    macOS telnet替代方式
    【RabbitMQ实战】07 3分钟部署一个RabbitMQ集群
  • 原文地址:https://blog.csdn.net/sinat_40572875/article/details/127932584