算法-24-子字符串查找(暴力+KMP+BM+RK)

目录

0、相关链接

1、定义

2、暴力子字符串查找

3、Knuth-Morris-Pratt 子字符串查找

4、Boyer-Moore 字符串查找算法

5、Rabin-Karp 指纹字符串查找算法

6、各种子字符串查找算法的成本比较


0、相关链接

算法-22-字符串的排序算法(四种排序)

算法-23-单词查找树(字符串搜索最快算法)

1、定义

子字符串查找:给定一段长度为 N 的文本和一个长度为 M 的模式 (pattern)字符串,在文本中找到一个和该模式相符的子字符串。解决该问题的大部分 算法都可以很容易地扩展为找出文本中所有和该模式相符的子字符串、统计该模式在文本中的出现次 数、或者找出上下文(和该模式相符的子字符串周围的文字)的算法。

2、暴力子字符串查找

子字符串查找的一个最显而易见的方法就是遍历文本N,然后对每个字符都进行检查,确保这个字符和后面的M-1个字符和要找的子字符串是一样的。

在最坏情况下,暴力子字符串查找算法在长度为 N 的文本中查找长度为 M 的模式需 要 ~NM 次字符比较。这是因为第一层for循环需要(N-M+1)第二层for循环需要M次,总共需要「 M*(N-M+1)」,因为M一般都远小于N,所以省去M^2+M。

public class SubstringSearch {
    /**
     * 实现一,正循环
     */
    public static int search(String pat, String txt) {
        int N = txt.length();
        int M = pat.length();

        for (int i = 0; i <= N - M; i++) {
            int j;
            for (j = 0; j < M; j++) {
                if (pat.charAt(j) != txt.charAt(i + j))
                    break;
            }
            if (j == M)
                return i; // 找到匹配
        }
        return N; // 未找到匹配
    }
    /**
     * 实现二,显示回退
     * 这段代码中的 i 值相当于上一段代码中的 i+j:它指向的是文本中已经匹配过的字 符序列的末端(i 以前指向的是这个序列的开头)。
     * 如果 i 和 j 指向的字符不匹配了,那么需要回 退这两个指针的值:将 j 重新指向模式的开头,将 i 指向本次匹配的开始位置的下一个字符。
     */
    public static int search1(String pat, String txt) {
        int N = txt.length();
        int M = pat.length();
        int i,j;
        for (i = 0, j = 0; i < N && j < M; i++) {

            if (pat.charAt(j) == txt.charAt(i)) {
                j++;
            } else {
                i = i - j;
                j = 0;
            }
        }
        if (j==M) return i-M;// 找到匹配
        else  return N; // 未找到匹配
    }

    public static void main(String[] args) {
        String pat = "abab";
        String txt = "sjosajofajfiajsessasabacababfofkokokofj";
        int result = search(pat, txt);
        int result1 = search1(pat, txt);
        System.out.println("result=" + result+",result1="+result1);
        System.out.println("txt.length=" + txt.length());
    }
}

3、Knuth-Morris-Pratt 子字符串查找

上面的暴力算法在最坏情况下需要N*M的时间复杂度,这不是我们想要的。这里将讲解一个最坏情况下也只需要N次循环的查找算法。

KMP算法时借鉴了上面暴力算法的第二种实现方式,它会遍历文本N,同时与pat匹配,当不匹配的时候回退子针。KMP与其不同的地方是:KMP不会回退子针,而是当你匹配0-M时,如果在中间某个字符不匹配时,通过预处理信息,直接知道我下一步应该怎么重新开始0-M的遍历。该算法理解起来还是比较麻烦的,所以可以先看一下代码。

通俗点说就是当 我们的pat=“aab”,   文本txt=“abcaaaabc”。

当我们遍历文本N的第一字符a时,j=1;

遍历第二个字符b时,j变成了0;

遍历第4和第5个字符时,j=2,但遍历到第三个a时,j不会变成0,j还是=2;

遍历第四个a的时候,j=2;

最后遍历到b,j=3,结果出来了。

对于长度为 M 的模式字符串和长度为 N 的文本,Knuth-Morris-Pratt 字符串查找算法访 问的字符不会超过 M+N 个。

KMP 算法为最坏情况提供的线性级别 运行时间保证是一个重要的理论成果。在 实际应用中,它比暴力算法的速度优势并 不十分明显,因为极少有应用程序需要在 重复性很高的文本中查找重复性很高的模 式。

