记事本中的查找是如何实现的呢?一起来看一下字符串匹配算法

「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」。

你们了解字符串匹配算法吗?在我们刷算法题的时候,总有一类问题是解决字符串匹配的,其实字符串匹配是有算法技巧的,最基本的BF算法,进阶的RK算法,Sunday 算法,BM算法,KMP算法,下面我们就来一起走进这些算法。

题意介绍

什么是字符串匹配问题呢?

给你两个字符串A和B,判断B是否是A的子串,并返回B在A中第一次出现的位置。

B串的长度小于A串的长度。

此类题目B字符串也叫模式串,A字符串叫主串

测试用例一:

字符串A:a b c d e f

字符串B:d e f

显然,字符串B是字符串A的子串,B在A中第一次出现的位置是3,所以返回3.

测试用例二:

字符串A:a b c d e f

字符串B:e c a f

显然,字符串B不是字符串A的子串,所以返回 -1.

BF 暴力检索算法

BF算法,就是Brute Force(暴力检索算法) ,也是我们最容易想到的一种解决思路,就是先找到B串第一个与A串中相匹配的,记录当前位置,然后依次遍历B串看看是否与A串中的相匹配,匹配则返回当前位置;如果B串后边与A串后边不匹配,则再从A串记录位置下一个继续遍历确认与B串第一个是否匹配。

代码如下:

public class BFSearch implements StrSearch {
​
    @Override
    public int search(String A, String B) {
        if (A == null || A.length() == 0 || B == null || B.length() == 0) {
            return -1;
        }
        for (int i = 0; i < A.length(); i++) {
            if (i + B.length() > A.length()) {
                break;
            }
            if (A.charAt(i) == B.charAt(0)) {
                boolean flag = true;
                for (int j = 0; j < B.length(); j++) {
                    if (B.charAt(j) != A.charAt(i + j)) {
                        flag = false;
                        break;
                    }
                }
                if (flag) {
                    return i+1;
                }
            }
        }
        return -1;
    }
}
复制代码

致命缺陷:

最坏时间复杂度过高

当字符串A:a a a a a a a a a a c d 字符串B:a a a d

假设A串长为m,B串长为n,此时时间复杂度就是O(mn)

RK 算法

RK算法全称是Rabin-Karp,它是以算法发明者名字命名的,RK算法是对BF算法的一种优化,在BF算法中,是首字母相同,然后再对每个字符进行比较从而得出结论,而在RK算法中我们只需要进行一次对比即可。

那我们怎样才能只通过一次对比完成判断呢?

hash,没错就是hash,我们通过比较两个字符串的哈希值就可以完成判断,哈希算法就能保证字符串可能是相同的。

但是我们一定不能忽视hash冲突,所以当哈希值相同时,我们也要精确匹配一次。

package com.zhj.algorithm.search.str;
​
public class RKSearch implements StrSearch {
​
    @Override
    public int search(String A, String B) {
        if (A == null || A.length() == 0 || B == null || B.length() == 0) {
            return -1;
        }
        for (int i = 0; i < A.length(); i++) {
            if (i + B.length() > A.length()) {
                break;
            }
            String ASub = A.substring(i, i + B.length());
            if (hash(B) == hash(ASub) && compareString(i, A, B)) {
                return i + 1;
            }
        }
        return -1;
    }
​
    private int hash(String str) {
        int hashcode = 0;
        for (int i = 0; i < str.length(); i++) {
            hashcode += str.charAt(i) - 'a';
        }
        return hashcode;
    }
​
    private boolean compareString(int i, String A, String B) {
        boolean flag = true;
        for (int j = 0; j < B.length(); j++) {
            if (B.charAt(j) != A.charAt(i + j)) {
                flag = false;
                break;
            }
        }
        return flag;
    }
}
复制代码

缺陷:

RK的缺陷就是hash冲突,如果hash冲突过多,就会退化成BF。

时间复杂度就是O(n)

Sunday 算法

Sunday 算法是一个极易理解的算法,并且具有非常可观的查找效率

