最长不含重复字符的子字符串
1、参考资料
https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/
2、题目要求
题目描述
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
3、代码思路
动态规划
-
为什么要使用动态规划?我们想要求
str(0, i)
字符串中最长不含重复字符的子串,可以这样考虑:假设我们已经求出了str(0, i-1)
中的最长子串长度,现在考虑上str(i)
字符,看看加上str(i)
之后,str(0, i)
字符串的最长子串长度是多少 -
于是我们定义
dp[i]
的含义为:以str[i]
结尾的最长不重复子串的长度。我们遍历字符串中每一个字符:for (int i = 1; i < str.length(); i++) {
,并以str[i]
作为右边界,往str[i]
左边寻找与str[i]
相同并且与str[i]
隔得最近的字符 -
由于
dp[i-1]
中存储的是以str[i-1]
结尾的最长不重复子串的长度,则证明以str[i-1]
结尾的最长不重复子串为str(i-dp[i-1], i-1)
,右边界为str[i]
,左边界为i-dp[i-1]
,因为再往左走一步的字符就会与str[i-1]
重复(或者数组越界),所以我们需要在str(i-dp[i-1], i-1)
中找到与str[i]
相同并且距离str[i]
最近的字符 -
我们使用
for (j = i - 1; j >= i - dp[i - 1]; j--) {
遍历str(i-dp[i-1], i-1)
中的所有字符,如果发现str[j]
与str[i]
相同,则跳出循环,此时j
指向str[j]
;如果执行完循环,还没有找到与str[i]
相同的字符,此时j
指向左边界的前一个字符,该字符和str[i-1]
重复(或者j=-1
),所以i-j
即为以str[i]
结尾的最长不重复子串的长度 -
当求出
dp[]
数组之后,我们就知道了以每个字符结尾的最长子串长度,再在其中选出最大值即可,这个操作我在求dp[]
数组的时候就一并做了,不免得又要去遍历一次dp[]
数组
时间复杂度为 O(n2)
4、代码实现
代码
class Solution {
public int lengthOfLongestSubstring(String str) {
// Guard safe
if (str == null || str.length() == 0) {
return 0;
}
int[] dp = new int[str.length()];
dp[0] = 1; // 单个字符的最长不重复子串长度为 1
int maxLength = 1; // 默认最大值为 1
// 遍历数组中每个字符
for (int i = 1; i < str.length(); i++) {
int j;
// 以 str[i-1] 为右边界,以 str[i - dp[i - 1]] 为左边界
// 寻找与 str[i] 相同并且距离 str[i] 最近的字符
for (j = i - 1; j >= i - dp[i - 1]; j--) {
if (str.charAt(i) == str.charAt(j)) {
break;
}
}
// 以 str[i] 结尾的最长不重复子串的长度
dp[i] = i - j;
// 整个字符串的长不重复子串的长度
maxLength = Math.max(maxLength, dp[i]);
}
return maxLength;
}
}
5、代码优化
优化思路
使用 map
代替内层 for
循环
- 在上述代码的内层循环:
for (j = i - 1; j >= i - dp[i - 1]; j--) {
中,不就是个判重操作,然后记录重复字符在字符串中的索引吗?我们可以使用HashMap
来代替内层for
循环,key
为字符,value
为该字符最近一次在字符串中出现的位置 - 从上面的代码可以看出,
dp[i]
只与dp[i-1]
有关系,所以我们可以将dp[]
数组优化为一个一维的变量:lastLength
,lastLength
代表dp[i-1]
,即以str[i-1]
结尾的最长不重复字串的长度,进行下一次循环(i+1
)的时候,将dp[i]
的值赋值给lastLength
如何利用 map
进行动态规划
- 遍历当前字符
str[i]
时,先将看看该字符有没有在map
中出现过:Integer index = map.get(str.charAt(i));
- 如果
index == null
,则说明str[i]
根本就没有出现过,以str[i]
结尾的最长子串长度 = 以str[i-1]
结尾的最长子串长度 +1
; - 如果
index != null
,则说明str[i]
在字符串中出现过,并且是最近一次出现的位置,这里又需要分为两种情况:- 因为以
str[i-1]
结尾的最长子串为:str(i-lastLength, i-1)
,所以如果index
没有落在这个区间内,则说明在str(i-lastLength, i-1)
没有str[i]
的重复字符,此时只需要将str[i]
排在str(i-lastLength, i-1)
的后面即可,所以最长子串长度为lastLength+1
- 但是如果
index
落在str(i-lastLength, i-1)
区间内,则说明此区间内有str[i]
的重复字符,index
指向与str[i]
重复的字符,所以最长子串长度为i-index
- 所以我们需要取两者之间的小值
- 因为以
代码实现
class Solution {
public int lengthOfLongestSubstring(String str) {
// Guard safe
if (str == null || str.length() == 0) {
return 0;
}
// key 是字符,value 是该字符上一次在字符串中出现的位置
Map<Character, Integer> map = new HashMap<>();
int maxLength = 0;
int lastLength = 0;
// 遍历字符串中每个字符
for (int i = 0; i < str.length(); i++) {
// 拿到之前的索引值
Integer index = map.get(str.charAt(i));
// 更新当前的索引值
map.put(str.charAt(i), i);
if (index == null) {
// 如果 index == null,证明该字符从未出现过,
lastLength++;
} else {
// 否则 index 表示该字符最近一次在字符串中出现的位置
// 如果 index 没有落在 str(i-lastLength, i-1) 区间内,最长子串长度为 lastLength+1
// 如果 index 落在 str(i-lastLength, i-1) 区间内,最长子串长度为 i - index
lastLength = Math.min(lastLength + 1, (i - index));
}
// 更新字符串最长不重复字串的最大值
maxLength = Math.max(lastLength, maxLength);
}
return maxLength;
}
}