1. 项目概述
今天要分享的是LeetCode上两道经典题目:"无重复字符的最长子串"和"赎金信"。这两道题分别来自LeetCode热题100和日常练习题库,是面试中经常被问到的题目类型。作为算法练习者,掌握这两道题的解法不仅能提升编程能力,更能深入理解滑动窗口和计数数组这两个重要算法技巧。
我在刷题过程中发现,很多初学者对这两道题的理解往往停留在表面,只是机械记忆解法而不知其所以然。本文将带大家从问题本质出发,通过详细的原理解析和多种解法对比,真正掌握这两类问题的解决思路。
2. 无重复字符的最长子串解析
2.1 问题描述与示例
题目要求给定一个字符串s,找出其中不含有重复字符的最长子串的长度。例如:
- 输入:"abcabcbb",输出:3("abc")
- 输入:"bbbbb",输出:1("b")
- 输入:"pwwkew",输出:3("wke")
这道题的关键在于"无重复字符"和"最长子串"两个条件。子串必须是连续的,且所有字符都不重复。
2.2 暴力解法分析
最直观的解法是暴力枚举所有可能的子串,然后检查是否有重复字符。这种方法时间复杂度为O(n^3),显然效率太低,无法通过LeetCode的测试用例。
python复制def lengthOfLongestSubstring(s: str) -> int:
n = len(s)
res = 0
for i in range(n):
for j in range(i, n):
if len(set(s[i:j+1])) == j - i + 1:
res = max(res, j - i + 1)
return res
2.3 滑动窗口优化
滑动窗口是解决这类子串问题的经典方法。其核心思想是维护一个窗口,通过调整窗口的左右边界来寻找符合条件的子串。
具体实现步骤:
- 使用哈希集合记录当前窗口中的字符
- 初始化左右指针left和right都指向字符串开头
- 右指针不断右移,将新字符加入集合
- 当遇到重复字符时,左指针右移直到消除重复
- 在整个过程中记录窗口的最大长度
python复制def lengthOfLongestSubstring(s: str) -> int:
char_set = set()
left = 0
res = 0
for right in range(len(s)):
while s[right] in char_set:
char_set.remove(s[left])
left += 1
char_set.add(s[right])
res = max(res, right - left + 1)
return res
时间复杂度优化到O(n),因为每个字符最多被访问两次(被右指针和左指针各访问一次)。
2.4 哈希表优化版本
我们可以进一步优化,使用哈希表记录字符最后一次出现的位置,这样当遇到重复字符时,可以直接将左指针跳到该字符上次出现位置的下一位。
python复制def lengthOfLongestSubstring(s: str) -> int:
last_seen = {}
left = 0
res = 0
for right, char in enumerate(s):
if char in last_seen and last_seen[char] >= left:
left = last_seen[char] + 1
last_seen[char] = right
res = max(res, right - left + 1)
return res
这种实现更加高效,特别是在处理长字符串时表现更好。
3. 赎金信问题解析
3.1 问题描述与示例
赎金信问题要求判断第一个字符串ransom能不能由第二个字符串magazine里面的字符构成。magazine中的每个字符只能在ransom中使用一次。
示例:
- 输入:ransomNote = "a", magazine = "b",输出:false
- 输入:ransomNote = "aa", magazine = "ab",输出:false
- 输入:ransomNote = "aa", magazine = "aab",输出:true
3.2 哈希表解法
最直接的思路是统计magazine中每个字符的出现次数,然后检查ransomNote中的字符是否都能被满足。
python复制def canConstruct(ransomNote: str, magazine: str) -> bool:
from collections import defaultdict
mag_counts = defaultdict(int)
for c in magazine:
mag_counts[c] += 1
for c in ransomNote:
mag_counts[c] -= 1
if mag_counts[c] < 0:
return False
return True
3.3 数组优化版本
由于题目说明只包含小写字母,我们可以使用固定大小的数组来代替哈希表,进一步优化空间效率。
python复制def canConstruct(ransomNote: str, magazine: str) -> bool:
counts = [0] * 26
for c in magazine:
counts[ord(c) - ord('a')] += 1
for c in ransomNote:
index = ord(c) - ord('a')
counts[index] -= 1
if counts[index] < 0:
return False
return True
这种方法时间复杂度为O(m+n),空间复杂度为O(1)(固定大小的数组)。
4. 滑动窗口的深入理解
4.1 滑动窗口的适用场景
滑动窗口技术特别适合解决数组/字符串的子区间问题,尤其是需要满足某些条件的连续子序列问题。常见应用场景包括:
- 寻找满足条件的最长子串/子数组
- 寻找满足条件的最短子串/子数组
- 计算满足条件的子串/子数组的数量
4.2 滑动窗口的模板代码
虽然不同问题的具体实现会有所不同,但滑动窗口通常遵循以下模板:
python复制def slidingWindow(s: str):
window = {} # 存储窗口内数据的哈希表
left = right = 0 # 窗口左右边界
while right < len(s):
# 扩大窗口
c = s[right]
right += 1
# 更新窗口数据
# 判断是否需要收缩窗口
while (window needs shrink):
# 缩小窗口
d = s[left]
left += 1
# 更新窗口数据
4.3 滑动窗口的变种
在实际问题中,滑动窗口可能有多种变种:
- 固定大小的窗口:窗口大小不变,滑动时整体移动
- 可变大小的窗口:窗口大小根据条件动态调整
- 多指针窗口:使用多个指针来维护复杂条件
5. 计数数组的深入理解
5.1 计数数组的适用场景
计数数组是处理有限范围计数问题的有效工具,特别适用于:
- 字符频率统计(如只包含小写字母的字符串)
- 有限范围内的数字统计
- 需要快速查询元素出现次数的场景
5.2 计数数组与哈希表的对比
| 特性 | 计数数组 | 哈希表 |
|---|---|---|
| 空间复杂度 | O(k),k为范围大小 | O(n),n为元素数量 |
| 时间复杂度 | O(1)访问 | 平均O(1)访问 |
| 适用场景 | 元素范围已知且有限 | 元素范围未知或很大 |
| 实现难度 | 简单 | 相对复杂 |
5.3 计数数组的扩展应用
计数数组不仅可以用于简单的频率统计,还可以用于:
- 排序(计数排序)
- 前缀和计算
- 差异统计(如两个字符串的字符差异)
6. 常见问题与调试技巧
6.1 无重复字符最长子串的常见错误
- 窗口收缩不彻底:遇到重复字符时,需要一直收缩窗口直到消除重复
- 初始条件处理不当:空字符串输入需要特殊处理
- 更新最大长度的时机:应该在每次扩展窗口后立即更新
6.2 赎金信问题的常见错误
- 大小写敏感:题目通常说明是否区分大小写
- 字符范围假设:明确题目对字符范围的限制
- 过早返回:需要完整遍历ransomNote才能确定结果
6.3 调试技巧
- 打印窗口状态:在滑动窗口问题中,打印left、right和当前窗口内容
- 可视化计数数组:打印计数数组的内容帮助理解
- 边界测试:测试空字符串、单字符字符串等边界情况
7. 性能优化与进阶思考
7.1 无重复字符最长子串的优化
对于某些特殊场景,可以进一步优化:
- 如果知道字符集很小,可以使用固定大小的数组代替哈希表
- 如果字符串很长,可以考虑并行处理不同区段
7.2 赎金信问题的变种
实际问题可能会有变种要求:
- 不区分大小写
- 支持Unicode字符
- 需要返回具体使用了哪些字符
7.3 相关题目推荐
为了巩固这两个技巧,推荐练习以下题目:
- 滑动窗口:最小覆盖子串、找到字符串中所有字母异位词
- 计数数组:有效的字母异位词、字符串的排列
在实际面试中,这两类问题经常会被问到,理解其本质并能够灵活应用非常重要。我在准备面试时,会刻意练习这些问题的多种解法,并思考它们之间的共性和差异,这样在遇到新问题时能够快速识别适用的解题模式。