public class KMP {

    private String pat;       // 需要查找的字符串
    private int[][] dfa;      // 预处理的二维数组
    private int R = 256;      // 字母表的大小

    public KMP(String pat) {
        this.pat = pat;
        int M = pat.length();
        dfa = new int[R][M];             

        dfa[pat.charAt(0)][0] = 1;             //0、预处理pat的第一个字符

        for (int X = 0, j = 1; j < M; j++) {

            for (int c = 0; c < R; c++) {
                dfa[c][j] = dfa[c][X];         //1、复制匹配失败情况下的值
            }
            dfa[pat.charAt(j)][j] = j + 1;     //2、设置匹配情况下的值
            X = dfa[pat.charAt(j)][X];         //3、更新重启状态,这里主要是给 pat="aaabc"
                                               // 前面的三个a或者更多的a更新重启状态,b、c字母会重启为0
        }

    }

    public int search(String txt) {
        int i, j;
        int N = txt.length(), M = pat.length();

        for (i = 0, j = 0; i < N && j < M; i++) {
            j = dfa[txt.charAt(i)][j];
        }
        if (j == M)
            return i - M;
        else
            return N;
    }

    public static void main(String[] args) {
        String pat = "abab";
        String txt = "sjosajofajfiajsessaababababsfofkokokofj";
        KMP kmp = new KMP(pat);
        int offset = kmp.search(txt);
        System.out.println("offset=" + offset);
        System.out.println("txt.length=" + txt.length());
    }
}

1、该算法会创建一个二维数组dfa来预处理要查找的模式字符串pat。

2、dfa中保存着256个大小为pat长度M的数组。 dfa = new int[R][M]; 也就是每个字符都有一个长度和pat长度一样的数组来保存与该字符匹配成功后j值的变化,和匹配失败后j值的变化。

3、pat=“aaa”  dfa[a][0]=1;  dfa[a][1]=2;   dfa[a][2]=3;   其余的数据都是0;

     pat=“aab”  dfa[a][0]=1;  dfa[a][1]=2;   dfa[a][2]=2;    其余的数据都是0;

                         dfa[b][0]=0;  dfa[b][1]=0;   dfa[b][2]=3;  

     pat=“abc”  dfa[a][0]=1;  dfa[a][1]=1;   dfa[a][2]=1;    其余的数据都是0;

                        dfa[b][0]=0;  dfa[b][1]=2;   dfa[b][2]=0;  

                        dfa[b][0]=0;  dfa[b][1]=0;   dfa[b][2]=3;  

上面的数据大致包括了字符串的几种形式‘abc'、’aab'、‘aaa'。

先从’aaa'开始,从上面的数据我们可以看到当我们遇到一个a的时候j=1;当我们遇到第二个a的时候j=2;但第三个字符不是a,那么j就会变回0,但是并没有回退。而是继续向后遍历。

‘aab'形式,当我们遇到一个a的时候j=1;第二个a的时候j=2,但第三个还是a的话,说明第三个字符不匹配,但是我们通过j我们知道前面还有两个a,所以我们不需要回退子针,也不需要将j变回0,而是继续从j=2开始匹配下一个字符,如果下一个字符还是a,那么j还是=2;如果不是a也不是b那么变回0.

‘abc'形式,所有子字符都不相同的情况就好处理了,除了每次碰见a,j变为1以外,其他位置的字符都只有前一个字符匹配的时候才会j才会增加,否则都会变成0。

4、Boyer-Moore 字符串查找算法

Boyer-Moore 算法选择从右到左扫描模式pat字符串并与txt文本比较。它借用了KMP算法的思想也通过预处理right[]数组来对所有字符进行位置的记录,然后当发生不匹配的时候,提供跳转的数量。

right[]数组会保存模式字符串 pat= “abc”中三个字符的索引,right[a]=0,right[b]=1,right[c]=2,然后其余的253个字符全是 -1.

模式字符串pat= “a b c” ,文本txt=“a b d e c b c a b c”  。

1、我们先从 txt的第三个字符“d”和 “abc”比较,发现“d”和“c”不匹配,又因为“d”不存在pat字符串中,说明txt的前三个字符肯定不会出现和pat完全匹配的子字符串,所以我们直接跳转2-(-1)=3个位置。(其中2是当前扫描的pat中字符的索引,c的索引j=2)