字符串A:a b a d d e a c d e a

字符串B:d e a c

原理:当字符串A与字符串B对比时,发现第一位不匹配时,这时就去查找i+ B.length(),是否在字符串B中出现,如果此时下一位在B中出现,则将出现的字符对其,然后对比是否成立,如果不成立,或者B中不存在与之相匹配的,直接移到下一位。

比较步骤一 a与d不匹配

字符串A:a b a d d e a c d e a

字符串B:d e a c

比较步骤二 d在B中存在,将其位置对应进行比较

字符串A:a b a d d e a c d e a

字符串B: d e a c

这样就会很快

如果字符串B是 e a c

比较步骤一 a与e不匹配

字符串A:a b a d d e a c d e a

字符串B:e a c

比较步骤二 d在B中比较发现没有匹配的

字符串A:a b a d d e a c d e a

字符串B: e a c

比较步骤二 直接后移B串,让其首位与下一个位对其,进行比较

字符串A:a b a d d e a c d e a

字符串B: e a c

代码实现:

package com.zhj.algorithm.search.str;
​
public class SundaySearch implements StrSearch{
​
    @Override
    public int search(String A, String B) {
        if (A == null || A.length() == 0 || B == null || B.length() == 0) {
            return -1;
        }
        int start = 0;
        while (start <= A.length() - B.length()) {
            if (A.charAt(start) == B.charAt(0)) {
                Boolean flag = true;
                for (int i = 1; i < B.length(); i++) {
                    if (A.charAt(start + i) != B.charAt(i)) {
                        flag = false;
                        break;
                    }
                }
                if (flag) {
                    return start + 1;
                }
                start += 1;
            } else {
                start += B.length();
                if (start >= A.length() - B.length()) {
                    return -1;
                }
                Boolean exist = false;
                for (int i = 0; i < B.length(); i++) {
                    if (A.charAt(start) == B.charAt(i)) {
                        exist = true;
                        start = start - i;
                        break;
                    }
                }
                if (!exist) {
                    start += 1;
                }
            }
        }
        return -1;
    }
}
复制代码

BM 算法

BM算法也是以发明者名字命名的Bob Boyer、JStrother Moore。这是一种效率极高,并且也易于理解的算法。

首先看下面一组例子

字符串A:a b a d e a w d a b e f c d e

字符串B:a b e f c d

BF 算法 会有很多轮无意义的比较,那么我们如何优化BF 算法呢?让每轮比较变的有意义,不会出现重复、无意义的遍历对比。

为了克服BF这一重大缺陷,BM算法制定了两条规则

  • 坏字符规则

    坏字符:就是B串与A串对应位置不相等的元素

    当检测(从后向前检测)到第一个坏字符后,就不需要再一位一位比较,因为只有B串中与坏字符a对齐的位置也是字符a的情况下,两者才有匹配的可能。所以我们只需要寻找B串中与坏字符相同的比较即可。如果B串不存在与坏字符相同的元素,只需要从坏字符下一位开始比较即可。

  • 好后缀规则

    好后缀:B串和A串当中相匹配的后缀。

    当检测(从后向前检测)发现有多位字符是相匹配的,然后才找到坏字符,这样的话,我们再去根据坏字符规则去移动对比,这样也可能会产生许多无效对比,反观,既然已经有多位匹配了,我们是否能应用这几位匹配到的数据,在B串中搜索是否还有与这几位相同的,这样是不是能加强效率呢?

    字符串A:a b a d d e a c d e a b e f c d e

    字符串B:d e a c d e a

    看这个例子,是不是通过好后最缀两次就能求得结果。那么,又有一个问题,如果好后最缀在B串中找不到相同的几位呢?这时是直接挪到坏字符下面一位吗?这样想就错了,而是应该缩短好后缀,继续查找是否存在相同片段

既然BM算法中存在两种规则,那么我们应该如何界定应该使用哪种规则进行移位,效率会更高呢?

