LeetCode30 Hard 查找所有子串
本文始发于个人公众号:TechFlow,原创不易,求个关注
链接
Substring with Concatenation of All Words
难度
Hard
描述
给定一个字符串s作为母串,和一系列长度相等的字符串words,要求返回s当中所有的位置,使得从该位置开始可以找到所有的words,并且所有的words只出现一次
You are given a string, s , and a list of words, words , that are all
of the same length. Find all starting indices of substring(s) in s that is
a concatenation of each word in words exactly once and without any
intervening characters.
样例 1:
**Input:
s =** "barfoothefoobarman",
**words =** ["foo","bar"]
Output: [0,9]
## Explanation: Substrings starting at index 0 and 9 are "barfoor" and "foobar" respectively.
The output order does not matter, returning [9,0] is fine too.
样例 2:
**Input:
s =** "wordgoodgoodgoodbestword",
**words =** ["word","good","best","word"]
Output: []
题解
这道题的难度是Hard,老实讲的确不简单,尤其是如果在面试当中被问到,恐怕很难一下想出最佳答案。
暴力
还是老规矩,我们退而求其次,忘了最佳答案这茬,先想出简单的方法再来思考怎么优化。最简单的方法当然是暴力,我们首先遍历所有的起始位置,然后后面一个单词一个单词的匹配。如果成功匹配就记录答案,失败的话则继续搜索下一个位置。
这么做看起来没有问题,但是一些细节需要注意。比如题目当中只说单词的长度一样,并没有说单词会不会重复。显然我们应该考虑单词出现重复的情况,既然要考虑单词出现重复,那么就不能用一个set来记录单词是否出现过,而是需要统计每个单词出现的个数。其次,我们在遍历的时候,也一样,也需要统计当前匹配到的单词的数量。
这道题暴力的思路还是比较清晰的,代码也不难写:
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
n = len(s)
# 单词不存在直接返回
if len(words) == 0:
return []
ret = []
word_cnt = len(words)
m = len(words[0])
words_dict = {}
# 初始化,记录词表
for word in words:
words_dict[word] = words_dict.get(word, 0) + 1
# 枚举开始的位置
for i in range(n):
cur_dict = {}
matched = 0
# 每次遍历一个单词
for start in range(i, n, m):
w = s[start: start+m]
# 如果单词存在,并且当前匹配的数量小于目标,则进行记录
if w in words_dict and cur_dict.get(w, 0) < words_dict[w]:
cur_dict[w] = cur_dict.get(w, 0) + 1
matched += 1
else:
break
# 所有单词已经匹配
if matched == word_cnt:
ret.append(i)
break
return ret
我们来分析一下这个算法的复杂度,我们在搜索的时候用到了两层循环。外层的循环遍历了所有的长度,内层的循环则是一个单词一个单词地枚举,在极端情况下依旧可以遍历完整个字符串,复杂度是\(\frac{n}{m}\)。但是由于m是常数,并且极端情况下等于1,所以整个算法的最坏的时间复杂度依然是\(O(n^2)\)。
这题官方卡的不严,即使是暴力的方法也可以通过。如果是在正规的算法竞赛当中,一定会卡时间,暴力的方法肯定是无法通过的。所以我们必须要进行优化。
Two pointers
在阐述优化方案之前,我们先来做一个仔细的分析。在这题当中,由于我们需要找到所有满足条件的答案,那么显然我们需要把所有可能的情况都遍历完。也就是说遍历是免不了的,在这题当中我们肯定不可能自己生成出答案,一定需要遍历。说白了,遍历所有情况的思路是对的,我们要做的并不是寻找新的方法,而是对它进行优化。
明白了前进的方向,就可以继续往下思考第二个问题了。究竟在暴力方法当中是哪里有问题,导致了大量消耗时间,哪里可以进行优化呢?
理一下思路不难想明白,会出现重复的情况只有两种。下面我们来列举一下,为了方便观看和理解, 我用[]表示一个单词,通过[]内的不同数字,表示不同的单词。
- ...[1][2][3]....[1][2][3]....,这种情况最容易想到。在一个正确答案后面一段距离之后还有另一个正确答案,由于我们每次找到正确答案就退出了,所以又需要遍历很多次才可以找到下一个答案。
- ....[1][2][1][2][3]....,这种情况当中,我们在找到了前面第一个错误的[1][2]之后,由于发现不对,所以退出了循环。接下来我们要遍历2m次(单词长度为m),才可以找到[1][2][3]这个答案。要是当时我们可以将错就错继续往下搜索,就可以直接找到答案了。
把上面两点综合一下,优化的方案其实已经很清楚了。就是不管是我们找到了答案还是没找到答案,遇到了问题,我们都不应该退出,我们应该继续搜索其他潜在的答案。
优化1
所以我们就得到了第一个优化,既然我们每次不论成功与否都会遍历结束,而且我们每一次遍历的时候,都会获取m长度的字符串和词库进行比较。那么我们在遍历起始位置的时候,就不用遍历n的长度了,而只需要遍历m个长度。
举个例子,比如说s='abcgoodgoodgirl',词库是['good', 'girl']。
我们第一次遍历a,可以获得这些单词:abcg, oodg, oodg, irl
第二次遍历遍历b,得到的单词是:a, bcgo, odgo, odgi, rl
第三次遍历c,单词是: ab, cgoo, dgoo, dgir, l
最后是遍历g,单词是: abc, good, good, girl
这样我们只需要遍历4次,就可以获取所有的单词组合。也就是说我们先获取所有的单词组合之后,再从这些组合当中寻找答案。所以我们将最外层的循环次数从n降到了m。
优化2
依然参考上面的例子,我们可以发现在上面4次遍历当中,只有最后一次能找到答案。我们单独来看这次的遍历内容:abc, good, good, girl。由于词库是['good', 'girl'],我们在遍历这个单词组合的时候,会遇到两个good,这和我们的逾期不符。按照正常的思路来看,我们应该跳过,然后将记录的答案清空,从下一个单词处开始遍历。
这当然是可以的,但是实际上,这个问题有更好的解法。如果对two pointers算法熟悉的同学,会发现这是一个经典的two pointers算法的应用场景。我们要找的是一个若干个连续的单词组成的区间,那么我们可以用两个指针维护这个区间。当我们右侧读入一个额外的单词导致数量超界的时候,应该怎么办?很简单,我们可以移动左侧边界,弹出掉一些单词,直到数量满足要求。
如果有对two pointers算法不了解的同学可以点击下面的链接回顾一下之前的内容:
我们把上面的思路整理一下,就可以写出代码了:
class Solution:
def findSubstring(self, s: str, words: List[str]) -> List[int]:
n = len(s)
if len(words) == 0:
return []
# 初始化的部分和之前一样
ret = []
word_cnt = len(words)
m = len(words[0])
words_dict = {}
for word in words:
words_dict[word] = words_dict.get(word, 0) + 1
# 只遍历[0, m)
for i in range(m):
cur_dict = {}
# l和r表示当前的区间两侧端点
l = i
matched = 0
for r in range(i, n, m):
# 获取当前的单词
word = s[r: r+m]
# 如果单词不在词库当中,清空之前的数据
if word not in words_dict:
# l赋值成下一个开始的r
l = r + m
# 所有匹配记录清空
matched = 0
cur_dict = {}
continue
# 记录单词
cur_dict[word] = cur_dict.get(word, 0) + 1
matched += 1
# 如果数量超界的话,就弹出左侧
while cur_dict[word] > words_dict[word]:
w = s[l: l+m]
cur_dict[w] -= 1
matched -= 1
l += m
# 如果匹配数量一致,则记录答案,也就是l的位置
if matched == word_cnt:
ret.append(l)
return ret
代码不长,但是里面的细节还是不少的,关于边界的处理以及一些运算的逻辑,真正想要一口气写正确还是很有挑战的。感兴趣的同学可以试试看,在不参考我代码的情况下,能不能一次写通过。
这道题给我最大的感受是从表面上看,它似乎是一道字符串匹配的问题。会引导我们往各种字符串匹配的算法上去思考,但其实它是一个遍历优化的问题。这道题在LeetCode当中评分不高,很多人给了差评,也许就是因为许多人被出题人骗了吧。但是我觉得它很有意思,也很锻炼人,不是那种无脑折磨人的题。毕竟在算法竞赛当中出题人”欺骗“选手是常有的事,这也是算法的魅力之一。
今天的文章就是这些,如果觉得有所收获,请顺手扫码点个关注吧,你们的举手之劳对我来说很重要。
LeetCode30 Hard 查找所有子串的更多相关文章
- 从vector容器中查找一个子串:search()算法
如果要从vector容器中查找是否存在一个子串序列,就像从一个字符串中查找子串那样,次数find()与find_if()算法就不起作用了,需要采用search()算法:例子: #include &qu ...
- java基础知识回顾之---java String final类普通方法的应用之“子串在整串中出现的次数”
/* * 2 一个子串在整串中出现的次数. * "loveerlovetyloveuiloveoplove" * 思路: * 1,要找的子串是否存在,如果存在获取其出现的位置.这个 ...
- C 查找子字符串
自己用 C 写的一个查找子字符串的函数 int findstr(char *str,char *substr) //C实现 find{ if(NULL == str || NULL== substr) ...
- hdu 3065 AC自动机(各子串出现的次数)
病毒侵袭持续中 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Sub ...
- [C++] 习题 2.18 倒序查找字串
目录 前置技能 字符串 KMP 算法 需求描述 概要设计 具体实现 string.cpp strmatching.cpp main.cpp 倒序查找字串: 设计一个算法,在串 str 中查找字串 su ...
- 数据结构(c语言第2版)-----了解链表,栈,队列,串
关于链表我觉得这都是最基本的东西,但是不常见,在实际的应用中很少的使用,了解它会用就OK,不需要研究的那么深,除非做那种内存压缩,存储方面工作. C语言中动态申请空间 malloc() q=(dlin ...
- C语言字符串操作总结大全
1)字符串操作 strcpy(p, p1) 复制字符串 函数原型strncpy(p, p1, n) 复制指定长度字符串 函数原型strcat(p, p1) 附加字符串 函数原型strn ...
- freemarker内置函数和用法
原文链接:http://www.iteye.com/topic/908500 在我们应用Freemarker 过程中,经常会操作例如字符串,数字,集合等,却不清楚Freemrker 有没有类似于Jav ...
- Java数据结构之字符串模式匹配算法---Brute-Force算法
模式匹配 在字符串匹配问题中,我们期待察看源串 " S串 " 中是否含有目标串 " 串T " (也叫模式串).其中 串S被称为主串,串T被称为子串. 1.如果在 ...
随机推荐
- Pycharm学习记录---注释
在方法下面添加三个双引号然后回车即可添加参数注释
- 利用selenium模拟登陆
第一部:利用selenium登陆 导入selenium库 from selenium import webdriver 明确模拟浏览器在电脑中存放的位置,比如我存在当前目录 chromePath = ...
- Spring Cloud第十四篇 | Api网关Zuul
本文是Spring Cloud专栏的第十四篇文章,了解前十三篇文章内容有助于更好的理解本文: Spring Cloud第一篇 | Spring Cloud前言及其常用组件介绍概览 Spring C ...
- 15-cookie技术和session技术的联系和区别
1. 联系: *session实现依赖于Cookie 2. session问题: * 由服务器创建,存储在服务器 * 当浏览器关闭时,服务器不关闭,再次打开浏览器时, 默认获得的不是同一个sessi ...
- Apache httpd.conf配置文件 3(虚拟主机)
### Section 3: Virtual Hosts 第三部分 虚拟主机 注意:在使用虚拟主机前,请先检查 http.conf 的 辅助配置文件httpd-vhosts.conf 是否注释 # ...
- win10双击执行python
一. 设置py环境 去官网下载Windows x86-64 executable installer安装 安装后会自动配置py的bin路径和pip的路径 Pip用于安装python库的 二. 设置wi ...
- Jenkins新建节点找不到通过Java web启动代理?
参考博客:Jenkins新建节点,启动方式没有“通过Java Web启动代理”选项怎么办? 在Jenkins中,打开“系统管理”→“管理节点”→“新建节点”页面时,“启动方式”选项没有“通过Java ...
- webpack nodejs npm关系
nodejs是js后端运行平台,可以把它看成java体系中对应的jdk,是三个里面最基础的.npm是nodejs的包管理工具,可以把它看成maven中包依赖管理那部分.webpack是前端工程化打包工 ...
- python函数定义中引用外部变量的一个问题
如果在函数定义的默认值中引用了一个外部变量,如下所示 x = 3 def func(a = x): print(a, x) 那么a的默认值就会是3, 但是print语句中的x会是调用时的x值 lamb ...
- Kakfa集群(2.11-0.10.1.0)版本滚动升级方案
Kafka集群版本升级(2.11-0.10.1.0)升级(2.11-0.10.2.2) 官网升级说明: 一.系统环境Zookeeper集群:172.16.2.10172.16.2.11172.16.2 ...