LeetCode28. Implement strStr() 字符串匹配

文章目录

      • 28.字符串匹配
        • 28. Implement strStr()
          • 暴力破解
          • Rabin-Karp算法
          • KMP算法
          • BM算法(Boyer-Moore)
          • Sunday算法

28.字符串匹配

28. Implement strStr()

Implement strStr().

Return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

Example 1:

Input: haystack = "hello", needle = "ll"
Output: 2

Example 2:

Input: haystack = "aaaaa", needle = "bba"
Output: -1

Clarification:

What should we return when needle is an empty string? This is a great question to ask during an interview.

For the purpose of this problem, we will return 0 when needle is an empty string. This is consistent to C’s strstr() and Java’s indexOf().

暴力破解

这道题本质上就是字符串的匹配问题。可以使用暴力破解的方式进行。首先先确立特殊情况。

  • 当模板串needle和要 文本串haystack都为空的时候,返回 0
  • 当模版串为空的时候,返回0
  • 当模板串比文本串要长的时候返回 -1

以上是首先要确定的三种特殊情况。

接下来进行暴力破解,我们可以知道,需要每个文本串都可以产生n-m个长度与模版串长度相同的子串,其中n为文本串的长度,m为模板串的长度。之后将模板串和文本串的字串进行逐一匹配。若匹配成功,则直接返回该匹配成功的子串的首字母在文本串的位置。这种方式需要的时间复杂度为O((n-m+1)m)。

class Solution {
    public int strStr(String haystack, String needle) {
        if((isNullOrEmpty(haystack) && isNullOrEmpty(needle)) || isNullOrEmpty(needle)) {
            return 0;
        }
        if(haystack.length() < needle.length()){
            return -1;
        }
        
        int n = haystack.length();
        int m = needle.length();
        for(int i = 0; i < n-m +1 ;i++){
            if(compareTwoString(haystack.substring(i,i+m),needle)){
                return i;
            }
        }
        return -1;
    }
    public boolean isNullOrEmpty(String s){
        return s == null || s.length() == 0;
    }
    
    public boolean compareTwoString(String s1, String s2){
        int n = s2.length() ;
        for(int i = 0; i < n; i++){
            if(s1.charAt(i) != s2.charAt(i)){
                return false;
            }
        }
        return true;
    }
    
}
Rabin-Karp算法

Rabin-Karp算法是暴力算法的改进。RK算法的思想是将模板串看成为一个数值,然后使用同样的规则计算出同等长度文本串的子串的数值。如果数值不等,那么就表示这两个字符串不可能是相等的字符串。如果相等,则说明这两个字符串有可能相等。对有可能相等的字符串的字符进行一一匹配。

这种方法对模板串进行了预先处理,能够减少了字符串匹配的次数。但是最糟糕的情况下就和暴力破解一样的。同时对模板进行处理需要O(m)的时间,而对于文本串值的计算需要O(m-n)的时间。

如何用数值来代表字符串,这里采用了秦九韶算法,其中在第13行传入的d表示进制,如256表示256进制。而这样的处理方法会出现数值过大的情况,进而导致不方便操作。因此通过mod q的方式使数值变小。这种处理方式和hash的处理方式相似。当然也可以通过其他方式来计算这个数值。

class Solution {
    public int strStr(String haystack, String needle) {
        if((isNullOrEmpty(haystack) && isNullOrEmpty(needle)) || isNullOrEmpty(needle)) {
            return 0;
        }
        if(haystack.length() < needle.length()){
            return -1;
        }

        return RKM(haystack,needle,256,23);
    }
    
    public int RKM(String haystack,String needle,int d,int  q){
        int n = haystack.length();
        int m = needle.length();
        int h = 1;
        int p = 0;
        int t = 0;
        
        for(int i = 0; i < m -1;i++){
            h = (h*d)%q;
        }
        
        for(int i = 0; i < m;i++){
            p = (d*p + Integer.valueOf(needle.charAt(i))) % q;
            t = (d*t + Integer.valueOf(haystack.charAt(i))) % q;
        }
        
        for(int s = 0; s < n -m + 1; s++){
            if(p == t){
                if(compareTwoString(haystack.substring(s,s+m),needle)){
                    return s;
                }
            }
            
            if(s < n-m ){
                t = (d * (t-haystack.charAt(s)*h) + haystack.charAt(s+m))% q;
                if(t < 0){
                    t = t + q;
                }
            }
        }
        return -1;
    }
    
    public boolean isNullOrEmpty(String s){
        return s == null || s.length() == 0;
    }
    
    public boolean compareTwoString(String s1, String s2){
        int n = s2.length() ;
        for(int i = 0; i < n; i++){
            if(s1.charAt(i) != s2.charAt(i)){
                return false;
            }
        }
        return true;
    }
}
KMP算法

KMP算法在更大程度上减少无效的匹配。举个例子如模板串为"abab",文本串为“abacabab“。匹配abac的时候,我们很容易知道是不能匹配的。但是对于第二个a和第一个b。我们也很容易知道同样是无效的。KMP就是通过一个next数组,通过改变偏移量来直接跳过已经知道是无效的匹配。

