【算法笔记】KMP算法

1、KMP算法

KMP算法是一种非常高效且经典的字符串匹配算法,它通过巧妙的预处理机制,显著提升了在主文本串中查找模式串(被包含的字符串)的效率。

1.1、KMP算法的核心思想

  • 在字符串匹配问题中,最直观的方法是暴力匹配,即从主串的每一个可能位置开始,逐个字符与模式串进行比较。
  • 如果失配,主串的指针就回溯到本次起始位置的下一位。
  • 这种方法在最坏情况下的时间复杂度是 O(nm)*(n和m分别是主串和模式串的长度),当字符串很长时效率很低。
  • KMP算法的核心思想是利用模式串自身的结构信息,避免了在匹配过程中不必要的回溯,从而实现线性时间复杂度的字符串匹配。
  • 在实现的过程中,KMP算法先求出模式串的next数组,next数组每个位置之前子串的"最长公共前后缀的长度"信息。
  • 然后在匹配的过程中,主串的下标x从0开始依次往后走,不进行回溯,子串的下标y一开始也从0开始,x和y下标的位置进行匹配,
  • 在这个过程中,x一直不回溯,y根据next数组的信息进行跳转,直到匹配到x或者y到头为止。
  • 在这个过程中有三种情况:
  • 1、x和y匹配成功,x和y都往后走一位。
  • 2、y跳转到了next为-1的时候,即y回到了0位置,x往后走一位。
  • 3、x和y匹配失败,y没有到-1,y根据next数组的信息进行跳转(即y = next[y]),直到匹配到x或者y到头为止。
  • 为什么y可以根据next的信息进行跳转呢?
  • 因为next数组中i位置记录的是i前面的最长公共子串的长度,如果i位置的值为y,则代表模式串str2的str2[0...y-1]等于str2[i-y...i-1],
  • 因为主串str1已经走到了x位置,模式串走到了y位置,此时str1[x] != str2[y],说明str1[x-y...x-1]等于str2[0...y-1],
  • 而在next数组中,y位置的值为j,则代表模式串str2的str2[0...j-1]等于str2[y-j...y-1],
  • 所以可以看出str1[x-j,x-1]、str2[0...j-1]和str2[y-j...y-1]三个数组的值是相等的。
  • 此时y跳到j的位置,只需要继续匹配str1[x]和str2[j]的值是否相等即可,恰面0到j-1的位置因为相等,就可以直接跳过了。
  • KMP算法的核心就是通过这种跳转,避免了在匹配过程中不必要的回溯,从而实现线性时间复杂度的字符串匹配。
  • 复杂度为O(n+m),其中n是主串的长度,m是模式串的长度。

1.2、next数组的含义

  • next数组的含义:
  • next数组的最长公共前后缀的长度的含义包含了几点:
  • 1、i位置的信息不包含i位置,是其前面的信息,且不能是[0,i-1]的范围(即不能包含i前面的所有字符)
  • 2、i位置的信息的相等的子串的两端字符一端必须以0开头,另一端必须以i-1位置的字符结尾,这个相等的子串的长度就是i位置的next数组的值。
  • 3、根据上面的定义,可以知道0位置没有前面的字符,定义为-1,1位置前面只有1个字符,因为不能全包含,所以为0。

