Java大型文本的字符串搜索算法

2025/04/10

1. 简介

在本文中,我们将展示几种在大型文本中搜索模式的算法,我们将结合提供的代码和简单的数学背景来描述每种算法。

请注意,在更复杂的应用程序中,提供的算法并不是进行全文搜索的最佳方法。为了正确地进行全文搜索,我们可以使用SolrElasticSearch

2. 算法

我们将从一个简单的文本搜索算法开始,它是最直观的算法,有助于发现与该任务相关的其他高级问题。

2.1 辅助方法

在开始之前,让我们定义一下在Rabin Karp算法中使用的计算素数的简单方法:

public static long getBiggerPrime(int m) {
    BigInteger prime = BigInteger.probablePrime(getNumberOfBits(m) + 1, new Random());
    return prime.longValue();
}

private static int getNumberOfBits(int number) {
    return Integer.SIZE - Integer.numberOfLeadingZeros(number);
}

2.2 简单文本搜索

这个算法的名字比任何其他解释都更能描述它,这是最自然的解决方案:

public static int simpleTextSearch(char[] pattern, char[] text) {
    int patternSize = pattern.length;
    int textSize = text.length;

    int i = 0;

    while ((i + patternSize) <= textSize) {
        int j = 0;
        while (text[i + j] == pattern[j]) {
            j += 1;
            if (j >= patternSize)
                return i;
        }
        i += 1;
    }
    return -1;
}

该算法的思想很简单:遍历文本,如果模式的第一个字母匹配,则检查模式的所有字母是否与文本匹配。

如果m是模式中的字母数,n是文本中的字母数,则该算法的时间复杂度为O(m(nm + 1))。

最坏的情况是当字符串有许多部分出现时:

Text: tuyuchentuyuchentuyuchen
Pattern: tuyucheng

2.3 Rabin Karp算法

如上所述,当模式很长并且模式中有大量重复元素时,简单文本搜索算法效率非常低。

Rabin Karp算法的思想是利用哈希来在文本中寻找模式,在算法开始时,我们需要计算模式的哈希值,并在之后的算法中使用。这个过程称为指纹计算,我们可以在这里找到详细的解释。

预处理步骤的重要之处在于其时间复杂度为O(m),而文本迭代将花费O(n),这使得整个算法的时间复杂度为O(m + n)。

算法代码:

public static int RabinKarpMethod(char[] pattern, char[] text) {
    int patternSize = pattern.length;
    int textSize = text.length;

    long prime = getBiggerPrime(patternSize);

    long r = 1;
    for (int i = 0; i < patternSize - 1; i++) {
        r *= 2;
        r = r % prime;
    }

    long[] t = new long[textSize];
    t[0] = 0;

    long pfinger = 0;

    for (int j = 0; j < patternSize; j++) {
        t[0] = (2 * t[0] + text[j]) % prime;
        pfinger = (2 * pfinger + pattern[j]) % prime;
    }

    int i = 0;
    boolean passed = false;

    int diff = textSize - patternSize;
    for (i = 0; i <= diff; i++) {
        if (t[i] == pfinger) {
            passed = true;
            for (int k = 0; k < patternSize; k++) {
                if (text[i + k] != pattern[k]) {
                    passed = false;
                    break;
                }
            }

            if (passed) {
                return i;
            }
        }

        if (i < diff) {
            long value = 2 * (t[i] - r * text[i]) + text[i + patternSize];
            t[i + 1] = ((value % prime) + prime) % prime;
        }
    }
    return -1;
}

在最坏的情况下,该算法的时间复杂度为O(m(n - m + 1))。但是,平均而言,该算法的时间复杂度为O(n + m)。

此外,该算法还有蒙特卡洛版本,速度更快,但可能导致错误匹配(误报)。

2.4 Knuth-Morris-Pratt算法

在简单文本搜索算法中,我们看到如果文本中有许多部分与模式匹配,算法就会变得很慢。

Knuth-Morris-Pratt算法的思想是计算移位表,该表为我们提供了应该在何处搜索模式候选的信息。

KMP算法的Java实现:

public static int KnuthMorrisPrattSearch(char[] pattern, char[] text) {
    int patternSize = pattern.length;
    int textSize = text.length;

    int i = 0, j = 0;

    int[] shift = KnuthMorrisPrattShift(pattern);

    while ((i + patternSize) <= textSize) {
        while (text[i + j] == pattern[j]) {
            j += 1;
            if (j >= patternSize)
                return i;
        }

        if (j > 0) {
            i += shift[j - 1];
            j = Math.max(j - shift[j - 1], 0);
        } else {
            i++;
            j = 0;
        }
    }
    return -1;
}

以下是我们计算移位表的方法:

public static int[] KnuthMorrisPrattShift(char[] pattern) {
    int patternSize = pattern.length;

    int[] shift = new int[patternSize];
    shift[0] = 1;

    int i = 1, j = 0;

    while ((i + j) < patternSize) {
        if (pattern[i + j] == pattern[j]) {
            shift[i + j] = i;
            j++;
        } else {
            if (j == 0)
                shift[i] = i + 1;

            if (j > 0) {
                i = i + shift[j - 1];
                j = Math.max(j - shift[j - 1], 0);
            } else {
                i = i + 1;
                j = 0;
            }
        }
    }
    return shift;
}

该算法的时间复杂度也是O(m + n)。

2.5 简单的Boyer-Moore算法

两位科学家Boyer和Moore提出了另一个想法,为什么不把这个模式与文本从右到左进行比较,而不是从左到右,同时保持移动方向不变呢?

public static int BoyerMooreHorspoolSimpleSearch(char[] pattern, char[] text) {
    int patternSize = pattern.length;
    int textSize = text.length;

    int i = 0, j = 0;

    while ((i + patternSize) <= textSize) {
        j = patternSize - 1;
        while (text[i + j] == pattern[j]) {
            j--;
            if (j < 0)
                return i;
        }
        i++;
    }
    return -1;
}

正如预期的那样,这将在O(m * n)时间内运行。但是该算法引入了发生和匹配启发式算法,从而显著加快了算法速度。我们可以在这里找到更多信息。

2.6 Boyer-Moore-Horspool算法

Boyer-Moore算法的启发式实现有很多种变体,最简单的是Horspool变体。

这个版本的算法被称为Boyer-Moore-Horspool,这个变体解决了负移位问题(我们可以在Boyer-Moore算法的描述中读到有关负移位问题的内容)。

与Boyer-Moore算法类似,最坏情况的时间复杂度为O(m * n),而平均复杂度为O(n)。空间使用量与模式的大小无关,仅取决于字母表的大小,即256,因为这是英文字母表中ASCII字符的最大值:

public static int BoyerMooreHorspoolSearch(char[] pattern, char[] text) {

    int shift[] = new int[256];

    for (int k = 0; k < 256; k++) {
        shift[k] = pattern.length;
    }

    for (int k = 0; k < pattern.length - 1; k++){
        shift[pattern[k]] = pattern.length - 1 - k;
    }

    int i = 0, j = 0;

    while ((i + pattern.length) <= text.length) {
        j = pattern.length - 1;

        while (text[i + j] == pattern[j]) {
            j -= 1;
            if (j < 0)
                return i;
        }

        i = i + shift[text[i + pattern.length - 1]];
    }
    return -1;
}

4. 总结

在本文中,我们介绍了几种文本搜索算法,由于有些算法需要扎实的数学功底,我们以通俗易懂的方式,阐述每种算法的核心思想。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章