LeetCode 1967. 作为子字符串出现在单词中的字符串数目(BM、KMP)

2023-11-14

给你一个字符串数组 patterns 和一个字符串 word ,统计 patterns 中有多少个字符串是 word 的子字符串。返回字符串数目。

子字符串 是字符串中的一个连续字符序列。

示例 1:

输入:patterns = [“a”,“abc”,“bc”,“d”], word = “abc”
输出:3
解释:

  • “a” 是 “abc” 的子字符串。
  • “abc” 是 “abc” 的子字符串。
  • “bc” 是 “abc” 的子字符串。
  • “d” 不是 “abc” 的子字符串。
    patterns 中有 3 个字符串作为子字符串出现在 word 中。

1 <= patterns.length <= 100
1 <= patterns[i].length <= 100
1 <= word.length <= 100
patterns[i] 和 word 由小写英文字母组成

解法一:直接用库函数:

class Solution {
public:
    int numOfStrings(vector<string>& patterns, string word) {
        int ans = 0;
        for (string &s : patterns) {
            if (word.find(s) != string::npos) {
                ++ans;
            }
        }

        return ans;
    }
};

如果输入数组patterns的长度为n,其中元素的长度为m,word的长度为l,此算法时间复杂度为O(nml),空间复杂度为O(1)。

解法二:暴力匹配,遍历patterns中的每个字符串s,看word中是否有字符串s:

class Solution {
public:
    int numOfStrings(vector<string>& patterns, string word) {
        int ans = 0;
        int wordSz = word.size();
        for (string &s : patterns) {
            int sSz = s.size();
            for (int i = 0; i < wordSz - sSz + 1; ++i) {
                int j = 0;
                for (j = 0; j < sSz; ++j) {
                    if (word[i + j] != s[j]) {
                        break;
                    }
                }

                if (j == sSz) {
                    ++ans;
                    break;
                }
            }
        }

        return ans;
    }
};

如果输入数组patterns的长度为n,其中元素的长度为m,word的长度为l,此算法时间复杂度为O(nml),空间复杂度为O(1)。

解法三:BM算法,全称为Boyer-Moore算法,它是在1977年由德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明的。下面用两位教授在论文中给出的例子来介绍该算法:文本串为HERE IS A SIMPLE EXAMPLE,模式串为EXAMPLE。开始时,把两个串头部对齐,从尾部开始对比:

HERE IS A SIMPLE EXAMPLE
EXAMPLE

如果最后一个字符不匹配(如上例中的S和E不匹配),我们称该不匹配的字符S为坏字符,且坏字符没有出现在文本串中,因此直接把模式串后移模式串的长度位即可,因此移动后的情况如下:

HERE IS A SIMPLE EXAMPLE
       EXAMPLE

之后我们看到P和E仍然不匹配,但坏字符P在模式串中也存在,因此只需要右移模式串使模式串和文本串中的坏字符P对齐:

HERE IS A SIMPLE EXAMPLE
         EXAMPLE

因此我们有了坏字符的移位规则,即模式串的向右移位的位数为坏字符对应模式串中的字符位置(如上例中,坏字符P作匹配时,匹配的是模式串中的E,该模式串中的E所在下标为6)减去坏字符在模式串中最后一次出现的位置(即模式串中的P所在下标,为4),因此上例中右移了两位。而在第一次匹配时,坏字符为S,而S没有在模式串中出现过,因此将S在模式串中最后一次出现的位置设为-1,这样6-(-1)=7,正好跳过整个模式串。

之后我们继续从后向前比较,会发现MPLE是匹配的,此时如果按坏字符移位规则,此时的坏字符为I,I在模式串中没有出现过,且当前坏字符I对应的模式串中的字符为A,A的下标为2,因此模式串右移位数为2-(-1)=3,坏字符规则移位结果如下:

HERE IS A SIMPLE EXAMPLE
            EXAMPLE