1.3、next数组的快速计算

  • 根据next数组的含义,i位置的信息代表了前面的最长公共子串的长度,且其中一个字段是从0开始的,所以i位置的信息就是第一个子串的第一个不相等的位置。
  • 例如:模式串为:ababa,next数组为:[-1,0,0,1,2],对于为4的位置,其值为2,前面的公共子串为ab,所以在字符串中2的位置刚好是第一个子串ab的b后面的一个位置(下标从0开始)。
  • 将原数组记为str,如果要求i位置的值,如果next[i-1]=j,则表示str[0...j-1]等于str[i-1-j...i-1-1],所以j位置和i-1位置分别是第一个和第二个子串不同的位置,
  • 1、此时如果str[j] == str[i-1],则代表在i-位置的基础上,又增加了一个值,所以next[i]=j+1,
  • 比如模式串str为:ababa,next数组为:[-1,0,0,1,2],求i=4位置的next数组值,因为next[i-1]=1,所以str[0,0]str2,2,
  • 此时如果判断str[j] == str[i-1],则代表在i-位置的基础上,又增加了一个值,所以next[i]=j+1=2。即str[4]=2
  • 2、如果str[j] != str[i-1],则代表在i-位置的基础上,不能增加一个值,需要重新判断,此时,如果j已经到了0位置,则代表前面没有公共子串,所以next[i]=0。
  • 比如str为:abcabdt,next数组为:[-1,0,0,0,1,2,?],求i=6位置的next数组值,此时i-1的位置为5,next[5]=2(即j==2),所以str[0,1]str3,4,
  • 此时判断str[j]是否等于str[i-1],因为c不等于d,且next[j]==0,所以next[i]=0。
  • 3、如果str[j] != str[i-1]且j != 0,此时,j要跳到next[j]的位置,继续判断,直到判断出相等或者j最终为0。
  • 比如str为acdbstacdtxeacdbstacdbk,我们要求i=22(即k位置)的next数组,此时i-1的位置为21,next[21]=9(最长前缀为acdbstacd),即j=9,
  • 所以str[0,8]str[12,20] (acdbstacd==acdbstacd),此时判断str[9]是否等于str[21],因为t不等于b,所以不能直接求出next[22]的值,
  • 此时要看next[j]的值,str[9]的字符为t,此时next[9]为3(相等的字符为acd),所以此时的j=next[j]=3,此时继续判断str[3]是否等于str[21],因为str[3]为b,
  • str[21]为b,相等,next[i]=j+1,此时的j为3,所以next[22]=4。即22位置相等的前缀为acdb.
  • 之所以在判断的过程中采用str[i-1]的位置不变,j的位置根据next数组不断往前缩的形式,是因为next数组的性质决定的,因为next要求的前缀公共子串中一个要以0开头,
  • 一个要以i-1结尾,我们就可以不断的根据以前的next的值,尝试前面最短相等的前缀,看看能不能加上i-1位置的字符,如果能加上,就可以直接用前面的值加一求出i的值,
  • 如果加不上,最后就只能为0.

2、KMP算法的实现

java 复制代码
/**
     * KMP算法
     */
    public static int getIndexOf(String s1, String s2) {
        if (s1 == null || s2 == null || s2.isEmpty() || s1.length() < s2.length()) {
            return -1;
        }
        char[] str1 = s1.toCharArray();
        char[] str2 = s2.toCharArray();
        // str1的匹配位置
        int x = 0;
        // str2的匹配位置
        int y = 0;
        // O(M) m <= n
        int[] next = getNextArray(str2);
        // 开始匹配
        while (x < str1.length && y < str2.length) {
            if (str1[x] == str2[y]) {
                // 对应位置相等,匹配下个位置
                x++;
                y++;
            } else if (next[y] == -1) {
                // y到了0的位置,x到下个位置继续匹配
                x++;
            } else {
                // y往前跳转
                y = next[y];
            }
        }
        // 如果匹配成功,y就到了str2的末尾
        // str2在str1里面的起始位置就是x - y(即x - str2.length)
        return y == str2.length ? x - y : -1;
    }


    /**
     * 求next数组
     */
    public static int[] getNextArray(char[] str) {
        if (str == null || str.length == 0) {
            return new int[0];
        }
        if (str.length == 1) {
            return new int[]{-1};
        }
        int[] next = new int[str.length];
        // 0位置为-1,1位置为0
        next[0] = -1;
        next[1] = 0;
        // 求next数组的位置,从2开始
        int i = 2;
        // next数组的跳转的值
        int j = 0;
        while (i < next.length) {
            if (str[i - 1] == str[j]) {
                // 匹配成功,直接在前一个的值上面+1
                next[i++] = ++j;
            } else if (j > 0) {
                // 还可以跳转
                j = next[j];
            } else {
                // j 无法跳转了,目前位置为0
                next[i++] = 0;
            }
        }
        return next;
    }

