Java中实现大文本的字符串搜索的几种算法——简单文本搜索、Rabin Karp算法、Knuth-Morris-Pratt、Boyer-Moore、Boyer-Moore-Horspool

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

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

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

在开始之前,让我们定义计算我们在 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);
}

一、 简单文本搜索

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

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(n-m + 1))。

最坏的情况发生在 String 具有许多部分出现的情况下:

Text: baeldunbaeldunbaeldunbaeldun
Pattern: baeldung

二、 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) 的时间复杂度。

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

三、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)。

四、 简单的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) 时间内运行。但是这种算法导致了发生和匹配启发式的实现,这大大加快了算法的速度。我们可以在这里找到更多。

五、 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;
}

在本文中,我们介绍了几种用于文本搜索的算法。由于几种算法需要更强的数学背景,因此我们试图表示每种算法背后的主要思想,并以简单的方式提供它。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
串匹配是指在一个文本查找另一个模式串的过程。常用的串匹配算法有Naïve算法、Rabin-Karp算法Knuth-Morris-Pratt算法。 1. Naïve算法 Naïve算法是最简单的串匹配算法,也称为暴力匹配算法。它的思路是从文本串的第一个字符开始,依次比较文本的每个字符是否与模式串字符相等。若不相等,则继续向后比较;若相等,则比较下一个字符,直到找到完全匹配的子串或文本串被匹配完为止。 Naïve算法的时间复杂度是O(mn),其m和n分别是模式串和文本串的长度。当模式串和文本串长度相等时,最坏情况下时间复杂度达到O(n^2)。 2. Rabin-Karp算法 Rabin-Karp算法是一种基于哈希值的串匹配算法。它的思路是先将模式串和文本串都转换为哈希值,然后比较它们的哈希值是否相等。如果哈希值相等,则再逐个比较模式串和文本字符是否相等。这种方法可以有效地减少比较次数,提高匹配效率。 Rabin-Karp算法的时间复杂度是O(m+n),其m和n分别是模式串和文本串的长度。但是,由于哈希函数的不完全性和哈希冲突的存在,Rabin-Karp算法在某些情况下可能会出现误判。 3. Knuth-Morris-Pratt算法 Knuth-Morris-Pratt算法是一种基于前缀函数的串匹配算法。它的思路是先计算出模式串的前缀函数,然后利用前缀函数的信息来跳过已经匹配过的部分,减少比较次数。 具体来说,KMP算法在匹配过程维护一个指针i和一个指针j,其i指向文本当前匹配的位置,j指向模式串当前匹配的位置。如果当前字符匹配成功,则i和j同时向后移动一位;如果匹配失败,则通过前缀函数计算出j需要跳转到的位置,使得前j-1个字符文本的对应字符已经匹配成功,然后将j指向这个位置,i不变,继续比较下一个字符。 KMP算法的时间复杂度是O(m+n),其m和n分别是模式串和文本串的长度。由于利用了前缀函数的信息,KMP算法可以在最坏情况下达到O(n)的时间复杂度,比Naïve算法和Rabin-Karp算法更加高效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值