这道题目要求我们找出给定字符串中最长的无重复字符的子串长度。乍看简单,实则暗藏多个考察点。让我们先拆解题目中的关键术语:
子串 vs 子序列:子串必须是字符串中连续的字符组合,而子序列只需保持相对顺序即可。例如在"pwwkew"中,"wke"是子串,而"pwke"只是子序列。
无重复字符:意味着子串中所有字符必须唯一,不能有任何重复。例如"abc"符合要求,"abca"则不符合。
最长长度:需要遍历所有可能的无重复子串,找出其中长度最大的那个。
在实际面试中,这道题常被用来考察候选人对滑动窗口算法的掌握程度。滑动窗口是处理数组/字符串子区间问题的利器,能够将暴力解法O(n²)的时间复杂度优化到O(n)。
最直观的解法是检查所有可能的子串:
java复制for (int i = 0; i < n; i++)
for (int j = i + 1; j <= n; j++)
if (allUnique(s, i, j)) maxLen = Math.max(maxLen, j - i);
这种方法需要O(n³)的时间复杂度(allUnique检查需要O(n)),显然不适用于长字符串。
滑动窗口通过维护一个可变大小的窗口来避免重复计算。基本思路是:
这种方法的精妙之处在于它确保了每个字符最多被访问两次(一次由右指针,一次由左指针),因此时间复杂度是O(2n) = O(n)。
为了快速判断字符是否重复,我们使用HashMap存储字符及其最新位置。当右指针遇到重复字符时,可以直接获取该字符上次出现的位置,将左指针移动到max(left, 上次位置+1)。
注意:左指针不能回退,因此需要取max(left, map.get(c) + 1)。例如"abba"的情况,当第二个b使左指针跳到2后,遇到第二个a时不能跳回1。
让我们深入分析给出的Java实现:
java复制public int lengthOfLongestSubstring(String s) {
int maxLen = 0;
HashMap<Character, Integer> map = new HashMap<>();
for(int left=0, right=0; right<s.length(); right++){
char temp = s.charAt(right);
if(map.containsKey(temp)){
left = Math.max(left, map.get(temp));
}
maxLen = Math.max(maxLen, right - left + 1);
map.put(s.charAt(right), right+1);
}
return maxLen;
}
关键点解析:
为什么存储right+1?
这样当遇到重复字符时,left可以直接跳到map.get(c)而不用+1,代码更简洁。不过原代码中left = Math.max(left, map.get(temp))实际上使用的是之前存储的right+1值。
当字符集已知且较小时(如ASCII),可以用数组代替HashMap:
java复制int[] index = new int[128]; // ASCII字符集
Arrays.fill(index, -1);
// 使用时:index[c] = right;
这样查找时间依然是O(1),但空间效率更高。
如果需要输出子串而非长度,只需记录最大长度时的左右指针:
java复制if (right - left + 1 > maxLen) {
maxLen = right - left + 1;
start = left;
}
// 最后返回s.substring(start, start + maxLen);
LeetCode 424题是允许替换k个字符的最长子串,可以用类似的滑动窗口思路,加入一个计数器来跟踪当前窗口中的主要字符。
左指针回退:
java复制left = map.get(c) + 1; // 错误!应该取max
长度计算时机错误:
在更新map前计算长度会导致窗口大小少算一位
初始值设置不当:
maxLen初始化为1会错过空字符串情况
可视化窗口变化:
打印每次循环后的窗口状态:
java复制System.out.println(s.substring(left, right+1));
检查指针移动:
记录left和right的移动轨迹,确保left不会回退
边界测试:
特别测试空串、全同字符、无重复字符等情况
这种算法思想在多个实际场景中有应用:
理解滑动窗口的思想,能帮助我们解决一大类子数组/子字符串问题,如:
在多次实现这个算法的过程中,我总结了几个关键点:
对于算法初学者,我建议先用暴力解法实现,理解问题本质后再优化。在纸上画出窗口移动的过程,比直接写代码更有效。