java字符串匹配算法 KMP算法


Java中的KMP算法是一种字符串匹配算法,其基本思想是利用已知信息跳过不必要的比较。

在KMP算法中,主要有两个步骤:预处理和匹配。

  1. 预处理

在预处理步骤中,我们需要创建一个前缀数组next,用来记录在当前字符之前的子串中有多大长度的相同前缀后缀,以便在匹配过程中跳过一些不必要的比较。

具体的实现方式如下:

public static int[] getNext(String pattern) {
    int n = pattern.length();
    int[] next = new int[n];
    next[0] = -1;
    int i = 0, j = -1;
    while (i < n - 1) {
        if (j == -1 || pattern.charAt(i) == pattern.charAt(j)) {
            i++;
            j++;
            next[i] = j;
        } else {
            j = next[j];
        }
    }
    return next;
}

在上面的代码中,我们使用两个指针i和j来遍历字符串pattern,其中i表示后缀的末尾,j表示前缀的末尾。初始时,i和j都是0。当i和j指向的字符相等时,我们将i和j都加1,并将next[i]设置为j。如果i和j指向的字符不相等,则我们将j更新为next[j],继续与i指向的字符进行比较。

在执行完上述步骤之后,我们得到了一个前缀数组next,它记录了在当前字符之前的子串中有多大长度的相同前缀后缀。这将在匹配步骤中帮助我们跳过不必要的比较。

  1. 匹配

在匹配步骤中,我们使用两个指针i和j来遍历字符串text和pattern,其中i表示text的当前位置,j表示pattern的当前位置。初始时,i和j都是0。

具体的实现方式如下:

public static int kmp(String text, String pattern) {
    int m = text.length(), n = pattern.length();
    int[] next = getNext(pattern);
    int i = 0, j = 0;
    while (i < m && j < n) {
        if (j == -1 || text.charAt(i) == pattern.charAt(j)) {
            i++;
            j++;
        } else {
            j = next[j];
        }
    }
    if (j == n) {
        return i - j;
    }
    return -1;
}

在上面的代码中,我们首先调用getNext方法来获取前缀数组next,然后使用两个指针i和j来遍历字符串text和pattern。如果text.charAt(i)等于pattern.charAt(j),则i和j都加1,继续比较下一个字符。如果text.charAt(i)不等于pattern.charAt(j),则将j更新为next[j],继续与text.charAt(i)进行比较。

在匹配过程中,如果j等于n,说明pattern已经完全匹配text,返回i-j即可。如果匹配失败,则返回-1。

KMP算法的时间复杂度为O(m+n),其中m是text的长度,n是pattern的长度。在预处理步骤中,我们需要遍历整个pattern,并为每个字符计算前缀数组next。在匹配步骤中,我们需要遍历整个text,并使用next数组跳过一些不必要的比较。

KMP算法的优点在于可以利用已知信息跳过不必要的比较,因此其时间复杂度比朴素的字符串匹配算法要低。但是,KMP算法需要使用额外的空间来存储前缀数组next,因此其空间复杂度也相应较高。

下面是一个使用KMP算法实现字符串匹配的例子:

public class KMP {
    public static int kmp(String text, String pattern) {
        int m = text.length(), n = pattern.length();
        int[] next = getNext(pattern);
        int i = 0, j = 0;
        while (i < m && j < n) {
            if (j == -1 || text.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
            } else {
                j = next[j];
            }
        }
        if (j == n) {
            return i - j;
        }
        return -1;
    }

    public static int[] getNext(String pattern) {
        int n = pattern.length();
        int[] next = new int[n];
        next[0] = -1;
        int i = 0, j = -1;
        while (i < n - 1) {
            if (j == -1 || pattern.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
                next[i] = j;
            } else {
                j = next[j];
            }
        }
        return next;
    }

    public static void main(String[] args) {
        String text = "hello world";
        String pattern = "world";
        int index = kmp(text, pattern);
        if (index == -1) {
            System.out.println("Pattern not found");
        } else {
            System.out.println("Pattern found at index " + index);
        }
    }
}

在上面的例子中,我们首先定义了一个KMP类,其中包含了kmp方法和getNext方法。在main方法中,我们使用kmp方法来查找字符串text中是否包含字符串pattern,并输出结果。

以下是对KMP算法的补充说明和示例代码:

KMP算法的核心思想是通过利用已匹配的信息,跳过不必要的比较,从而提高字符串匹配的效率。它利用前缀函数(也称为部分匹配表)来确定在匹配过程中出现不匹配时,应该将模式串的指针向前移动多少位,而不是直接回溯到模式串的起始位置。

补充说明:

KMP算法的前缀函数(部分匹配表)的构建是关键步骤之一。下面是对getNext方法的具体说明:

  1. 创建长度为n(模式串的长度)的整型数组next,并将next[0]初始化为-1,表示模式串的第一个字符之前没有前缀。
  2. 设置两个指针ij,初始时i=0j=-1
  3. 从位置1开始遍历模式串,计算next数组的值:
    • 如果j==-1,或者模式串的第i个字符与第j个字符相等,则令i++j++,并将next[i]赋值为j
    • 如果模式串的第i个字符与第j个字符不相等,则令j=next[j],即将指针j回退到next[j]的位置,继续进行比较。
  4. 返回构建好的next数组。

下面是一个示例代码,演示了如何使用KMP算法进行字符串匹配:

public class KMPAlgorithm {
    public static int kmp(String text, String pattern) {
        int m = text.length();
        int n = pattern.length();
        int[] next = getNext(pattern);

        int i = 0; // 指向text的指针
        int j = 0; // 指向pattern的指针

        while (i < m && j < n) {
            if (j == -1 || text.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
            } else {
                j = next[j];
            }
        }

        if (j == n) {
            return i - j; // 返回匹配的起始位置
        }

        return -1; // 匹配失败
    }