全部代码和测试:

java 复制代码
/**
 * KMP算法:
 * KMP算法是一种非常高效且经典的字符串匹配算法,它通过巧妙的预处理机制,显著提升了在主文本串中查找模式串(被包含的字符串)的效率。
 *
 * <br>
 * KMP算法的核心思想:
 * 在字符串匹配问题中,最直观的方法是暴力匹配,即从主串的每一个可能位置开始,逐个字符与模式串进行比较。
 * 如果失配,主串的指针就回溯到本次起始位置的下一位。
 * 这种方法在最坏情况下的时间复杂度是 O(nm)*(n和m分别是主串和模式串的长度),当字符串很长时效率很低。
 * KMP算法的核心思想是利用模式串自身的结构信息,避免了在匹配过程中不必要的回溯,从而实现线性时间复杂度的字符串匹配。
 * 在实现的过程中,KMP算法先求出模式串的next数组,next数组每个位置之前子串的"最长公共前后缀的长度"信息。
 * 然后在匹配的过程中,主串的下标x从0开始依次往后走,不进行回溯,子串的下标y一开始也从0开始,x和y下标的位置进行匹配,
 * 在这个过程中,x一直不回溯,y根据next数组的信息进行跳转,直到匹配到x或者y到头为止。
 * 在这个过程中有三种情况:
 * 1、x和y匹配成功,x和y都往后走一位。
 * 2、y跳转到了next为-1的时候,即y回到了0位置,x往后走一位。
 * 3、x和y匹配失败,y没有到-1,y根据next数组的信息进行跳转(即y = next[y]),直到匹配到x或者y到头为止。
 * 为什么y可以根据next的信息进行跳转呢?
 * 因为next数组中i位置记录的是i前面的最长公共子串的长度,如果i位置的值为y,则代表模式串str2的str2[0...y-1]等于str2[i-y...i-1],
 * 因为主串str1已经走到了x位置,模式串走到了y位置,此时str1[x] != str2[y],说明str1[x-y...x-1]等于str2[0...y-1],
 * 而在next数组中,y位置的值为j,则代表模式串str2的str2[0...j-1]等于str2[y-j...y-1],
 * 所以可以看出str1[x-j,x-1]、str2[0...j-1]和str2[y-j...y-1]三个数组的值是相等的。
 * 此时y跳到j的位置,只需要继续匹配str1[x]和str2[j]的值是否相等即可,恰面0到j-1的位置因为相等,就可以直接跳过了。
 * KMP算法的核心就是通过这种跳转,避免了在匹配过程中不必要的回溯,从而实现线性时间复杂度的字符串匹配。
 * 复杂度为O(n+m),其中n是主串的长度,m是模式串的长度。
 * <br>
 * next数组的含义:
 * next数组的最长公共前后缀的长度的含义包含了几点:
 * 1、i位置的信息不包含i位置,是其前面的信息,且不能是[0,i-1]的范围(即不能包含i前面的所有字符)
 * 2、i位置的信息的相等的子串的两端字符一端必须以0开头,另一端必须以i-1位置的字符结尾,这个相等的子串的长度就是i位置的next数组的值。
 * 3、根据上面的定义,可以知道0位置没有前面的字符,定义为-1,1位置前面只有1个字符,因为不能全包含,所以为0。
 * <br>
 * next数组的快速计算:
 * 根据next数组的含义,i位置的信息代表了前面的最长公共子串的长度,且其中一个字段是从0开始的,所以i位置的信息就是第一个子串的第一个不相等的位置。
 * 例如:模式串为:ababa,next数组为:[-1,0,0,1,2],对于为4的位置,其值为2,前面的公共子串为ab,所以在字符串中2的位置刚好是第一个子串ab的b后面的一个位置(下标从0开始)。
 * 将原数组记为str,如果要求i位置的值,如果next[i-1]=j,则表示str[0...j-1]等于str[i-1-j...i-1-1],所以j位置和i-1位置分别是第一个和第二个子串不同的位置,
 * 1、此时如果str[j] == str[i-1],则代表在i-位置的基础上,又增加了一个值,所以next[i]=j+1,
 * 比如模式串str为:ababa,next数组为:[-1,0,0,1,2],求i=4位置的next数组值,因为next[i-1]=1,所以str[0,0]str[2,2](str的0位置和2位置的a相等),
 * 此时如果判断str[j] == str[i-1],则代表在i-位置的基础上,又增加了一个值,所以next[i]=j+1=2。即str[4]=2
 * 2、如果str[j] != str[i-1],则代表在i-位置的基础上,不能增加一个值,需要重新判断,此时,如果j已经到了0位置,则代表前面没有公共子串,所以next[i]=0。
 * 比如str为:abcabdt,next数组为:[-1,0,0,0,1,2,?],求i=6位置的next数组值,此时i-1的位置为5,next[5]=2(即j==2),所以str[0,1]str[3,4](ab==ab),
 * 此时判断str[j]是否等于str[i-1],因为c不等于d,且next[j]==0,所以next[i]=0。
 * 3、如果str[j] != str[i-1]且j != 0,此时,j要跳到next[j]的位置,继续判断,直到判断出相等或者j最终为0。
 * 比如str为acdbstacdtxeacdbstacdbk,我们要求i=22(即k位置)的next数组,此时i-1的位置为21,next[21]=9(最长前缀为acdbstacd),即j=9,
 * 所以str[0,8]str[12,20](acdbstacd==acdbstacd),此时判断str[9]是否等于str[21],因为t不等于b,所以不能直接求出next[22]的值,
 * 此时要看next[j]的值,str[9]的字符为t,此时next[9]为3(相等的字符为acd),所以此时的j=next[j]=3,此时继续判断str[3]是否等于str[21],因为str[3]为b,
 * str[21]为b,相等,next[i]=j+1,此时的j为3,所以next[22]=4。即22位置相等的前缀为acdb.
 * 之所以在判断的过程中采用str[i-1]的位置不变,j的位置根据next数组不断往前缩的形式,是因为next数组的性质决定的,因为next要求的前缀公共子串中一个要以0开头,
 * 一个要以i-1结尾,我们就可以不断的根据以前的next的值,尝试前面最短相等的前缀,看看能不能加上i-1位置的字符,如果能加上,就可以直接用前面的值加一求出i的值,
 * 如果加不上,最后就只能为0.
 */
