1.题目
请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。
示例 1:
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
提示:
s.length <= 40000
2.初始思路
哈希表:
从第一个字符开始将每个字符依次存入哈希表,若存在重复字符,不储存,记录下当前哈希表的大小max,清空哈希表
再从第二个字符开始,重复上述操作并与此时的哈希表大小比较,获得较大的max
直到遍历整个字符串
返回max
时间复杂度:两个循环,N2, 哈希表1
空间复杂度:常数个数的哈希表,1
3.初始算法实现
class Solution {
public int lengthOfLongestSubstring(String s) {
HashSet<Character> hs = new HashSet<>();
int max = 0;
int start = 0;
while(start < s.length())
{
for(int i = start; i < s.length(); i++)
{
char ch = s.charAt(i);
if( !hs.contains(ch) )
{
hs.add(ch);
}
else
{
max = Math.max(hs.size(), max);
start++;
hs.clear();
break;
}
}
}
return max;
}
}
4.交流后总结
这道题主要有下面几种思路
1.动态规划
2.动态规划+哈希表
3.滑动窗口(双指针)
4.滑动窗口(双指针)+ 哈希表
1.动态规划
- 构造状态:设动态规划表dp,dp[j]表示由字符s[j]结尾的最长不重复子序列的长度
为什么不表示子序列的开头:我们假定j=0,那么不存在上一个状态,但实际上尾部是不断移动的,不是一个状态而是多个状态 - 状态转移:对于dp[j],字符结尾为j,所以要考虑字符开头,设s[i]是左边距离s[j]最近的相同字符,s[i]= s[j]
当i<0,s[j]左边无相同字符,dp[j] = dp[j - 1] + 1
当s[j]在dp[j - 1]之外,dp[j - 1] < j - i,j不重复, dp[j] = dp[j - 1] + 1;
当s[j]在dp[j - 1]之内,dp[j - 1] >= j - i,j重复, dp[j] = j - i ;
第1种情况是第二种的特例,可以合并 - 返回值:max{dp}
只要求一个最大值,因此可以借助变量存储最大值即可,减小dp表的空间复杂度
class Solution {
public int lengthOfLongestSubstring(String s) {
int max = 0;
int temp = 0;
for(int j = 0; j < s.length(); j++)
{
int i = j - 1;
while( i >= 0 && s.charAt(j) != s.charAt(i) )
i--;
temp = (j - i) > temp ? temp + 1 : j - i;//状态转移方程
max = Math.max(temp, max);
}
return max;
}
}
时间复杂度:N2(遍历N,寻找i N)
空间复杂度:1
2.动态规划+哈希表
思路和上述类似,只是使用哈希表来判断i 的位置
class Solution {
public int lengthOfLongestSubstring(String s) {
int max = 0;
int temp = 0;
Map<Character, Integer> map = new HashMap<>();
for(int j = 0; j < s.length(); j++)
{
int i = map.getOrDefault(s.charAt(j), -1);
map.put(s.charAt(j), j);
temp = (j - i) > temp ? temp + 1 : j - i;
max = Math.max(max, temp);
}
return max;
}
}
时间复杂度:N(遍历N,哈希表寻找i 1)
空间复杂度:1(字符的ASCII码只有128个)
3.滑动窗口(双指针)
题目中要求是子串的长度,那么子串在字符串中一定是连续的。因此我们将答案看作原字符串中的一个滑动窗口,并维护窗口中不能有重复字符,同时更新窗口的最大值
算法:
-
初始化头尾两个指针
-
尾指针向右移动并判断指向的元素是否已经存在于头尾指针的窗口内
若窗口中没有此元素,将此元素加入窗口,更新窗口长度,尾指针继续右移
若窗口中已经存在此元素,头指针右移直到不存在重复元素 -
返回窗口长度最大值
class Solution {
public int lengthOfLongestSubstring(String s) {
//滑动窗口(双指针)
if(s.length() < 2)
return s.length();
int max = 0;
int start = 0;
for(int end = 0; end < s.length(); end++)
{
if( !s.substring(start, end).contains(s.substring(end, end + 1)))
max = Math.max(max, end - start + 1);
else
{
while(s.substring(start, end).contains(s.substring(end, end + 1)))
start++;
}
}
return max;
}
}
时间复杂度:两个循环N2(for循环N, 查重N)
空间复杂度:常数个变量,1
4.滑动窗口(双指针)+ 哈希表
基本思想与上述相同,不同的是使用哈希表来判断字符重复,键为字符,值为角标,当遇到重复的键时,更新头指针,使得新的区间内没有重复元素
class Solution {
public int lengthOfLongestSubstring(String s) {
//滑动窗口(双指针)+哈希表
Map<Character, Integer> map = new HashMap<>();
int max = 0;
int start = 0;
for(int end = 0; end < s.length(); end++)
{
if(map.containsKey(s.charAt(end)))
start = Math.max(start, map.get(s.charAt(end)) + 1 );//更新头指针
map.put(s.charAt(end), end);//更新哈希表
max = Math.max(max, end - start + 1);
}
return max;
}
}
时间复杂度:N(遍历一次字符串N,哈希表1)
空间复杂度:1(字符的ASCII码只有128个)
5.相关知识
String类:
substring(index1, index2)按角标位获得子串,含头不含尾
contains()判断是否包含指定字符
Map类:
containsKey()判断是否包含指定键
哈希表中相同的键会覆盖前面的
getOrDefault(Key, DefaultValue):如果存在Key对应的值,返回此值,不存在则返回DefaultValue