题目描述
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。
示例 3:
输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
其实这一题的解释在官方题解里面已经写得比较清楚了,现在只是用自己的话二次解释一遍。
思路
方法一:暴力破解
先编写一个函数boolean allUnique(String substring)
,如果substring
中的字符都是唯一的,则返回true
;否则返回false
。一个比较方便的实现方法是利用HashSet
,遍历这个子字符串中的每一个字符,HashSet
中没有当前字符就加入,有的话就返回false
。
public boolean allUnique(String s, int start, int end) {
Set<Character> set = new HashSet<>();
for (int i = start; i < end; i++) {
Character ch = s.charAt(i);
if (set.contains(ch)) return false;
set.add(ch);
}
return true;
}
再利用这个函数,根据substring
的长度不同,遍历给定字符串s
,设s
的长度为length
。
substring
长度为 1 时,遍历次数为 lengthsubstring
长度为 2 时,遍历次数为 length - 1substring
长度为 3 时,遍历次数为 length - 2- ……
substring
长度为 length 时,遍历次数为 1
这样算下来,暴力破解的过程中,要调用函数boolean allUnique(String s, int start, int end)
的次数为n(n-1)/2
。再考虑到boolean allUnique(String s, int start, int end)
每一次调用,都会访问给定字符串n
次。所以时间复杂度为O(N^3)
。
源代码
public class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int ans = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j <= n; j++)
if (allUnique(s, i, j)) ans = Math.max(ans, j - i);
return ans;
}
public boolean allUnique(String s, int start, int end) {
Set<Character> set = new HashSet<>();
for (int i = start; i < end; i++) {
Character ch = s.charAt(i);
if (set.contains(ch)) return false;
set.add(ch);
}
return true;
}
}
方法二:滑动窗法
滑动窗口是数组 / 字符串问题中常用的抽象概念。 窗口通常是在数组 / 字符串中由开始和结束索引定义的一系列元素的集合,即 [i, j)(左闭,右开)。而滑动窗口是可以将两个边界向某一方向 “滑动” 的窗口。例如,我们将 [i, j) 向右滑动 1 个元素,则它将变为 [i+1, j+1)(左闭,右开)。
使用一个HashSet
来实现滑动窗,用来检查重复字符。向右侧滑动索引j
,遇到不重复的字符就把它放进窗里,更新长度;遇到重复的字符,向右侧滑动索引i
。重复以上操作直到遍历整个字符串。
源代码
public int lengthOfLongestSubstring (String s) {
if (s.length() == 0) {
return 0;
}
Set<Character> set = new HashSet<>();
int length = s.length();
int ans = 0, i = 0, j = 0;
while (i < length && j < length) {
if (!set.contains(s.charAt(j))) {
set.add(s.charAt(j++));
ans = Math.max(ans, j - i);
} else {
set.remove(s.charAt(i++));
}
}
return ans;
}
在最坏情况下,每个字符被i
和j
访问了两次。
其实我觉得滑动窗有点类似于双指针的做法,都是维护两个指针,两个指针的移动速率不同,对一个线性集合操作,最后达到我们想要的结果。
方法三:优化滑动窗
除了使用HashSet
判断重复字符,我们还可以建立字符到索引的映射,即可以使用HashMap
判断重复字符。碰到重复字符,不需要一个个移动i
,直接将i
变为j+1
。
源代码
public int lengthOfLongestSubstring(String s) {
int n = s.length(), ans = 0;
Map<Character, Integer> map = new HashMap<>(); // current index of character
// try to extend the range [i, j]
for (int j = 0, i = 0; j < n; j++) {
if (map.containsKey(s.charAt(j))) {
i = Math.max(map.get(s.charAt(j)), i);
}
ans = Math.max(ans, j - i + 1);
map.put(s.charAt(j), j + 1);
}
return ans;
}
因为我们知道给定字符串的字符种类比较少,只有字母和数字,字符集比较小(都是 ASCII 码),所以可以进一步优化,使用整数数组替代HashMap
,用来建立字符和索引的映射。
public int lengthOfLongestSubstring(String s) {
int n = s.length(), ans = 0;
int[] index = new int[128]; // current index of character
// try to extend the range [i, j]
for (int j = 0, i = 0; j < n; j++) {
i = Math.max(index[s.charAt(j)], i);
ans = Math.max(ans, j - i + 1);
index[s.charAt(j)] = j + 1;
}
return ans;
}