我们可以在每次比较之后,对好后缀和坏字符串的挪移位置进行比较,选择挪移范围长的,如果好后缀可以挪四步,而坏字符串只能挪一步,我们就根据好后缀的规则挪移。

代码实现:

这里我没有实现好后缀规则,好后缀查找的本质就是在B串中查找好后缀(也是字符串查找算法)

package com.zhj.algorithm.search.str;

public class BMSearch implements StrSearch {

    @Override
    public int search(String A, String B) {
        if (A == null || A.length() == 0 || B == null || B.length() == 0 || A.length() < B.length()) {
            return -1;
        }
        int start = 0;
        while (start <= A.length() - B.length()) {
            int i;
            for (i= B.length() - 1; i >= 0; i--) {
                if (A.charAt(start + i) != B.charAt(i)) {
                    break;
                }
            }
            if (i < 0) {
                return start + 1;
            }
            int charIndex = findCharacter(B, A.charAt(start+i), i);
            int bcOffset = charIndex >= 0 ? i-charIndex : i+1;
            start += bcOffset;
        }
        return -1;
    }
    
    private int findCharacter(String B, char badCharacter, int index) {
        for (int i = index-1; i >= 0; i--) {
            if (B.charAt(i) == badCharacter) {
                return i;
            }
        }
        return -1;
    }

}
复制代码

KMP 算法

KMP算法也是字符串匹配算法的一种,它也是非常高效的一种字符串查找算法,KMP是由D.E.Knuth、J.H.Morris和V.R.Pratt提出的。

示例:

字符串A:A B A B A G C D C D A B A B C D E

字符串B:A B A B C D

KMP 与 BM 算法有些许相似之处,首先KMP是按正序进行比较,发现坏字符时,记录已匹配的前缀。然后取有效前缀(就是在已匹配前缀中有重叠的部分AB)然后将AB与下一个 位置的AB 进行对比。

字符串A:A B A B A G C D C D A B A B C D E

字符串B: A B A B C D

字符串A:A B A B A G C D C D A B A B C D E

字符串B: A B A B C D

KMP核心:

已匹配的前缀当中寻找到最长可匹配后缀子串最长可匹配前缀子串,下一轮直接把两者对齐,从而实现B串的快速移动。

那么如何寻找最长可匹配后缀子串最长可匹配前缀子串又成为我们面临的一个问题?

这时候我们就需要引入next 数组,那什么是next数组呢?

如图所示,我们可以根据模式串,直接生成它对应的next数组,这时候,我们需要对应的值(最长可匹配后缀子串最长可匹配前缀子串),直接根据下标获取即可。

image.png

使用KMP解决问题的流程:

  • 对模式串预处理,初始化next数组
  • 进入主循环,遍历主串
    • 比较主串和模式串的字符
    • 如果发现坏字符,查询next数组,得到匹配前缀所对应的最长可匹配前缀子串,移动模式串到对应位置
    • 如果当前字符匹配,继续循环

代码如下:

package com.zhj.algorithm.search.str;

public class KMPSearch implements StrSearch {

    @Override
    public int search(String A, String B) {
        if (A == null || A.length() == 0 || B == null || B.length() == 0 || A.length() < B.length()) {
            return -1;
        }
        int[] next = getNext(B);
        int j = 0;
        for (int i = 0; i < A.length(); i++) {
            while (j > 0 && A.charAt(i) != B.charAt(j)) {
                j = next[j];
            }
            if (A.charAt(i) == B.charAt(j)) {
                j++;
            }
            if (j == B.length()) {
                return i - B.length() + 2;
            }
        }
        return -1;
    }

    private int[] getNext(String pattern) {
        int[] next = new int[pattern.length()];
        int j = 0;
        for (int i = 2; i < pattern.length(); i++) {
            while (j != 0 && pattern.charAt(j) != pattern.charAt(i-1)) {
                j = next[j];
            }
            if (pattern.charAt(j) == pattern.charAt(i-1)) {
                j++;
            }
            next[i] = j;
        }
        return next;
    }

}
复制代码

猜你喜欢

转载自juejin.im/post/7062250792949907463