但现在有一个更好的移位方式,我们将末尾匹配的部分称为好后缀,上例中,E、LE、PLE、MPLE都是好后缀。假如模式串为ABCDAB,且AB是它的一个好后缀,而这个好后缀在文本串中还出现了一次,我们就把模式串右移4位,把第一个AB移动到第二个AB处。如果模式串中有多个好后缀,则将倒数第二次出现的好后缀移动到当前匹配的好后缀处。

有关好后缀还有一点,如果最长的好后缀没有出现在模式串的其他位置,则选择出现在模式串头部的其他好后缀,如在E、LE、PLE、MPLE这四个好后缀中,在模式串中MPLE只出现过一次,且只有好后缀E出现在了模式串头部,因此根据好后缀规则移位后的结果为:

HERE IS A SIMPLE EXAMPLE
               EXAMPLE

可见根据坏字符规则只能移3位,而根据好后缀规则可以移动6位,我们选择两者中移位次数更多的值。

之后继续比较,发现坏字符为P,坏字符对应模式串中的字符为E、位置为6,而P在模式串中最后一次出现的位置为4,因此需要右移6-4=2位,移位后结果为:

HERE IS A SIMPLE EXAMPLE
                 EXAMPLE

然后进行匹配,发现匹配成功。如果还想继续匹配(全部匹配),根据好后缀规则,需要右移模式串6位,使开头和末尾的E重合。

以下是bm算法的实现:

class Solution {
public:
    int numOfStrings(vector<string>& patterns, string word) {
        int ans = 0;
        int wordSz = word.size();
        for (string &s : patterns) {
            if (bm(s, word) >= 0) {
                ++ans;
            }
        }

        return ans;
    }

private:
	// map会值初始化int为0,我们用该结构体代替int,从而代替int的值初始化
    class intDefaultMinusOne {
    public:
        int num = -1;
    };

    int bm(string &needle, string &haystack) {
        // 找到needle中每个字符最后一次出现的位置,没有出现过的字符,map会将其位置初始化为成员为-1的结构体
        unordered_map<char, intDefaultMinusOne> lastAppear;
        int needleLength = needle.size();
        for (int i = 0; i < needleLength; ++i) {
            lastAppear[needle[i]].num = i;
        }

		// 找到needle的每一个后缀串在needle中倒数第二次出现的起始位置
		// lastSuffixAppear的key是后缀串长度,值为该长度后缀串在needle中倒数第二次出现的位置
        unordered_map<int, intDefaultMinusOne> lastSuffixAppear;
        for (int i = 0; i < needleLength - 1; ++i) {
            int j = i;
            for (; j >= 0; --j) {
                if (needle[j] != needle[needleLength - i + j - 1]) {
                    break;
                }

                lastSuffixAppear[i - j + 1].num = j;
            }
        }

        int i = 0;
        int haystackLength = haystack.size();
        while (i <= haystackLength - needleLength) {
            int j = needleLength - 1;
            for (; j >= 0; --j) {
                if (needle[j] != haystack[i + j]) {
                    break;
                }
            }
            // 匹配成功
            if (j < 0) {
                return i;
            }

			// 找到坏字符条件下可以跳过多少搜索起点
            int badCharSkip = 0;
            // 如果needle中有坏字符
            if (lastAppear[haystack[i + j]].num != -1) {
                badCharSkip = j - lastAppear[haystack[i + j]].num;
            } else {    // 如果needle中没有坏字符,将下次搜索的起始位置移动到坏字符后面的那个位置
                badCharSkip = j + 1;
            }

			// 找到好后缀条件下可以跳过多少搜索起点
            int goodSuffixSkip = 0;
            do {
                int goodSuffixLenth = needleLength - j - 1;
                // 如果好后缀在needle中出现过至少两次
                if (lastSuffixAppear[goodSuffixLenth].num != -1) {
                    goodSuffixSkip = j - lastSuffixAppear[goodSuffixLenth].num;
                    break;
                }
				// 如果好后缀串在needle中只出现过一次,则找好后缀串的子串是否出现在needle的头部
                for (int suffixLen = goodSuffixLenth - 1; suffixLen >= 0; --suffixLen) {
                    if (lastSuffixAppear[suffixLen].num != 0) {
                        continue;
                    }

                    goodSuffixSkip = j - lastSuffixAppear[suffixLen].num;
                    break;
                }
            } while (0);
            // 选更大的跳过搜索位置
            int skip = max(badCharSkip, goodSuffixSkip);
            // 如果两种方式都找不到要跳过的搜索位置,说明整个串都要被跳过
            if (skip <= 0) {
                skip = needleLength;
            }

            i += skip;
        }

        return -1;
    }
};