    private static int[] getNext(String pattern) {
        int n = pattern.length();
        int[] next = new int[n];
        next[0] = -1;

        int i = 0;
        int j = -1;

        while (i < n - 1) {
            if (j == -1 || pattern.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
                next[i] = j;
            } else {
                j = next[j];
            }
        }

        return next;
    }

    public static void main(String[] args) {
        String text = "ABABDABACDABABCABAB";
        String pattern = "ABABCABAB";
        int index = kmp(text, pattern); if (index == -1) { System.out.println("Pattern not found"); } else { System.out.println("Pattern found at index " + index); 
    }
    }
    }

在上面的示例中,我们使用KMPAlgorithm类来演示KMP算法的使用。在main方法中,我们定义了一个文本字符串text和一个模式字符串pattern,然后调用kmp方法来查找模式字符串在文本中的位置。如果找到了匹配的位置,我们输出其起始索引;否则,输出"Pattern not found"。

运行上述示例代码,输出结果为:

Pattern found at index 10

这表明模式字符串"ABABCABAB"在文本字符串"ABABDABACDABABCABAB"中的起始位置是索引10。

通过KMP算法,我们可以高效地进行字符串匹配,尤其适用于处理大文本和复杂模式的情况。它的时间复杂度为O(m+n),其中m是文本的长度,n是模式的长度。相较于朴素的字符串匹配算法,KMP算法通过利用已知信息跳过不必要的比较,提高了匹配效率。

当使用KMP算法进行字符串匹配时,有几点需要注意:

  1. 当模式串为空字符串时,KMP算法将始终返回0,表示匹配成功。因此,在使用KMP算法前,需要确保模式串不为空。
  2. 如果需要查找所有匹配的位置,而不仅仅是第一个匹配位置,可以使用一个循环来遍历整个文本字符串,并在每次匹配成功时更新匹配位置。

下面是一个扩展示例,展示如何查找所有匹配位置的代码:

public class KMPAlgorithm {
    public static List<Integer> kmp(String text, String pattern) {
        List<Integer> matches = new ArrayList<>();
        int m = text.length();
        int n = pattern.length();
        int[] next = getNext(pattern);

        int i = 0; // 指向text的指针
        int j = 0; // 指向pattern的指针

        while (i < m) {
            if (j == -1 || text.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
            } else {
                j = next[j];
            }

            if (j == n) {
                matches.add(i - j);
                j = next[j];
            }
        }

        return matches;
    }

    private static int[] getNext(String pattern) {
        int n = pattern.length();
        int[] next = new int[n];
        next[0] = -1;

        int i = 0;
        int j = -1;

        while (i < n - 1) {
            if (j == -1 || pattern.charAt(i) == pattern.charAt(j)) {
                i++;
                j++;
                next[i] = j;
            } else {
                j = next[j];
            }
        }

        return next;
    }

    public static void main(String[] args) {
        String text = "ABABDABACDABABCABAB";
        String pattern = "ABAB";
        List<Integer> matches = kmp(text, pattern);

        if (matches.isEmpty()) {
            System.out.println("Pattern not found");
        } else {
            System.out.println("Pattern found at positions:");
            for (int position : matches) {
                System.out.println(position);
            }
        }
    }
}

在上面的示例中,我们使用一个List<Integer>来存储所有匹配的起始位置。在每次匹配成功后,我们将匹配的起始位置添加到列表中。如果列表为空,则表示未找到匹配位置。

运行上述示例代码,输出结果为:

Pattern found at positions:
0
2
10

这表明模式字符串"ABAB"在文本字符串"ABABDABACDABABCABAB"中出现在索引位置0、2和10。

通过扩展KMP算法,我们可以找到文本字符串中所有匹配模式的位置,而不仅仅是第一个匹配位置。

当使用KMP算法进行字符串匹配时,还可以进行一些优化来进一步提高算法的效率。其中一个优化是在构建next数组时,处理重复的前缀。

在标准的KMP算法中,当模式串中出现重复的前缀时,构建next数组时会进行重复的比较,造成一定的性能损失。为了避免这种情况,可以通过观察模式串的特征,跳过重复的前缀的比较。

下面是优化后的getNext方法的实现:

private static int[] getNext(String pattern) {
    int n = pattern.length();
    int[] next = new int[n];
    next[0] = -1;

    int i = 0;
    int j = -1;

    while (i < n - 1) {
        if (j == -1 || pattern.charAt(i) == pattern.charAt(j)) {
            i++;
            j++;

            // 优化:如果下一个字符与当前字符相等,则直接跳过比较
            if (pattern.charAt(i) == pattern.charAt(j)) {
                next[i] = next[j];
            } else {
                next[i] = j;
            }
        } else {
            j = next[j];
        }
    }

    return next;
}

在优化后的getNext方法中,我们在处理重复的前缀时进行了优化。当下一个字符与当前字符相等时,我们将next[i]的值设为next[j],以跳过比较。

这种优化可以减少比较的次数,从而提高KMP算法的性能。尤其在模式串中包含大量重复前缀的情况下,优化的getNext方法能够更好地发挥作用。

请注意,优化的getNext方法仅适用于处理重复前缀的情况。对于其他类型的模式串,标准的getNext方法仍然是有效的。

通过优化KMP算法,我们可以进一步提高字符串匹配的效率,并更好地应对包含重复前缀的模式串。


原文链接:codingdict.net