从找朋友到找变位词:一道趣味字符串问题的深入解析|LeetCode 438 找到字符串中所有字母异位词
LeetCode 438 找到字符串中所有字母异位词
点此看全部题解 LeetCode必刷100题:一份来自面试官的算法地图(题解持续更新中)
生活中的算法
还记得小时候玩的"找朋友"游戏吗?每个人都有一个字母牌,需要找到拥有相同字母组合的伙伴。比如,拿着"ate"的同学要找到拿着"eat"或"tea"的同学。这其实就是在寻找字母异位词!
在实际应用中,字母异位词的检测有着广泛的用途。比如在密码学中检测可能的密文变体,或在文本分析中找出词语的不同排列组合。
问题描述
LeetCode第438题"找到字符串中所有字母异位词"是这样描述的:给定两个字符串 s 和 p,找到 s 中所有 p 的异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词指由相同字母重排列形成的字符串(包括相同的字符串)。
例如:
- 输入: s = “cbaebabacd”, p = “abc”
- 输出: [0,6]
- 解释: 起始索引等于 0 的子串是 “cba”, 它是 “abc” 的异位词。起始索引等于 6 的子串是 “bac”, 它也是 “abc” 的异位词。
最直观的解法:排序比较法
最容易想到的方法是:取出s中长度等于p的每个子串,将子串和p分别排序后比较是否相等。如果相等,就找到了一个异位词。
让我们用一个例子来模拟这个过程:
s = "cbae", p = "abc"
检查所有长度为3的子串:
"cba" -> 排序后:"abc" = "abc"(p排序后) ✓
"bae" -> 排序后:"abe" ≠ "abc"(p排序后) ✗
这种思路可以用Java代码这样实现:
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s.length() < p.length()) return result;
// 将p排序
char[] sortedP = p.toCharArray();
Arrays.sort(sortedP);
// 检查每个可能的子串
for (int i = 0; i <= s.length() - p.length(); i++) {
// 取出子串并排序
char[] window = s.substring(i, i + p.length()).toCharArray();
Arrays.sort(window);
// 比较排序后的字符串
if (Arrays.equals(window, sortedP)) {
result.add(i);
}
}
return result;
}
优化解法:滑动窗口 + 字符计数法
仔细思考会发现,我们其实不需要对字符串排序。只要两个字符串包含相同的字符,且每个字符出现的次数相同,它们就是异位词。这启发我们使用字符计数的方法。
结合滑动窗口技巧,我们可以在一次遍历中完成所有比较。
算法原理
- 使用数组记录p中每个字符的出现次数
- 维护一个大小等于p长度的滑动窗口
- 对窗口中的字符计数,与p的字符计数比较
- 当找到匹配时记录起始索引
算法步骤(伪代码)
- 初始化两个大小为26的数组,分别记录p和窗口中字符出现次数
- 先统计p中字符出现次数
- 使用滑动窗口遍历s:
- 右边加入新字符,更新计数
- 当窗口大小超过p长度时,左边移除字符,更新计数
- 比较两个计数数组是否相等
示例运行
让我们用s = “cbae”, p = "abc"模拟这个过程:
初始状态:
p的字符计数:[a:1, b:1, c:1]
窗口字符计数:[]
1. 添加'c':
窗口计数:[c:1]
2. 添加'b':
窗口计数:[b:1, c:1]
3. 添加'a':
窗口计数:[a:1, b:1, c:1]
比较:相等 ✓
记录索引0
4. 添加'e',移除'c':
窗口计数:[a:1, b:1, e:1]
比较:不等 ✗
Java代码实现
public List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
if (s.length() < p.length()) return result;
// 使用数组记录字符出现次数
int[] pCount = new int[26];
int[] windowCount = new int[26];
// 统计p中字符出现次数
for (char c : p.toCharArray()) {
pCount[c - 'a']++;
}
// 初始化窗口
for (int i = 0; i < p.length(); i++) {
windowCount[s.charAt(i) - 'a']++;
}
// 如果初始窗口就是异位词
if (Arrays.equals(pCount, windowCount)) {
result.add(0);
}
// 滑动窗口
for (int i = p.length(); i < s.length(); i++) {
// 移除窗口最左边的字符
windowCount[s.charAt(i - p.length()) - 'a']--;
// 添加新的字符
windowCount[s.charAt(i) - 'a']++;
// 比较是否是异位词
if (Arrays.equals(pCount, windowCount)) {
result.add(i - p.length() + 1);
}
}
return result;
}
解法比较
让我们比较这两种解法:
排序比较法:
- 时间复杂度:O(n·k·log k),其中k是p的长度
- 空间复杂度:O(k)
- 优点:直观易懂
- 缺点:需要频繁排序,效率较低
滑动窗口 + 字符计数法:
- 时间复杂度:O(n)
- 空间复杂度:O(1)(因为数组大小固定为26)
- 优点:一次遍历就能得到结果,无需排序
- 缺点:需要额外空间存储字符计数
题目模式总结
这道题体现了几个重要的算法思想:
- 滑动窗口:用于高效处理子串问题
- 计数统计:用计数替代排序来判断字符组成是否相同
- 空间换时间:使用额外空间来优化时间复杂度
这种解题模式在很多字符串问题中都有应用,比如:
- 无重复字符的最长子串
- 最小覆盖子串
- 字符串的排列
解决此类问题的通用思路是:
- 考虑能否用计数方法代替排序
- 思考是否可以用滑动窗口优化
- 注意窗口更新时的计数维护
- 考虑边界条件的处理
小结
通过这道题,我们不仅学会了如何找到字符串中的所有异位词,更重要的是掌握了字符统计和滑动窗口相结合的技巧。从排序比较到字符计数,我们看到了如何通过巧妙的思路来提升算法效率。
记住,在处理字符串相关的问题时,排序并不总是最好的选择。有时候,简单的计数可能会带来意想不到的效率提升!
作者:忍者算法
公众号:忍者算法
从找朋友到找变位词:一道趣味字符串问题的深入解析|LeetCode 438 找到字符串中所有字母异位词的更多相关文章
- [LeetCode]438. 找到字符串中所有字母异位词、76. 最小覆盖子串(滑动窗口解决子串问题系列)
题目438. 找到字符串中所有字母异位词 给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引. 说明: 字母异位词指字母相同,但排列不同的字符 ...
- [Swift]LeetCode438. 找到字符串中所有字母异位词 | Find All Anagrams in a String
Given a string s and a non-empty string p, find all the start indices of p's anagrams in s. Strings ...
- Q438 找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引. 字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100. 说明: ...
- *438. Find All Anagrams in a String 找到字符串中所有字母异位词
1. 原始题目 给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引. 字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 201 ...
- Leetcode438.Find All Anagrams in a String找到字符串中所有字母异位词
给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引. 字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100. 说明: ...
- Java实现 LeetCode 438 找到字符串中所有字母异位词
438. 找到字符串中所有字母异位词 给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引. 字符串只包含小写英文字母,并且字符串 s 和 p ...
- C#版 - Leetcode49 - 字母异位词分组 - 题解
C#版 - Leetcode49 - 字母异位词分组 - 题解 Leetcode49.Group Anagrams 在线提交: https://leetcode.com/problems/group- ...
- [Swift]LeetCode49. 字母异位词分组 | Group Anagrams
Given an array of strings, group anagrams together. Example: Input: ["eat", "tea" ...
- [Swift]LeetCode242. 有效的字母异位词 | Valid Anagram
Given two strings s and t , write a function to determine if t is an anagram of s. Example 1: Input: ...
- 有效的字母异位词的golang实现
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的一个字母异位词. 输入: s = "anagram", t = "nagaram" 输出: ...
随机推荐
- JVM中的Hello World是如何运行的?
每一个Java开发者都是通过Hello World敲开面向对象世界的大门.但是在一开始,我们考虑的只是这个语言是什么样的,我们如何更好的编码,却很少有人关心他内部是怎么运行的.看下面一个简单的hell ...
- 使用 LLVM 框架创建有效的编译器,第 2 部分
使用 clang 预处理 C/C++ 代码 无论您使用哪一种编程语言,LLVM 编译器基础架构都会提供一种强大的方法来优化您的应用程序.在这个两部分系列的第二篇文章中,了解在 LLVM 中测试代码,使 ...
- python下的多线程与多进程
多进程: 进程我们可以理解为是一个可以独立运行的程序单位,比如打开一个浏览器,这就开启了一个浏览器进程:打开一个文本编辑器,这就开启了一个文本编辑器进程.但一个进程中是可以同时处理很多事情的,比如在浏 ...
- PostgreSql Docker 主从热备,异步流复制方案
环境说明 Docker Windows 11 PostgreSql 16 方案步骤 0. 宿主机准备: 找个地方创建一个文件夹用来挂载容器中数据库Data文件夹,这里我用的是: C:\Users\Ad ...
- 前端17号学习(html完结)
一.路径 1.目录文件夹和跟目录 实际工作中需要创建一个文件夹来管理他们. 目录文件夹,就是普通文件夹,里面存放页面相关素材,如html文件.图片等. 根目录,打开目录文件夹的第一层就是根目录. 2. ...
- uniapp权限判断
写法如下 // 检查是否有写入外部存储的权限 function writeExternalStoragePermission() { return new Promise((resolve, reje ...
- opencv imshow 大图片显示,拖动显示
用imshow显示大图片,不能完整显示,不方便.结合网友的代码,修改成现在的代码,可以拖动图片,方便查看完整图片.而且方便调试,只需要用MatShow替换imshow. struct MousePar ...
- 二进制安装Kubernetes(k8s)v1.32.0
二进制安装Kubernetes(k8s)v1.32.0 介绍 https://github.com/cby-chen/Kubernetes 开源不易,帮忙点个star,谢谢了 kubernetes(k ...
- R机器学习:重复抽样在机器学习模型建立过程中的地位理解
在做机器学习项目的时候,一开始我们会将数据集分为训练集和测试集,要记住测试集只能用一次,只能用来评估最终最好的模型.如果你反复去使用测试集,反复测试后从里面挑最好的,你就是在耍流氓. 建模过程中肯定有 ...
- 关于Git上传项目报错error: RPC failed; HTTP 413 curl 22 The requested URL returned error: 413
问题 今天用Git上传项目时,最后一步push时命令行报错 error: RPC failed; HTTP 413 curl 22 The requested URL returned error: ...