如果n为文本串的长度,m为模式串的长度,对于BM算法,时间复杂度最好为O(n/m),最坏为O(nm)。一般文本搜索算法都使用BM,如Windows记事本的Ctrl+F搜索、Unix的grep命令。

如果输入数组patterns的长度为n,其中元素的长度为m,word的长度为l,此算法时间复杂度为O(nl/m);空间复杂度为O(n(m+字符集大小)),字符集大小来源于记录每个字符最后一次出现的位置的lastAppear,m来源于记录后缀串首次出现的lastSuffixAppear,如果每个后缀串都在needle中出现过,则需要m大小的空间。

解法四:KMP算法,假如模式串是abeabf,文本串为abeababeabf,我们从左往右比较两个串:

abeababeabf
abeabf

我们从左往右匹配两个串,发现前5个字符是匹配的,但第六个字符不匹配。前5个字符中,拥有相同的前后缀ab,因此我们可以把模式串右移,直到前缀到达原来的后缀处:

abeababeabf
   abeabf

然后我们继续匹配,会发现前两个字符是匹配的,但第3个字符就不匹配了,匹配的两个字符为ab,没有相同的前后缀,因此直接右移模式串到当前匹配的部分之后:

abeababeabf
     abeabf

再进行匹配,会发现匹配成功。

根据以上过程我们发现,当出现不匹配的情况时,模式串右移的位数取决于当前已经匹配的部分是否有相同前后缀,如果有,就把前缀移动到相同后缀处,如果没有,就直接右移模式串到当前匹配的部分之后。因此我们需要构建一个数组,数组的key是模式串的前缀长度,值是该前缀长度的相同前后缀字符数。

以下是kmp实现:

class Solution {
public:
    int numOfStrings(vector<string>& patterns, string word) {
        int ans = 0;
        int wordSz = word.size();
        for (string &s : patterns) {
            if (kmp(s, word) >= 0) {
                ++ans;
            }
        }

        return ans;
    }

private:
    int kmp(string &needle, string &haystack) {
        int needleLen = needle.size();
        vector<int> next(needleLen, -1);
        findNext(needle, next);

        int needleIdx = 0;
        int haystackLen = haystack.size();
        for (int haystackIdx = 0; haystackIdx < haystackLen; ++haystackIdx) {
            while (needleIdx > 0 && needle[needleIdx] != haystack[haystackIdx]) {
                needleIdx = next[needleIdx - 1] + 1;
            }
            
            if (needle[needleIdx] == haystack[haystackIdx]) {
                ++needleIdx;
            }
            
            if (needleIdx == needleLen) {
                return haystackIdx - needleIdx + 1;
            }
        }

        return -1;
    }

    // 如果needle为ababf,则返回的next为-1 -1 0 1 -1
    void findNext(string &needle, vector<int> &next) {
        int k = -1;
        int nextLen = next.size();
        for (int i = 1; i < nextLen; ++i) {
            while (k != -1 && needle[k + 1] != needle[i]) {
                k = next[k];
            }

            if (needle[k + 1] == needle[i]) {
                ++k;
            }

            next[i] = k;
        }
    }
};

如果输入数组patterns的长度为n,其中元素的长度为m,word的长度为l,此算法时间复杂度为O(n(m+l)),空间复杂度为O(m)。

