题目描述
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
提示:
s.length <= 40000
题目分析
动态规划解析:
状态定义: 设动态规划列表 dp ,dp[j] 代表以字符 s[j] 为结尾的 “最长不重复子字符串” 的长度。
转移方程: 固定右边界 j ,设字符 s[j] 左边距离最近的相同字符为 s[i] ,即 s[i] = s[j] 。
当 i < 0 ,即 s[j] 左边无相同字符,则 dp[j] = dp[j-1] + 1;
当 dp[j - 1] < j - i,说明字符 s[i] 在子字符串 dp[j-1] 区间之外 ,则 dp[j] = dp[j - 1] + 1;
当 dp[j−1] ≥ j−i ,说明字符 s[i] 在子字符串 dp[j-1] 区间之中 ,则 dp[j] 的左边界由 s[i] 决定,即 dp[j] = j - i;
当 i < 0时,由于 dp[j−1] ≤ j 恒成立,因而 dp[j−1] < j−i 恒成立,因此分支 1. 和 2. 可被合并。
空间复杂度优化:
由于返回值是取 dp 列表最大值,因此可借助变量 tmp 存储 dp[j] ,变量 res 每轮更新最大值即可。
此优化可节省 dp 列表使用的 O(N) 大小的额外空间。
方法一:动态规划 + 哈希表,掌握思想。
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> dic = new HashMap<>();
int res = 0, tmp = 0;
for(int j = 0; j < s.length(); j++) {
int i = dic.getOrDefault(s.charAt(j), -1); // 获取索引 i
dic.put(s.charAt(j), j); // 更新哈希表
tmp = tmp < j - i ? tmp + 1 : j - i; // dp[j - 1] -> dp[j]
res = Math.max(res, tmp); // max(dp[j - 1], dp[j])
}
return res;
}
}。
方法二:典型的滑动窗口,双指针 + HashSet去重,与方法一的区别在于,因为是从左到右判断,在left指针移动的同时,已经判断了 j - i 与 dp[j-1] 的大小,推荐。
class Solution {
public int lengthOfLongestSubstring(String s) {
Set<Character> set = new HashSet<>();
int left = 0, right = 0, res = 0;
while(right < s.length()){
char c = s.charAt(right++);
//存在重复的字符,则移动左指针,直到滑动窗口中不含有该字符
while(set.contains(c)){
set.remove(s.charAt(left++));
}
set.add(c);
res = Math.max(res, right-left);
}
return res;
}
}