这道题目要求我们从一个字符串中找出最长的不包含重复字符的子字符串,并返回其长度。这是一个经典的字符串处理问题,在算法面试中经常出现。我们先来看几个示例:
这个问题本质上是在寻找字符串中满足特定条件(无重复字符)的最长子串。这类子串问题通常可以考虑以下几种解法:
最直观的解法是暴力枚举所有可能的子串,然后检查每个子串是否包含重复字符。具体实现如下:
java复制public int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) return 0;
int n = s.length();
int maxLen = 0;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
if (isUnique(s, i, j)) {
maxLen = Math.max(maxLen, j - i + 1);
} else {
break;
}
}
}
return maxLen;
}
private boolean isUnique(String s, int left, int right) {
Set<Character> set = new HashSet<>();
for (int i = left; i <= right; i++) {
char c = s.charAt(i);
if (set.contains(c)) return false;
set.add(c);
}
return true;
}
这种方法的时间复杂度是O(n³),对于较长的字符串效率很低,在实际应用中不推荐使用。
滑动窗口是一种更高效的解法,它通过维护一个窗口来动态调整子串范围。基本思路是:
java复制public int lengthOfLongestSubstring(String s) {
Set<Character> window = new HashSet<>();
int left = 0, right = 0;
int maxLen = 0;
while (right < s.length()) {
char c = s.charAt(right);
if (!window.contains(c)) {
window.add(c);
right++;
maxLen = Math.max(maxLen, right - left);
} else {
window.remove(s.charAt(left));
left++;
}
}
return maxLen;
}
这种解法的时间复杂度是O(2n)=O(n),因为每个字符最多被访问两次(被left和right各访问一次)。
基本滑动窗口解法在遇到重复字符时,左指针只能一步一步向右移动,效率可以进一步优化。我们可以记录每个字符最后出现的位置,当遇到重复字符时,直接将左指针跳到重复字符的下一个位置。
java复制public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> charIndex = new HashMap<>();
int left = 0, maxLen = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (charIndex.containsKey(c) && charIndex.get(c) >= left) {
left = charIndex.get(c) + 1;
}
charIndex.put(c, right);
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
这种优化将时间复杂度降低到严格的O(n),因为每个字符只被访问一次。
考虑到ASCII字符只有128个,我们可以使用固定大小的数组代替哈希表,进一步优化空间效率。
java复制public int lengthOfLongestSubstring(String s) {
int[] lastIndex = new int[128];
Arrays.fill(lastIndex, -1);
int left = 0, maxLen = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (lastIndex[c] >= left) {
left = lastIndex[c] + 1;
}
lastIndex[c] = right;
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}
这种实现的空间复杂度是O(1)(固定128大小的数组),时间复杂度仍然是O(n),是最优的解法。
| 解法类型 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力解法 | O(n³) | O(min(n,128)) | 不推荐使用 |
| 基本滑动窗口 | O(2n) | O(min(n,128)) | 通用解法 |
| 优化滑动窗口 | O(n) | O(min(n,128)) | 推荐解法 |
| 数组优化 | O(n) | O(1) | 最优解法 |
java复制@Test
public void testSolution() {
Solution solution = new Solution();
assertEquals(0, solution.lengthOfLongestSubstring(""));
assertEquals(1, solution.lengthOfLongestSubstring("b"));
assertEquals(3, solution.lengthOfLongestSubstring("abcabcbb"));
assertEquals(1, solution.lengthOfLongestSubstring("bbbbb"));
assertEquals(3, solution.lengthOfLongestSubstring("pwwkew"));
assertEquals(4, solution.lengthOfLongestSubstring("abcd"));
assertEquals(3, solution.lengthOfLongestSubstring("abba"));
assertEquals(5, solution.lengthOfLongestSubstring("tmmzuxt"));
}
滑动窗口适合解决这类"满足某条件的最长子串"问题,因为它可以动态调整窗口大小,只遍历一次字符串就能找到解,效率很高。
对于Unicode字符,可以使用HashMap代替数组,或者使用更大的数组(但要注意内存消耗)。
数组的访问时间是O(1)且常数因子更小,没有哈希冲突问题,对于小范围字符集(如ASCII)效率更高。
在实际编码中,我发现以下几点特别重要:
这个问题的优化过程也很有启发性:从暴力解法到基本滑动窗口,再到利用哈希表优化,最后用数组进一步优化,每一步都使算法更高效。这种逐步优化的思路在解决其他算法问题时也同样适用。