以上算法构建next的过程中,while循环里令k = next[k],此行代码可由一个例子解释,如当前已遍历到i,有如下包含i个字符的字符串,用.表示相同前后缀,*表示非最长前后缀的内容:
...*****...
如果我们想找[0,i+1]子串的最长前后缀,我们需要比较下标为4和i+1(下标为i+1的字符用-来表示)的两个字符是否相同,即下图中两个箭头对应的字符是否相同:
...****...-
...↑***...↑
如果不同,我们需要看前3个字符的最长前后缀,这是由于,假如前3个字符的最长前后缀是2个字节,则表示遍历到i时的后3个字符的最长前后缀也是2个字节(因为遍历到i时的最长前后缀长度为3,意味着前3个和后3个字符是相同的),因此遍历到i时,下标组[0,1]、[1,2]、[i-2,i-1]、[i-1,i]是相同的,其中最重要的信息是下标组[0,1]和[i-1,i]是相同的,因此我们只需要比较下标2和下标i+1是否相同即可,因此需要每次都需要回溯到[0,i]的最长前后缀的最长前后缀(此例中为1,即next[k]),因此需要k = next[k]

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

LeetCode 1967. 作为子字符串出现在单词中的字符串数目(BM、KMP) 的相关文章

  • 五个实施环节

    定级 定级流程 在 信息系统安全等级保护定级指南 中 说明了定级的一般流程 1 确定作为定级对象的信息系统 2 确定业务信息安全受到破坏时所侵害的客体 3 根据不同的受侵害客体 从多个方面综合评定业务信息安全被破坏对客体的侵害程度 4 根据

