• 程序员面试金典 - 面试题 17.17. 多次搜索


    题目难度: 中等

    原题链接

    今天继续更新程序员面试金典系列, 大家在公众号 算法精选 里回复 面试金典 就能看到该系列当前连载的所有文章了, 记得关注哦~

    题目描述

    给定一个较长字符串 big 和一个包含较短字符串的数组 smalls,设计一个方法,根据 smalls 中的每一个较短字符串,对 big 进行搜索。输出 smalls 中的字符串在 big 里出现的所有位置 positions,其中 positions[i]为 smalls[i]出现的所有位置。

    示例:

    • 输入:
      • big = “mississippi”
      • smalls = [“is”,“ppi”,“hi”,“sis”,“i”,“ssippi”]
    • 输出:
      • [[1,4],[8],[],[3],[1,4,7,10],[5]]

    提示:

    • 0 <= len(big) <= 1000
    • 0 <= len(smalls[i]) <= 1000
    • smalls 的总字符数不会超过 100000。
    • 你可以认为 smalls 中没有重复字符串。
    • 所有出现的字符均为英文小写字母。

    题目思考

    1. 如何尽可能优化时间复杂度?

    解决方案

    方案 1

    思路

    • 分析题目, 一个很容易想到的方案就是使用各个语言提供的字符串 find()函数
    • 然后针对每个 small 字符串, 查找它在 big 字符串中所有可能的起点
    • 以 Python3 为例, find()可以传入待查找的起点, 并返回从那个起点开始查找到的子串的首个起点, 如果没找到则返回-1
    • 这样我们就可以不停循环上述过程, 直到找不到 small 子串为止
    • 下面代码有详细的注释, 方便大家理解

    复杂度

    • 时间复杂度 O(NMS): 假设 N 是 big 字符串的长度, M 是 smalls 字符串数组的元素个数, S 是 small 字符串的平均长度; 则外层循环复杂度为 O(M), 内层循环最多需要对每个起点匹配对应的子串, 就是 O(NS), 所以总时间复杂度是 O(NMS)
    • 空间复杂度 O(1): 除了结果数组外, 没有使用额外的空间

    代码

    class Solution:
        def multiSearch(self, big: str, smalls: List[str]) -> List[List[int]]:
            # 方法1: 语言自带find
            res = []
            for s in smalls:
                res.append([])
                # 初始化起点为-1, 因为下面的find逻辑是从i+1开始查找
                i = -1
                if s != "":
                    while True:
                        # i是上一个查找到的起点, 接下来从i+1开始查, 保证不重复
                        i = big.find(s, i + 1)
                        if i != -1:
                            # 找到了, 追加起点
                            res[-1].append(i)
                        else:
                            # 没找到, 跳出循环
                            break
            return res
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    方案 2

    思路

    • 回顾方案 1, 可以发现它在查找起点时会进行很多重复的匹配, 效率比较低, 有没有更优的方案呢?
    • 重新分析题目, 我们可以发现, 某个 small 字符串要想成为 big 的子串, 则它一定是 big 从某个起点开始的前缀
    • 所以这里我们可以引入前缀树 trie, 将所有 small 字符串加入 trie 中
    • 然后依次遍历 big 的每个起点, 查找从它开始的某个前缀是否已经在 trie 中, 是的话则说明该前缀就是一个 small 字符串
    • 这里我们需要对每个节点额外加上一个属性 index, 来代表当前 small 字符串的下标
    • 这样在找到一个前缀后, 我们就可以对结果列表对应的下标处追加上当前的 big 起点了
    • 下面代码有详细的注释, 方便大家理解

    复杂度

    • 时间复杂度 O(MS+NS): 假设 N 是 big 字符串的长度, M 是 smalls 字符串数组的元素个数, S 是 small 字符串的平均长度; 则插入 trie 的复杂度为 O(MS), 查找起点时, 外层循环复杂度为 O(N), 内层最多遍历 S 层, 复杂度为 O(S), 所以这部分总复杂度为 O(NS)
    • 空间复杂度 O(MS): 需要存储 small 字符串列表的所有字符

    代码

    class Solution:
        def multiSearch(self, big: str, smalls: List[str]) -> List[List[int]]:
            # 方法2: 逆向思考+trie存small+遍历big起点
            class Node:
                def __init__(self, c):
                    self.c = c
                    # children字典, 存储字符到节点的映射
                    self.children = {}
                    # small字符串下标, 初始化为-1, 只有当遍历到small最后一个字符时才赋值, 保证完整性
                    self.index = -1
    
            n, m = len(big), len(smalls)
            root = Node("")
            for i, w in enumerate(smalls):
                # 将当前字符串加入trie中, 达到终点时记录当前下标
                # 因为smalls的字符串都不重复, 所以不存在下标覆盖的情况
                cur = root
                for c in w:
                    if c not in cur.children:
                        cur.children[c] = Node(c)
                    cur = cur.children[c]
                # 赋值下标, 从root到cur的路径形成了完整的当前small字符串
                cur.index = i
            # 预生成结果列表
            res = [[] for _ in range(m)]
            for start in range(n):
                # 遍历big的每个起点start
                cur = root
                i = start
                while cur and i < n:
                    c = big[i]
                    if c not in cur.children:
                        # 当前字符不在trie中, 后面就更不存在有效的small字符串了, 直接退出循环
                        break
                    # 进入下一层
                    cur = cur.children[c]
                    i += 1
                    if cur.index != -1:
                        # 当前节点索引不是-1, 说明找到了一个完整的small字符串 (从root一路到cur)
                        # 此时需要将结果列表的small下标处追加上当前big的起点start, 表示这个small字符串在start位置处出现
                        res[cur.index].append(start)
            return res
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    大家可以在下面这些地方找到我~😊

    我的 GitHub

    我的 Leetcode

    我的 CSDN

    我的知乎专栏

    我的头条号

    我的牛客网博客

    我的公众号: 算法精选, 欢迎大家扫码关注~😊

    算法精选 - 微信扫一扫关注我

  • 相关阅读:
    自定义线程实现c++代码回调run方法
    前端研习录(11)——CSS3新特性——圆角及阴影讲解及示例说明
    java计算机毕业设计航空机票预订系统MyBatis+系统+LW文档+源码+调试部署
    conda 的一些指令(jupyter notebook 在虚拟环境pytorch)
    【图像分类】2022-RepLKNet CVPR 31x31卷积了解一下
    RabbitMQ-基础篇-黑马程序员
    Vue项目优化方案
    一年后斩获腾讯T5,这份呕心之作Java学习笔记有多厉害
    【Linux集群教程】10 集群监控 - Nagios 搭建
    【SpringBoot】SpringBoot:构建安全的Web应用程序
  • 原文地址:https://blog.csdn.net/zjulyx1993/article/details/126068099