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

简介: 你们了解字符串匹配算法吗?在我们刷算法题的时候,总有一类问题是解决字符串匹配的,其实字符串匹配是有算法技巧的,最基本的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;
    }

}
目录
相关文章
|
3月前
|
算法
【算法】滑动窗口——找到字符串中所有字母异位词
【算法】滑动窗口——找到字符串中所有字母异位词
|
1月前
|
算法
两个字符串匹配出最长公共子序列算法
本文介绍了最长公共子序列(LCS)问题的算法实现,通过动态规划方法求解两个字符串的最长公共子序列,并提供了具体的编程实现细节和示例。
74 1
两个字符串匹配出最长公共子序列算法
|
3月前
|
算法 Java
掌握算法学习之字符串经典用法
文章总结了字符串在算法领域的经典用法,特别是通过双指针法来实现字符串的反转操作,并提供了LeetCode上相关题目的Java代码实现,强调了掌握这些技巧对于提升算法思维的重要性。
|
4月前
|
自然语言处理 算法 搜索推荐
字符串相似度算法完全指南:编辑、令牌与序列三类算法的全面解析与深入分析
在自然语言处理领域,人们经常需要比较字符串,这些字符串可能是单词、句子、段落甚至是整个文档。如何快速判断两个单词或句子是否相似,或者相似度是好还是差。这类似于我们使用手机打错一个词,但手机会建议正确的词来修正它,那么这种如何判断字符串相似度呢?本文将详细介绍这个问题。
273 1
|
4月前
|
数据采集 算法 JavaScript
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
JavaScript字符串搜索涵盖`indexOf`、`includes`及KMP算法。`indexOf`返回子字符串位置,`includes`检查是否包含子字符串。KMP是高效的搜索算法,尤其适合长模式匹配。示例展示了如何在数据采集(如网页爬虫)中使用这些方法,结合代理IP进行安全搜索。代码示例中,搜索百度新闻结果并检测是否含有特定字符串。学习这些技术能提升编程效率和性能。
118 1
揭开JavaScript字符串搜索的秘密:indexOf、includes与KMP算法
|
3月前
|
算法 C++
惊爆!KPM算法背后的秘密武器:一行代码揭秘字符串最小周期的终极奥义,让你秒变编程界周期大师!
【8月更文挑战第4天】字符串最小周期问题旨在找出字符串中最短重复子串的长度。KPM(实为KMP,Knuth-Morris-Pratt)算法,虽主要用于字符串匹配,但其生成的前缀函数(next数组)也可用于求解最小周期。核心思想是构建LPS数组,记录模式串中每个位置的最长相等前后缀长度。对于长度为n的字符串S,其最小周期T可通过公式ans = n - LPS[n-1]求得。通过分析周期字符串的特性,可证明该方法的有效性。提供的C++示例代码展示了如何计算给定字符串的最小周期,体现了KPM算法在解决此类问题上的高效性。
81 0
|
5月前
|
存储 算法 Cloud Native
C++ bcrypt算法 字符串加密,亲测有效
C++ bcrypt算法 字符串加密,亲测有效
|
5月前
|
存储 算法
算法训练,牛客.判断是不是平衡二叉树 牛客.最大子矩阵两个数组的交集牛客.数组中两个字符串的最小距离
算法训练,牛客.判断是不是平衡二叉树 牛客.最大子矩阵两个数组的交集牛客.数组中两个字符串的最小距离
算法训练,牛客.判断是不是平衡二叉树 牛客.最大子矩阵两个数组的交集牛客.数组中两个字符串的最小距离
|
4月前
|
算法 Java
KMP算法详解及其在字符串匹配中的应用
KMP算法详解及其在字符串匹配中的应用
|
5月前
|
存储 算法 Java
Java数据结构与算法:用于高效地存储和检索字符串数据集
Java数据结构与算法:用于高效地存储和检索字符串数据集