题目描述
给定一个字符串,请你找出其中不含重复字符的最长子串的长度。
示例1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字串的最长子串是“abc”,所以其长度为 3.
示例2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是“b”,所以其长度为1.
示例3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是“wke”,所以其长度为3.
说明:
请注意,你的答案必须是子串的长度,示例3中,"pwke"使用一个子序列,而不是子串
思路分析
我们通过题意可以理解是寻找无重复字符的最长子串,返回其长度。这里我们讲问题进行拆解:
- 无重复字符
- 最长子串
对于问题1 无重复字符,需要无重复字符,这里我们为寻求较好的时间复杂度不采取对每一个字符c
在某范围内都进行搜索判重的方法。这里我们借助辅助容器来达到记录历史的目的,进而当遍历到后面的字符时,直接在辅助容器中查询是否包含即可。
这一题让我直接联想到得是计算机网络中TCP的滑动窗口,我们通过界定窗口的左边界和右边界来判断需要的数据区域,这里即为包括left
不包括right
的字符串均无重复字符。这里我们通过图示来说明:
图1
我们通过两个下标来确定滑动窗口的范围left, right
,且为左闭右开;我们借助集合set
来存储遍历历史,可知我们每个元素均为唯一。
- 当
s.charAt(right)
没有在set
中时,先添加进set
,且right++
,即滑动窗口右扩展,重复步骤1; - 若
set
已经包括了s.charAt(right)
,则此时一定是left
下标的字符为重复字符,滑动窗口左收缩,且更新left
为left++
;
接下来再看问题2 最长子串,我们这里需要返回的是这个最长子串的长度,即一个整型数据,我们这里可采用直接对比的方式,每次都更新为比较值得最大值即可:
maxSubStrLen = Math.max(maxSubStrLen , currLen)
。
此时可以得知解题步骤为:
核心操作
- 当
s.charAt(right)
没有在set
中时,先添加进set
,且right++
,即滑动窗口右扩展,重复步骤1, 更新maxSubStrLen
; - 若
set
已经包括了s.charAt(right)
,则此时一定是left
下标的字符为重复字符,滑动窗口左收缩,且更新left
为left++
;
解题代码1
public static int solution1(String s){
if(s == null) return 0;
int len = s.length();
if(len == 1) return 1;
/* Step 1: Init. integer and container.
*/
int maxSubStrLen = 0, left = 0, right = 0;
Set<Character> set = new HashSet<>();
/* Step 2: go-through string
and
check whether repeated in container to re-fine sliding window
*/
while(left < len && right < len){
if(!set.contains(s.charAt(right))){ // enlarge window on the right
set.add(s.charAt(right++));
maxSubStrLen = Math.max(maxSubStrLen, right-left);
}else{ // shrink window from left
set.remove(s.charAt(left++));
}
}
return maxSubStrLen;
}
复杂度分析
时间复杂度: 最坏情况下,需要对每个字符都访问两边,2N
,故时间复杂度为O(N)
;
空间复杂度: 我们借助了辅助容器,且考虑字符集大小M
的情况下,空间复杂度为O(min(M, N))
.
进阶思考
在solution1 中最坏情况(均为同一个字符),我们对每个字符都需要访问两遍,能否存在一种方法使得访问字符的次数减少。我们已知字符的可取范围是有限的,这里假定为M
,则不妨在right
界定右边界的情况下,判断并更新滑动窗口内的有且唯一的字符的下标,更新maxSubStrLen
。
在这里我们依然需要借助辅助容器,且还需要记录字符的下标,则可选取辅助容器为哈希表HashMap<char, index>
, key
为浏览过的字符,value
为滑动窗口内(此时包含right
在内)的相应字符的最新下标。
则可总结为下:
核心操作
- 当
map
中包含s.charAt(right)
时,更新重复字符left
的最新下标;否则直接进入步骤2; - 更新
maxSubStrLen
,同时将s.charAt(right)
添加入map
;
解题代码2
public static int solution2(String s){
if(s == null) return 0;
int len = s.length();
if(len == 1) return 1;
/* Step 1: Init. integer and container.
*/
int maxSubStrLen = 0;
// <char, index>
Map<Character, Integer> map = new HashMap<>();
/* Step 2: go-through string
and
update existed index if checked.
*/
for(int left=0, right=0; right < len; right++){
if(map.containsKey(s.charAt(right))){
left = Math.max(map.get(s.charAt(right)), left);
}
maxSubStrLen = Math.max(maxSubStrLen, right - left + 1);
map.put(s.charAt(right), right + 1);
}
return maxSubStrLen;
}
复杂度分析
时间复杂度: 这里我们对数据进行了一次遍历,故时间复杂度为O(N)
;
空间复杂度: 我们借助了辅助容器,同solution1一样,为O(min(M, N))
;
Github源码
完整可运行文件请访问GitHub。