public class KMP {

    /**
     * KMP算法
     */
    public static int getIndexOf(String s1, String s2) {
        if (s1 == null || s2 == null || s2.isEmpty() || s1.length() < s2.length()) {
            return -1;
        }
        char[] str1 = s1.toCharArray();
        char[] str2 = s2.toCharArray();
        // str1的匹配位置
        int x = 0;
        // str2的匹配位置
        int y = 0;
        // O(M) m <= n
        int[] next = getNextArray(str2);
        // 开始匹配
        while (x < str1.length && y < str2.length) {
            if (str1[x] == str2[y]) {
                // 对应位置相等,匹配下个位置
                x++;
                y++;
            } else if (next[y] == -1) {
                // y到了0的位置,x到下个位置继续匹配
                x++;
            } else {
                // y往前跳转
                y = next[y];
            }
        }
        // 如果匹配成功,y就到了str2的末尾
        // str2在str1里面的起始位置就是x - y(即x - str2.length)
        return y == str2.length ? x - y : -1;
    }


    /**
     * 求next数组
     */
    public static int[] getNextArray(char[] str) {
        if (str == null || str.length == 0) {
            return new int[0];
        }
        if (str.length == 1) {
            return new int[]{-1};
        }
        int[] next = new int[str.length];
        // 0位置为-1,1位置为0
        next[0] = -1;
        next[1] = 0;
        // 求next数组的位置,从2开始
        int i = 2;
        // next数组的跳转的值
        int j = 0;
        while (i < next.length) {
            if (str[i - 1] == str[j]) {
                // 匹配成功,直接在前一个的值上面+1
                next[i++] = ++j;
            } else if (j > 0) {
                // 还可以跳转
                j = next[j];
            } else {
                // j 无法跳转了,目前位置为0
                next[i++] = 0;
            }
        }
        return next;
    }

