leetcode
leetcode 1 ~ 50
无重复字符的最长子串

无重复字符的最长子串

难度:

标签:

题目描述

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

 

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

 

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成

代码结果

运行时间: 76 ms, 内存: 17 MB


// Problem: Find the length of the longest substring without repeating characters
// Approach: Java Streams are not the best fit for this problem due to its stateful nature, but we can simulate the sliding window approach using Streams in combination with additional state tracking.
// This solution is provided for completeness but is less efficient than using traditional loops.
 
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
 
public class StreamSolution {
    public int lengthOfLongestSubstring(String s) {
        Map<Character, Integer> map = new HashMap<>();
        BiFunction<Character, Integer, Integer> updateMap = (c, index) -> {
            map.put(c, index);
            return index;
        };
        int[] result = new int[1];
        IntStream.range(0, s.length())
            .boxed()
            .collect(Collectors.toMap(i -> s.charAt(i), Function.identity(), (a, b) -> b, () -> map))
            .forEach((c, i) -> {
                if (map.get(c) != null && map.get(c) >= result[0]) {
                    result[0] = map.get(c) + 1;
                }
                map.put(c, i);
                result[0] = Math.max(result[0], i - result[0] + 1);
            });
        return result[0];
    }
}
 

解释

方法:

这个题解使用了滑动窗口的思路。用两个指针 l 和 r 表示滑动窗口的左右边界,用一个哈希表 memo 记录窗口内每个字符出现的次数。每次移动右指针 r,将新字符加入窗口,并更新哈希表。如果发现哈希表中新加入字符的次数大于1,说明有重复字符,则移动左指针 l 缩小窗口,并更新哈希表,直到窗口内没有重复字符为止。在移动过程中,用一个变量 ret 来记录最长无重复字符子串的长度。

时间复杂度:

O(n)

空间复杂度:

O(min(m, n))

代码细节讲解

🦆
为什么在更新最长无重复字符子串的长度时,使用`max(ret, r - l)`而不是其他方式?
在该算法中,`r - l` 计算的是当前无重复字符的子串长度。由于在每次循环中子串的长度可能增加(当右指针向右移动并添加新字符时)或减少(当左指针向右移动以排除重复字符时),因此我们需要不断地更新记录的最大长度。使用 `max(ret, r - l)` 确保了每次都将最大长度记录下来,而不是仅仅记录最后一个窗口的大小。这是因为最长的无重复子串可能出现在任何位置,并不一定是最后一个窗口。
🦆
哈希表`memo`的作用是什么?为什么要记录每个字符出现的次数?
哈希表 `memo` 用于实时记录滑动窗口内每个字符的出现次数。这样做的目的是快速检测窗口中是否有重复字符。当右指针 `r` 向右移动并添加一个新的字符到窗口时,我们需要检查该字符是否已经存在于窗口中。如果有,则需要通过移动左指针 `l` 来排除重复,直到窗口中该字符的数量减少到1或0。记录每个字符的出现次数使得这个过程变得快速和简单,避免了每次都需要遍历整个窗口来查找重复字符。
🦆
当发现新加入的字符导致重复时,为什么只需要移动左指针`l`而不需要同时调整右指针`r`?
当右指针 `r` 添加一个新的字符后出现重复时,说明要解决这个重复问题,需要从窗口的左边开始排除字符,因为滑动窗口是从左往右扩展的。移动左指针 `l` 是为了缩小窗口,直到窗口中不再有重复字符。右指针 `r` 保持不动是因为我们不需要回退其进展——我们已经知道在 `r` 之前的位置直到 `l` 中没有重复,所以只需继续从 `r` 向右扩展即可。这样可以有效地通过单次遍历解决问题,保持时间复杂度控制在线性级别。
🦆
在内层while循环中,为什么要检查`if d in memo`再进行`memo[d] -= 1`操作?
这个检查是为了确保代码的健壮性和正确性。虽然在当前的上下文中,`d`(左指针 `l` 指向的字符)总是存在于哈希表 `memo` 中,因为它是之前加入到窗口的。但在更复杂的修改或不同的实现中,可能会出现 `d` 不在 `memo` 中的情形,特别是如果代码被修改以处理更复杂的场景或在不同的算法结构中重用。这种检查可以避免在尝试减少一个不存在的键的值时抛出错误,从而使函数更加安全和健壮。

相关问题

至多包含两个不同字符的最长子串

至多包含两个不同字符的最长子串

至多包含 K 个不同字符的最长子串

至多包含 K 个不同字符的最长子串

K 个不同整数的子数组

给定一个正整数数组 nums和一个整数 k,返回 nums 中 「好子数组」 的数目。

如果 nums 的某个子数组中不同整数的个数恰好为 k,则称 nums 的这个连续、不一定不同的子数组为 好子数组 」

  • 例如,[1,2,3,1,2] 中有 3 个不同的整数:12,以及 3

子数组 是数组的 连续 部分。

 

示例 1:

输入:nums = [1,2,1,2,3], k = 2
输出:7
解释:恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2].

示例 2:

输入:nums = [1,2,1,3,4], k = 3
输出:3
解释:恰好由 3 个不同整数组成的子数组:[1,2,1,3], [2,1,3], [1,3,4].

 

提示:

  • 1 <= nums.length <= 2 * 104
  • 1 <= nums[i], k <= nums.length