关于字符串搜索算法 | 青训营笔记

178 阅读2分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第 1 篇笔记

2022-05-05

KMP 算法

首先需要明白 KMP 算法是什么,可以阅读这两篇博客文章:KMP 算法 - 刘毅字符串匹配的KMP算法 - 阮一峰

实现 KMP 算法的关键是 双指针next 数组的生成。next[j] 表示当 j 位置的字符不匹配时,j 应该移动到哪个位置。next[j] 是 -1 时表示要完全跳过(i++;j = 0;),比如 next[0] 一定是 -1,因为第一个字符都不匹配,当然就是直接跳过了。

这里我讲一下 KMP 算法的 next 数组的生成。next 数组是通过模式串自己跟自己匹配计算出来的,假设我们的模式串,即搜索的目标是“abac”,其 next 数组的计算过程如下:

// (1) 一开始,next[0] == -1
              i = 1
next =  -1
       ['a', 'b', 'a', 'c']
            ['a', 'b', 'a', 'c']
              j = 0


// (2.0) 当 next[i] != next[j] 时,next[i] = j
              i = 1
next =  -1    0
       ['a', 'b', 'a', 'c']
            ['a', 'b', 'a', 'c']
              j = 0

// (2.1) j = next[j]
              i = 1
next =  -1    0
       ['a', 'b', 'a', 'c']
            ['a', 'b', 'a', 'c']
         j = -1

// (3) i++; j++;
                   i = 2
next =  -1    0
       ['a', 'b', 'a', 'c']
                 ['a', 'b', 'a', 'c']
                   j = 0

// (4) 当 next[i] == next[j] 时,next[i] = next[j]
                   i = 2
next =  -1    0   -1
       ['a', 'b', 'a', 'c']
                 ['a', 'b', 'a', 'c']
                   j = 0

// (5) i++; j++;
                        i = 3
next =  -1    0   -1
       ['a', 'b', 'a', 'c']
                 ['a', 'b', 'a', 'c']
                        j = 1

// 剩下就是根据 next[i] 和 next[j] 是否相等,循环 (2.0) 或 (4) 操作

KMP 算法的时间复杂度为 O(n + m) ,空间复杂度为 O(m) ,它的 Java 的实现如下:

public final class KMP {

    private KMP() {
        // private
    }

    public static int search(String str, String target) {
        int n = str.length(), m = target.length();
        if (m == 0) return 0;// 这一行不能少
        if (n < m) return -1;
        int[] next = getNext(target);
        for (int i = 0, j = 0; i < n; i++, j++) {
            while (j > -1 && str.charAt(i) != target.charAt(j)) j = next[j];
            if (j == m - 1) return i - j;
        }
        return -1;
    }

    static int[] getNext(String target) {
        int[] next = new int[target.length()];
        next[0] = -1;
        for (int i = 1, j = 0; i < target.length(); i++, j++) {
            if (j == -1) continue;
            if (target.charAt(i) != target.charAt(j)) {
                next[i] = j;
                j = next[j];
            } else {
                next[i] = next[j];
            }
        }
        return next;
    }
}

由于

  • 空间复杂度 O(m) 不够好
  • 实现过程复杂,容易出错

实际应用中很少选择 KMP 算法。

Rabin-Karp 算法

其实存在一种时间复杂度和空间复杂度都优于 KMP 算法的字符串搜索算法,就是 Rabin-Karp 算法。

在计算机科学中,拉宾-卡普算法(英语:Rabin–Karp algorithm),是一种由理查德·卡普与迈克尔·拉宾于1987年提出的、使用散列函数在文本中搜寻单个模式串的字符串搜索算法。

该算法使用滚动哈希快速地跳过哈希不匹配的子串,只在哈希匹配时才对比整个模式串的每个字符。此算法可推广到用于在文本搜寻单个模式串的所有匹配或在文本中搜寻多个模式串的匹配。

Java 实现

只要不发生或极少发生哈希碰撞,Rabin–Karp 算法能发挥出 O(n) 的时间复杂度,而哈希碰撞发生的概率是很低的,还有它的空间复杂度是 O(1),所以 Rabin–Karp 算法是我个人比较推荐的字符串搜索算法,其 Java 实现如下:

public final class RabinKarpAlgorithm {

    private RabinKarpAlgorithm() {
        // private
    }

    static final int PRIME = 16777619;

    /**
     * 不发生哈希碰撞时,时间复杂度为 O(n);全都哈希碰撞时,时间复杂度为 O(nm)
     */
    public static int search(String str, String target) {
        int n = str.length(), m = target.length();
        if (n < m) return -1;
        int p = PRIME, maxPow = pow(p, m - 1);
        int t = rollingHash(target, m, p);
        int hash = rollingHash(str, m, p);
        for (int i = 0, end = n - m; i < end; i++) {
            if (hash == t && str.regionMatches(i, target, 0, m)) return i;
            hash = (hash - str.charAt(i) * maxPow) * p + str.charAt(i + m);
        }
        return -1;
    }

    /**
     * 求幂,时间复杂度 O(log n)
     */
    public static int pow(int x, int n) {
        int a = 1;
        while (n > 0) {
            if ((n & 1) == 1) a *= x;
            x *= x;
            n >>= 1;
        }
        return a;
    }

    static int rollingHash(String str, int count, int p) {
        int hash = 0;
        for (int i = 0; i < count; i++) {
            hash += str.charAt(i) * pow(p, count - i - 1);
        }
        return hash;
    }
}

参考