    public static void main(String[] args) {
        int possibilities = 5;
        int strSize = 20;
        int matchSize = 5;
        int testTimes = 5000000;
        System.out.println("测试开始");
        for (int i = 0; i < testTimes; i++) {
            String str = getRandomString(possibilities, strSize);
            String match = getRandomString(possibilities, matchSize);
            int ans = getIndexOf(str, match);
            int comparator = str.indexOf(match);
            if (ans != comparator) {
                System.out.println("测试失败!");
                System.out.printf("str:%s,match:%s,ans:%d,comparator:%d", str, match, ans, comparator);
                break;
            }
        }
        System.out.println("测试结束");
    }

    // for test
    public static String getRandomString(int possibilities, int size) {
        char[] ans = new char[(int) (Math.random() * size) + 1];
        for (int i = 0; i < ans.length; i++) {
            ans[i] = (char) ((int) (Math.random() * possibilities) + 'a');
        }
        return String.valueOf(ans);
    }
}

3、KMP算法的练习题目

3.1、另一棵树的子树

  • 题目一:另一棵树的子树
  • 给定两棵二叉树的头节点head1和head2,返回head1中是否有某个子树的结构和head2完全一样
  • 测试链接 : https://leetcode.cn/problems/subtree-of-another-tree/
  • 这种算法不是这个题的最优解,但是可以用这种思路解
  • 思路:
    • 将两个数按照同一种遍历方式进行遍历,形成一个数组,然后用kmp算法判断subRoot的数组是否是root的数组的子数组
java 复制代码
    /**
     * 判断root是否包含subRoot的子树
     * 思路:
     * 将两个数按照同一种遍历方式进行遍历,形成一个数组,然后用kmp算法判断subRoot的数组是否是root的数组的子数组
     */
    public static boolean isSubtree(TreeNode root, TreeNode subRoot) {
        if (subRoot == null) {
            return true;
        }
        if (root == null) {
            return false;
        }
        // 先用前序遍历两棵树,然后转为数组
        ArrayList<String> rootList = new ArrayList<>();
        ArrayList<String> subRootList = new ArrayList<>();
        preSerial(root, rootList);
        preSerial(subRoot, subRootList);
        // 转为数组
        String[] rootStr = new String[rootList.size()];
        for (int i = 0; i < rootStr.length; i++) {
            rootStr[i] = rootList.get(i);
        }
        String[] subRootStr = new String[subRootList.size()];
        for (int i = 0; i < subRootStr.length; i++) {
            subRootStr[i] = subRootList.get(i);
        }
        // 用kmp算法判断subRoot的数组是否是root的数组的子数组
        return getIndexOf(rootStr, subRootStr) != -1;
    }

    /**
     * KMP算法判断subRootStr是否是rootStr的子数组
     */
    private static int getIndexOf(String[] rootStr, String[] subRootStr) {
        if (rootStr == null || subRootStr == null || rootStr.length < 1 || rootStr.length < subRootStr.length) {
            return -1;
        }
        int x = 0;
        int y = 0;
        int[] next = getNextArray(subRootStr);
        while (x < rootStr.length && y < subRootStr.length) {
            if (isEqual(rootStr[x], subRootStr[y])) {
                x++;
                y++;
            } else if (next[y] == -1) {
                x++;
            } else {
                y = next[y];
            }
        }
        return y == subRootStr.length ? x - y : -1;
    }

    public static int[] getNextArray(String[] ms) {
        if (ms.length == 1) {
            return new int[]{-1};
        }
        int[] next = new int[ms.length];
        next[0] = -1;
        next[1] = 0;
        int i = 2;
        int cn = 0;
        while (i < next.length) {
            if (isEqual(ms[i - 1], ms[cn])) {
                next[i++] = ++cn;
            } else if (cn > 0) {
                cn = next[cn];
            } else {
                next[i++] = 0;
            }
        }
        return next;
    }

    public static boolean isEqual(String a, String b) {
        if (a == null && b == null) {
            return true;
        } else {
            if (a == null || b == null) {
                return false;
            } else {
                return a.equals(b);
            }
        }
    }


    /**
     * 前序遍历,放入list中
     * 因为有顺序,这里用ArrayList,
     * 因为要填充null,所以要转为string,不能直接用int
     */
    public static void preSerial(TreeNode node, ArrayList<String> res) {
        if (node == null) {
            res.add(null);
        } else {
            res.add(String.valueOf(node.val));
            preSerial(node.left, res);
            preSerial(node.right, res);
        }
    }