2、然后用用第六个字符“b”和“abc”比较,又发现“b”和“c”不匹配,但是“b”存在于“abc”中,我们预处理right[]数组会保存“b”存在于“abc”的位置为1,这时候我们需要将 “abc”向后跳转 2-1=1个位置。

3、然后我们用第七个字符“c”和“abc”比较,发现匹配,并且前面的“b”也和“abc”匹配,但是再前面“c”和“a”不匹配,这时候扫描的字符的索引 j[a]=0,  0-2=-2。我们“abc”肯定不能跳转-2个位置,当我们遇到跳转小于的1的位置的时候,我们强制让他跳转1个位置,防止它原地打转。

上面的三种情况包含了我们在跳转过程中遇到的所有情况,只要我们把这三种情况处理好,那这个算法就成功了,并且还能将时间复杂度降低到(~N/M)。

一般情况下,对于长度 为 N 的文本和长度为 M 的模式字 符串,使用了 Boyer-Moore 的子字 符串查找算法通过启发式处理不匹 配的字符需要 ~N/M 次字符比较。

public class BoyerMoore {

    private String pat;
    private int[] right;

    /**
     * 计算跳跃表 right
     */
    public BoyerMoore(String pat) {
        this.pat = pat;
        int R = 256;
        int M = pat.length();
        right = new int[R];

        for (int c = 0; c < R; c++) {
            right[c] = -1; // 不包含在模式字符串中字符的值为-1
        }
        for (int j = 0; j < M; j++) {
            right[pat.charAt(j)] = j; // 包含在模式字符串中的字符的值为它出现在模式字符串中最右的位置,
                                      // 就比如 pat="aaa" right[a]=2;不是0和1
        }
    }
    public int search(String txt) {
        int N = txt.length();
        int M = pat.length();

        int skip;
        for (int i = 0; i <= N - M; i += skip) {
            skip=0;
            for (int j = M - 1; j >= 0; j--) {                 //1、每次从pat的最右边的字符开始与txt比较
                if (pat.charAt(j) != txt.charAt(j + i)) {      //2、如果第 j+i个字符和pat不匹配,进行跳转
                    skip = j - right[txt.charAt(j + i)];       //3、跳转的距离等于
                    if (skip < 1) skip = 1;
                    break;
                }
            }
            if (skip == 0) return i;
        }
        return N;
    }
}

5、Rabin-Karp 指纹字符串查找算法

M.O.Rabin 和 R.A.Karp 发明了一种完全不同的基于散列的字符串查找算法。

我们需要计算模式 字符串的散列函数,然后用相同的散列函数计算文本中所有可能的 M 个字符的子字符串散列值并 寻找匹配。如果找到了一个散列值和模式字符串相同的子字符串,那么再继续验证两者是否匹配。 这个过程等价于将模式保存在一张散列表中,然后在文本的所有子字符串中进行查找。但不需要为 散列表预留任何空间,因为它只会含有一个元素。

根据这段描述直接实现的算法将会比暴力子字符 串查找算法慢很多(因为计算散列值将会涉及字符串中的每个字符,成本比直接比较这些字符要高 得多)。Rabin 和 Karp 发明了一种能够在常数时间内算出 M 个字符的子字符串散列值的方法(需 要预处理),这样就得到了在实际应用中的运行时间为线性级别的字符串查找算法。

public class RabinKarp {
    private String pat;      // the pattern  // needed only for Las Vegas
    private long patHash;    // pattern hash value
    private int m;           // pattern length
    private long q;          // a large prime, small enough to avoid long overflow
    private int R;           // radix
    private long RM;         // R^(M-1) % Q

    /**
     * Preprocesses the pattern string.
     *
     * @param pattern the pattern string
     * @param R the alphabet size
     */
    public RabinKarp(char[] pattern, int R) {
        this.pat = String.valueOf(pattern);
        this.R = R;        
        throw new UnsupportedOperationException("Operation not supported yet");
    }

    /**
     * Preprocesses the pattern string.
     *
     * @param pat the pattern string
     */
    public RabinKarp(String pat) {
        this.pat = pat;      // save pattern (needed only for Las Vegas)
        R = 256;
        m = pat.length();
        q = longRandomPrime();

        // precompute R^(m-1) % q for use in removing leading digit
        RM = 1;
        for (int i = 1; i <= m-1; i++)
            RM = (R * RM) % q;
        patHash = hash(pat, m);
    } 