随机推荐

  • 专项-弱网络测试

    弱网络 简单理解 网络不好 网络环境复杂 使用场景多变 异常逻辑检查 弱网络测什么 测试标准 客户端的核心场景必须有断线重连机制 并在有网络抖动 延时 丢包的网络场景下 客户端需达到以下要求 一 不能出现以下现象 1 游戏中不能出现收支不等
  • Lloyd-Max条件、DPCM系统最佳预测系数推导以及最小二乘:梯度下降、牛顿法、高斯牛顿法总结

    文章目录 1 Lloyd Max条件推导 2 DPCM系统最佳预测系数推导 3 最小二乘 梯度下降 牛顿法 高斯牛顿法总结 3 1 梯度下降 3 2 牛顿法 3 3 高斯牛顿法 1 Lloyd Max条件推导 2 DPCM系统最佳预测系数推
  • Kimera-VIO-ROS,Kimera-Semantic源码运行结果及问题解决

    前期条件准备 1 VMware Ubuntu18 04 2 ROS rviz melodic 3 Eigen3 3 7 一 Kimera VIO ROS 1 源码运行 Polygon mode solid Polygon mode wire
  • vue+springboot中使用ueditor,路由跳转后再进入ueditor,ueditor无法加载出来

    问题描述 已经在标题上了 解决办法 1 在页面销毁时删除ueditor实例 mounted时创建实例 这样做的目的是再次进来时重新加载ueditor mounted 自定义上传路径 const baseUrl http localhost
  • deebot扫地机器人响四声_科沃斯扫地机器人故障声叫4声什么故障_科沃斯扫地机器人故障...

    科沃斯扫地机器人初次使用时 请多多陪同 这样能及时发现问题 解决问题 常见故障二 地宝出现语音提示 运行困难 请检查驱动轮 解决方法 关机后滑动驱动轮 检查其能否转动 回弹 并清理干净 然后把地宝放回原地 点击主机启动键即可继续工作 常见
  • APK的安装流程

    文章目录 我们来思考一下Android系统是如何安装一个APK文件的 从直观的流程上 当我们点击一个APK文件或者从应用商店下载一个APK文件 会弹起一个安装对话框 点击安装就可以安装应用 那么这里面的流程是什么样的呢 首先很容易想到的是
  • QT增加链接库和头文件搜索目录(相对目录)

    QT开发的时候 需要增加链接的动态库或者静态库 或者搜索的头文件 正常情况下 使用相对目录是最好的 下面是常用的方法 1 增加库依赖 如下 OUT PWD表示QT编译后的输出目录 比如Debug或者Release目录 后续发布的时候 把so
  • 【设计模式】Java设计模式——模板方法模式(Template Pattern)

    文章目录 1 介绍 1 1 定义 1 2 作用 2 模式结构 2 1 UML类图 2 2 模式组成 3 代码实例 3 1 背景 3 2 应用 4 优点 5 缺点 6 应用场景 1 介绍 1 1 定义 模板方法模式 Template Patt
  • 上古神器Vim

    玩转Vim 世界上10种人 会用Vim的和不会用Vim的 玩转Vim 从放弃到爱不释手 什么是Vim Linux下两大编辑神器之一Vim Linux Unix下使用最多的编辑器 Vi的改进版 可能是最难上手的编辑器之一 为什么要学习Vim
  • 【经验分享】用PS如何将图片的四角做成圆弧角

    经验分享 用PS如何将图片的四角做成圆弧角 在很多情况下圆角图片看起来更美观整洁 今天分享一下自己经常使用 PS 是如何将图片做出圆弧角 仅供参考 以下面这张图片为例 一 在 PS 中打开素材图片 选择 圆角矩形工具 二 在上方选项卡中选择
  • 基于javaweb的网上电商项目(前后端分离+java+vue+springboot+ssm+mysql+redis)

    基于javaweb的网上电商项目 前后端分离 java vue springboot ssm mysql redis 运行环境 Java 8 MySQL 5 7 Node js 10 开发工具 后端 eclipse idea myeclip
  • chrony服务部署,实现时间同步

    目录 chrony服务部署实验 实验一 第一台机器从阿里云同步时间 第二台机器从第一台机器同步时间 实验二 第一台服务器使用系统时间作为第二台服务器的时钟源 第一台服务器层级设置为6 问题排错 NTP 是网络时间协议 Network Tim
  • 记一次ubuntu16误删libc.so.6操作的恢复过程

    背景 操作系统 ubuntu16 glibc版本 2 23 修改原因 经过一系列报错和手工构建之后 vulkansdk成功安装 起码运行 vulkansdk成功 在进行 vulkaninfo进行验证时 报错 意思是当前glibc版本过低 需
  • 数字化转型面临的五大挑战

    如果将数字化转型分成五个阶段 全球大多数组织正处在第二和第三的僵局阶段 而处于第四和第五阶段的组织 全球不超过25 总体而言 未来数字化转型将面临五大挑战 一是陈旧的考核体系 数字企业需要新的度量标准来了解进度和引导投资 如果考核体系不变
  • 1_01李婉玲_函数_1019

    day04作业练习 作业01 小明和他家人在泰国旅游 到3个不同的饭店吃饭 账单 bill 分别是124元 48元和268元 为了给服务员小费 tip 小明创建了一个简单 的小费计算器函数 tipCalculator a 如果账单小于50元
  • spring jdbctemplate操作数据库

  • 【高数】Abel定理,幂级数的和收敛半径,不同幂级数收敛半径的比较,缺项幂级数的解法

    目录 一 收敛区间及收敛点 二 收敛半径的变化 三 借助正项级数敛散性求幂级数收敛区间 四 缺项幂级数的解法 五 小结 一 收敛区间及收敛点 现对该形式的幂级数进行如下讨论 1 幂级数的收敛区间 对称中心是x0 收敛半径是R 2 经有限次的
  • 最美丽的理论:爱因斯坦引力场方程的推导

    全文共2948字 预计学习时长9分钟 图源 superprof 伟大的前苏联物理学家列夫 朗道和叶夫根尼 利夫希茨在他们的著作 经典场论 中写道 建立在相对论基础上的引力场理论被称为广义相对论 它是由爱因斯坦建立的 并且可能是现存的物理理论
  • 局域网速度测试,三款软件轻松搞定(转)

    局域网络可谓随处可见 我们也十分关注其实际运行速度如何 比如两台计算机间的文件传输 访问对方计算机的快慢等 而决定局域网络速度的因素很多 又不可能通过简单的操作检测出速度的大小 同时也希望能有一些软件能帮助我们管理局域网 以方便故障的排查
  • LeetCode 1967. 作为子字符串出现在单词中的字符串数目(BM、KMP)

    给你一个字符串数组 patterns 和一个字符串 word 统计 patterns 中有多少个字符串是 word 的子字符串 返回字符串数目 子字符串 是字符串中的一个连续字符序列 示例 1 输入 patterns a abc bc d