题目一:无重复字符的最长子串
题目描述
给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。
注意: 子串是连续的字符序列。
示例 1
- 输入:
s = "abcabcbb" - 输出:
3 - 解释: 无重复字符的最长子串是
"abc",其长度为3。
示例 2
- 输入:
s = "bbbbb" - 输出:
1 - 解释: 无重复字符的最长子串是
"b",其长度为1。
示例 3
- 输入:
s = "pwwkew" - 输出:
3 - 解释: 无重复字符的最长子串是
"wke",其长度为3。
说明: 答案必须是子串的长度,"pwke"虽然是一个子序列,但不是子串。
解题思路
利用滑动窗口思想解决此题:
- 窗口定义:
使用两个指针start和end定义窗口的左右边界,初始时都指向字符串的开始位置。 - 状态维护:
使用一个字典(或哈希表)char_index记录每个字符上一次出现的位置。 - 窗口滑动:
遍历字符串:- 如果当前字符
s[end]在char_index中出现过且其索引不小于start,说明该字符在当前窗口内重复。此时,将start更新为char_index[s[end]] + 1,以缩小窗口,确保窗口内字符不重复。 - 更新
char_index[s[end]]为当前的索引end。 - 每一步都计算当前窗口的长度
end - start + 1,并更新最大值。
- 如果当前字符
- 时间复杂度:
该算法只遍历一次字符串,时间复杂度为 O(n)。
Python 实现
def length_of_longest_substring(s: str) -> int:
char_index = {} # 记录字符最后出现的位置
longest = 0
start = 0 # 窗口起始位置
for end, char in enumerate(s):
if char in char_index and char_index[char] >= start:
# 出现重复字符,移动窗口起始位置
start = char_index[char] + 1
char_index[char] = end
longest = max(longest, end - start + 1)
return longest
# 测试用例
if __name__ == "__main__":
test_cases = [
("abcabcbb", 3),
("bbbbb", 1),
("pwwkew", 3),
("", 0),
("au", 2)
]
for s, expected in test_cases:
result = length_of_longest_substring(s)
print(f"输入: \"{s}\", 预期: {expected}, 得到: {result}")
代码疑问
last_index = char_index.get(char, -1)
是 Python 字典的 get() 方法,它的作用是:
- 如果
char在char_index这个字典里,就返回char对应的值(即字符的上次索引)。 - 如果
char不在字典里,就返回-1(默认值-1是get()方法的第二个参数)
1. char_index 是空字典,get() 不会报错?
不会报错!
因为 get() 方法即使在字典里找不到 char,它也不会抛出错误,而是返回 -1(或者你设置的默认值)。
示例:
char_index = {} # 空字典
print(char_index.get('a', -1)) # 不会报错,返回 -1
相比于 char_index[char](如果 char 不在字典里会报错 KeyError),get() 更安全。
2. -1 是什么意思?
-1 代表字符 还没有出现过。
- 在
char_index字典里,存的是字符的最新索引,但如果char还没出现,就找不到索引。 - 我们用
-1作为默认值,表示“这个字符从未出现”。
例子:
s = "abcabcbb"
char_index = {} # 存字符的索引
for i, char in enumerate(s):
last_index = char_index.get(char, -1) # 获取字符的上次索引,默认值是 -1
print(f"字符: {char}, 之前索引: {last_index}")
char_index[char] = i # 更新最新索引
输出
字符: a, 之前索引: -1
字符: b, 之前索引: -1
字符: c, 之前索引: -1
字符: a, 之前索引: 0
字符: b, 之前索引: 1
字符: c, 之前索引: 2
字符: b, 之前索引: 4
字符: b, 之前索引: 6
a、b、c 第一次出现时,last_index 是 -1。
后续 a 再次出现时,它的 last_index 就是 0(上一次出现的位置)。
3. 为什么要用 -1 而不是 None?
-1这个数值可以直接进行数值比较(>= start),代码更简洁。- 不能直接比较,会报错:
if None >= 0: # TypeError
print("错误")
4. get() vs dict[key]
| 方法 | 键存在 | 键不存在 | 是否报错 |
|---|---|---|---|
dict[key] |
✅ 返回值 | ❌KeyError |
会报错 |
dict.get(key) |
✅ 返回值 | ✅ 返回 None |
不会报错 |
dict.get(key, 默认值) |
✅ 返回值 | ✅ 返回 默认值 |
不会报错 |
Java 实现
import java.util.HashMap;
import java.util.Map;
public class LongestSubstringWithoutRepeating {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> charIndex = new HashMap<>();
int longest = 0;
int start = 0; // 窗口起始位置
for (int end = 0; end < s.length(); end++) {
char ch = s.charAt(end);
if (charIndex.containsKey(ch) && charIndex.get(ch) >= start) {
// 遇到重复字符,更新窗口起始位置
start = charIndex.get(ch) + 1;
}
charIndex.put(ch, end);
longest = Math.max(longest, end - start + 1);
}
return longest;
}
public static void main(String[] args) {
LongestSubstringWithoutRepeating solver = new LongestSubstringWithoutRepeating();
String[] testCases = {"abcabcbb", "bbbbb", "pwwkew", "", "au"};
int[] expectedResults = {3, 1, 3, 0, 2};
for (int i = 0; i < testCases.length; i++) {
int result = solver.lengthOfLongestSubstring(testCases[i]);
System.out.println("输入: \"" + testCases[i] + "\", 预期: " + expectedResults[i] + ", 得到: " + result);
}
}
}
题目二:至多包含 K 个不同字符的最长子串
题目描述
给定一个字符串 s 和一个整数 k,请你找出其中包含最多 k 个不同字符的最长子串,并返回该子串的长度。
示例 1:
- 输入:
s = "eceba",k = 2 - 输出:
3 - 解释: 最长子串为
"ece",长度为 3。
示例 2:
- 输入:
s = "aa",k = 1 - 输出:
2 - 解释: 整个字符串
"aa"满足条件,长度为 2。
解题思路
- 定义滑动窗口:
使用两个指针left和right定义窗口的左右边界,初始时均指向字符串开头。 - 维护窗口状态:
使用一个字典char_freq记录窗口内各字符出现的次数,从而知道窗口中不同字符的个数。 - 扩展窗口:
不断将right指针向右移动,将新字符加入窗口,并更新其频率。 - 收缩窗口:
当窗口内不同字符的个数超过k时,移动left指针缩小窗口,直至窗口内不同字符数不超过k。 - 更新答案:
在每个窗口状态下,更新当前最长子串的长度。
Python 实现
def length_of_longest_substring_k_distinct(s: str, k: int) -> int:
if k == 0:
return 0 # 如果 k 为 0,则任何字符都不允许出现
char_freq = {} # 记录当前窗口内各字符的出现频率
max_length = 0
left = 0 # 窗口左边界
for right, char in enumerate(s):
# 将当前字符加入窗口,更新频率
char_freq[char] = char_freq.get(char, 0) + 1
# 当窗口内不同字符数超过 k 时,收缩窗口
while len(char_freq) > k:
# 移除窗口最左侧的字符
char_freq[s[left]] -= 1
if char_freq[s[left]] == 0:
del char_freq[s[left]]
left += 1
# 更新当前最长子串长度
max_length = max(max_length, right - left + 1)
return max_length
# 测试用例
if __name__ == "__main__":
test_cases = [
{"s": "eceba", "k": 2, "expected": 3},
{"s": "aa", "k": 1, "expected": 2},
{"s": "a", "k": 0, "expected": 0},
{"s": "abaccc", "k": 2, "expected": 4} # "accc" 或 "bacc" 均满足条件,长度为4
]
for case in test_cases:
result = length_of_longest_substring_k_distinct(case["s"], case["k"])
print(f"输入: s = \"{case['s']}\", k = {case['k']} -> 预期: {case['expected']}, 得到: {result}")
说明
- 滑动窗口思想:
通过维护一个不断扩展或收缩的窗口,保证窗口内的不同字符数不超过 k,从而高效计算满足条件的子串长度。 - 时间复杂度:
该算法仅需遍历字符串一次,时间复杂度为 O(n)。 - 字典的使用:
Python 中的dict就是基于哈希表实现的,用来快速记录和更新字符频率。
评论区