3.2、旋转字符串

  • 题目一:旋转字符串
  • 判断str1和str2是否互为旋转字符串,
  • s的旋转操作就是将s最左边的字符移动到最右边,类似于二进制的按位循环左移。
  • 例如, 若 s = 'abcde',在旋转一次之后结果就是'bcdea'
  • 测试链接 : https://leetcode.cn/problems/rotate-string/
  • 思路:
    • 因为这里的旋转字符是类似于按位左移的效果,所以可以把一个字符串直接拼接上本上,那么移动后的字符串就会包含在拼接后的字符串中
    • 判断是否包含,可以用kmp算法
java 复制代码
    /**
     * 判断str1和str2是否互为旋转字符串
     * 思路:
     * 因为这里的旋转字符是类似于按位左移的效果,所以可以把一个字符串直接拼接上本上,那么移动后的字符串就会包含在拼接后的字符串中
     * 判断是否包含,可以用kmp算法
     */
    public static boolean rotateString(String a, String b) {
        if (a == null || b == null || a.length() != b.length()) {
            return false;
        }
        String b2 = b + b;
        return getIndexOf(b2, a) != -1;
    }

    // KMP Algorithm
    public static int getIndexOf(String s, String m) {
        if (s.length() < m.length()) {
            return -1;
        }
        char[] ss = s.toCharArray();
        char[] ms = m.toCharArray();
        int si = 0;
        int mi = 0;
        int[] next = getNextArray(ms);
        while (si < ss.length && mi < ms.length) {
            if (ss[si] == ms[mi]) {
                si++;
                mi++;
            } else if (next[mi] == -1) {
                si++;
            } else {
                mi = next[mi];
            }
        }
        return mi == ms.length ? si - mi : -1;
    }

    public static int[] getNextArray(char[] ms) {
        if (ms.length == 1) {
            return new int[]{-1};
        }
        int[] next = new int[ms.length];
        next[0] = -1;
        next[1] = 0;
        int pos = 2;
        int cn = 0;
        while (pos < next.length) {
            if (ms[pos - 1] == ms[cn]) {
                next[pos++] = ++cn;
            } else if (cn > 0) {
                cn = next[cn];
            } else {
                next[pos++] = 0;
            }
        }
        return next;
    }

后记

个人学习总结笔记,不能保证非常详细,轻喷

相关推荐
是苏浙42 分钟前
蓝桥杯备战day1
算法
汉克老师43 分钟前
CCF-NOI2025第一试题目与解析(第二题、序列变换(sequence))
c++·算法·动态规划·noi
程序员东岸1 小时前
《数据结构——排序(下)》分治与超越:快排、归并与计数排序的终极对决
数据结构·c++·经验分享·笔记·学习·算法·排序算法
无限进步_1 小时前
C++初始化列表详解:语法、规则与最佳实践
java·开发语言·数据库·c++·git·github·visual studio
vx_bisheyuange1 小时前
基于SpringBoot的交通在线管理服务系统
java·spring boot·后端·毕业设计
Hello.Reader1 小时前
FF4J 用特性开关玩转 Java 应用灰度与发布
java·开发语言
某林2121 小时前
在slam建图中为何坐标base_link,laser,imu_link是始终在一起的,但是odom 会与这位三个坐标在运行中产生偏差
人工智能·算法
想看一次满天星1 小时前
阿里140-n值纯算
爬虫·python·算法·网络爬虫·阿里140
Keep__Fighting1 小时前
【机器学习:逻辑回归】
人工智能·python·算法·机器学习·逻辑回归·scikit-learn·matplotlib