如abab的next数组为[0,0,1,2]。而next表示的是下一个匹配的字符的下标。同样是上面的那一个例子,我们匹配C失败的时候,但是我们知道上一次匹配a是成功的,因此不再回退到最原始的情况。而是回到上一次匹配a成功的状态,通过next数组我们回到了下标为1的状态,即匹配b。b是不能匹配c的,同理,我们通过next数组回到了0的状态。这个时候才回到了最原始的状态。所以KMP是不直接回到原始状态,而是回到上一次匹配成功的状态,尽可能减少回退。

怎么求next数组,这个也很简单。只要将模板串和其自己进行比较就好了。next[i]换一种角度理解就是以i结尾的字符串P1的真后缀P2的最长前缀长度。首个字母的必然为0。以abac为例。

计算next[1]:P1为 “ab”, 真后缀为"b",因此真后缀的最长前缀为0。(这里只是选取了最长前缀的一个后缀)

计算next[2]:P1为 “aba”, 真后缀为"a",因此真后缀的最长前缀为1。

计算next[3]:P1为 “abab”, 真后缀为"ab",因此真后缀的最长前缀为2。

KMP的本质思想和自动机的思想相似。根据当前的状态判断下一步的状态是怎么样子的。而next数组换一种理解就是。如果当前状态不匹配的话,就跳回之前匹配过的最好状态,正如上面所说的,当c不匹配b的时候,就回到上一次匹配的状态。

KMP算法在计算next数组的时候,需要O(m)的时间,匹配需要O(n)的时间,所以总的时间复杂度为O(m+n)。空间复杂度为O(m)

class Solution {
    public int strStr(String haystack, String needle) {
        if((isNullOrEmpty(haystack) && isNullOrEmpty(needle)) || isNullOrEmpty(needle)) {
            return 0;
        }
        if(haystack.length() < needle.length()){
            return -1;
        }
        int n = haystack.length();
        int m = needle.length();
        int[] next = getNext(needle);
        int q = 0;
        for(int i = 0; i < n ;i++){
            while(q > 0 && needle.charAt(q) != haystack.charAt(i)){
                q = next[q -1];
            }
            if(needle.charAt(q) == haystack.charAt(i)){
                q++;
            }
            if( q == m){
                return i - m +1;
            }
            // q = next[q];
        }
        return -1;
    }
    
    public int[] getNext(String needle){
        int m = needle.length();
        int[] next = new int[m];
        next[0] = 0;
        int k = 0;
        for(int i = 1; i < m ; i++){
            while(k > 0 && needle.charAt(k) != needle.charAt(i)){
                k = next[k - 1];
            }
            if(needle.charAt(k)== needle.charAt(i)){
                k ++;
            }
            next[i] = k;
        }
        return next;
    }
    
    public boolean isNullOrEmpty(String s){
        return s == null || s.length() == 0;
    }
}
BM算法(Boyer-Moore)

BM算法在一定程度上是KMP算法的优化。KMP算法在每一次失败的时候都会回到上一次成功的状态。但是如果模板串当中不存在着要匹配的字符,那么KMP就会通过几次跳跃跳回到模板串的第一个字符进行重新进行匹配。而BM算法最大的特点就是获得更大的跳转,而不需要像KMP一样进行多次跳转。

Sunday算法

Sunday算法和KMP算法一样,都是通过计算偏移来减少匹配的次数。不同的是如何进行偏移。我们可以将文本串位置固定,通过模板串的方式来理解这个算法。

Sunday算法主要关注的是参加匹配的最末位的下一位字符。通过这个字符来决定如何偏移。那么会有两种情况:

  1. 这个字符已经存在在模板串当中,那么就将模板串的最右端该字符与文本串的该字符对齐。即移动的位数为 模板串的长度 - 该字符最右出现的位置
  2. 如果该字符不存在模板串中,那么则直接跳过,移动的位数为模板串长度 + 1

那么就需要使用一个next数组来计算匹配上模板串的字符的时候,需要移动的位数。

根据上面的两种情况,可以参考13~18行代码,不存在的模板串的字符就设置为模板串的长度 + 1,存在的就为 模板串的长度 - 该字符最右出现的位置。

Sunday 算法计算next数组的时间为O(m + 字符集的长度),最坏的情况下就变为了暴力破解,时间复杂度为O(mn),平均时间复杂度为O(n),空间复杂度为O(字符集的长度)

class Solution {
    public int strStr(String haystack, String needle) {
        if((isNullOrEmpty(haystack) && isNullOrEmpty(needle)) || isNullOrEmpty(needle)) {
            return 0;
        }
        if(haystack.length() < needle.length()){
            return -1;
        }
        int n = haystack.length();
        int m = needle.length();
        int[] next = new int[256];
        // calculate next array
        for(int i = 0; i < next.length - 1;i++){
            next[i] = m + 1;
        }
        for(int i = 0; i < m; i++){
            next[needle.charAt(i)] = m - i;
        }
        
        int s = 0;//haystack position
        int j = 0;//needle position
        while( s <= n -m){
            j = 0;
            while( s + j < n && j < m && haystack.charAt(s+j) == needle.charAt(j)){
                j++;
                if(j == m){
                    return s;
                }
            }
            int max = s+m < n ? s+m : n - 1;
            s += next[haystack.charAt(max)];
        }
        return -1;
    }
    
    public boolean isNullOrEmpty(String s){
        return s == null || s.length() == 0;
    }
}

你可能感兴趣的:(算法笔记)