    // Compute hash for key[0..m-1]. 
    private long hash(String key, int m) { 
        long h = 0; 
        for (int j = 0; j < m; j++) 
            h = (R * h + key.charAt(j)) % q;
        return h;
    }

    // Las Vegas version: does pat[] match txt[i..i-m+1] ?
    private boolean check(String txt, int i) {
        for (int j = 0; j < m; j++) 
            if (pat.charAt(j) != txt.charAt(i + j)) 
                return false; 
        return true;
    }

    // Monte Carlo version: always return true
    // private boolean check(int i) {
    //    return true;
    //}
 
    /**
     * Returns the index of the first occurrrence of the pattern string
     * in the text string.
     *
     * @param  txt the text string
     * @return the index of the first occurrence of the pattern string
     *         in the text string; n if no such match
     */
    public int search(String txt) {
        int n = txt.length(); 
        if (n < m) return n;
        long txtHash = hash(txt, m); 

        // check for match at offset 0
        if ((patHash == txtHash) && check(txt, 0))
            return 0;

        // check for hash match; if hash match, check for exact match
        for (int i = m; i < n; i++) {
            // Remove leading digit, add trailing digit, check for match. 
            txtHash = (txtHash + q - RM*txt.charAt(i-m) % q) % q; 
            txtHash = (txtHash*R + txt.charAt(i)) % q; 

            // match
            int offset = i - m + 1;
            if ((patHash == txtHash) && check(txt, offset))
                return offset;
        }

        // no match
        return n;
    }


    // a random 31-bit prime
    private static long longRandomPrime() {
        BigInteger prime = BigInteger.probablePrime(31, new Random());
        return prime.longValue();
    }

    /** 
     * Takes a pattern string and an input string as command-line arguments;
     * searches for the pattern string in the text string; and prints
     * the first occurrence of the pattern string in the text string.
     *
     * @param args the command-line arguments
     */
    public static void main(String[] args) {
        String pat = args[0];
        String txt = args[1];

        RabinKarp searcher = new RabinKarp(pat);
        int offset = searcher.search(txt);

        // print results
        StdOut.println("text:    " + txt);

        // from brute force search method 1
        StdOut.print("pattern: ");
        for (int i = 0; i < offset; i++)
            StdOut.print(" ");
        StdOut.println(pat);
    }
}

6、各种子字符串查找算法的成本比较

 

 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
BF算法KMP算法都是串的模式匹配算法,但是它们的时间复杂度不同。BF算法的时间复杂度为O(m*n),其m和n分别为主串和模式串的长度。而KMP算法的时间复杂度为O(m+n)。因此,当模式串较长时,KMP算法的效率更高。 下面是BF算法KMP算法的介绍和演示: 1. BF算法暴力匹配算法) BF算法是一种朴素的模式匹配算法,它的思想是从主串的第一个字符开始,依次和模式串的每个字符进行比较,如果匹配成功,则继续比较下一个字符,否则从主串的下一个字符开始重新匹配。BF算法的时间复杂度为O(m*n)。 下面是BF算法的Python代码演示: ```python def BF(main_str, pattern_str): m = len(main_str) n = len(pattern_str) for i in range(m-n+1): j = 0 while j < n and main_str[i+j] == pattern_str[j]: j += 1 if j == n: return i return -1 # 测试 main_str = 'ababcabcacbab' pattern_str = 'abcac' print(BF(main_str, pattern_str)) # 输出:6 ``` 2. KMP算法Knuth-Morris-Pratt算法KMP算法是一种改进的模式匹配算法,它的核心思想是利用已经匹配过的信息,尽量减少模式串与主串的匹配次数。具体来说,KMP算法通过预处理模式串,得到一个next数组,用于指导匹配过程的跳转。KMP算法的时间复杂度为O(m+n)。 下面是KMP算法的Python代码演示: ```python def KMP(main_str, pattern_str): m = len(main_str) n = len(pattern_str) next = getNext(pattern_str) i = 0 j = 0 while i < m and j < n: if j == -1 or main_str[i] == pattern_str[j]: i += 1 j += 1 else: j = next[j] if j == n: return i - j else: return -1 def getNext(pattern_str): n = len(pattern_str) next = [-1] * n i = 0 j = -1 while i < n-1: if j == -1 or pattern_str[i] == pattern_str[j]: i += 1 j += 1 next[i] = j else: j = next[j] return next # 测试 main_str = 'ababcabcacbab' pattern_str = 'abcac' print(KMP(main_str, pattern_str)) # 输出:6 ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值