LeetCode高频题3:无重复字符的最长子串
提示:本题是系列LeetCode的150道高频题,你未来遇到的互联网大厂的笔试和面试考题,基本都是从这上面改编而来的题目
互联网大厂们在公司养了一大批ACM竞赛的大佬们,吃完饭就是设计考题,然后去考应聘人员,你要做的就是学基础树结构与算法,然后打通任督二脉,以应对波云诡谲的大厂笔试面试题!
你要是不扎实学习数据结构与算法,好好动手手撕代码,锻炼解题能力,你可能会在笔试面试过程中,连题目都看不懂!比如华为,字节啥的,足够让你读不懂题
题目
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
本题是一个非常非常经典的题目,基本每个月都会有大厂考!!!
一、审题
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-substring-without-repeating-characters
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
子串子序列:经典的解决思路:考虑以i开头或者结尾的情况
遇到子串:尝试考虑以i开头或者结尾的情况
本题是考虑以i结尾的子串,它的最长不重复字符的子串
整个字符串s的每一个位置i都求一个结果,整体max必在其中
比如:
pwwkew
看下图,i=0时,必须以i=0结尾的子串,最长就是p,长度为1
必须以i=1结尾的子串,最长就是pw,长度为2
必须以i=2结尾的子串,最长就是w,长度为1
必须以i=3结尾的子串,最长就是wk,长度为2
必须以i=4结尾的子串,最长就是wke,长度为3
必须以i=5结尾的子串,最长就是kew,长度为3
因此结果为3
代码怎么实现整个逻辑呢?
很巧妙!
来到i位置的,看到str[i]字符,你怎么求这个结果呢?
咱们至少要准备i-1位置之前的所有结果,不妨设放入dp数组
dp[i]就是:以i结尾的子串,它的最长不重复字符的子串长
这样的话,你看看i-1位置那个字符,最远能推多远呢?比如在j1处
下图,i-1处是b,b最远能推到j1=2处,j1=i-1-dp[i-1]
那么dp[i]是多少呢?还得看i位置字符a上次出现的位置j2
上图中上次j2=1
那么我dp[i]到底和j1和j2有啥关系呢?
实际上dp[i]最远能推到的位置是j1和j2的最大值!
因为i往前,谁都不能重复出现,上次dp[i-1]已经有一个界限了j1,你现在a上次出现虽然j2更远,但是你不能上海b字符,让b进来重复,不达标!
所以呢,j1就是a字符能推过去的最远位置
dp[i]=i-max(j1,j2)=5-2=3
当然,万一j2更大的话,就是去j2了,看看下图的例子,上次a出现的位置是3的话,情况就不一样了,但是计算方法一样
所以呢,j2就是a字符能推过去的最远位置
dp[i]=i-max(j1,j2)=5-3=2
明白?
其实咱们就只需要做2件事:
(1)用一个哈希表map将每个字符上次出现的位置i记忆下来,既然是遇到字符,就不用真的是哈希表
完全可以用数组替代,只不过字符就是ASCII码而已
直接用数组,0–255的ASCII码值做下标就行,这是常用额技巧,速度快
(2)还得记录一个结果,dp数组,用来记忆s以i结尾的子串,最长的不重复长度
比如上面案例中i=4时,dp[4]=2,dp[5]=3
想清楚这件事,其实手撕代码就好说了:
//复习:优化算法,用哈希表记忆上次字符出现的位置
public int lengthOfLongestSubstringReview(String s) {
if (s.compareTo("") == 0 || s.length() == 0) return 0;
//(1)用一个哈希表map将每个字符上次出现的位置i记忆下来,既然是遇到字符,就不用真的是哈希表
//完全可以用数组替代,只不过字符就是ASCII码而已
int[] map = new int[256];
for (int i = 0; i < 256; i++) {
map[i] = -1;//最开始默认为-1,好计算结果
}
//(2)还得记录一个结果,dp数组,用来记忆s以i结尾的子串,最长的不重复长度
char[] str = s.toCharArray();
int N = str.length;
int[] dp = new int[N];
dp[0] = 1;//至少有一个字符的
int max = 1;//结果
map[str[0]] = 0;
//每个i结尾,求一个答案,看看子串长度多少?
for (int i = 1; i < N; i++) {
//实际上dp[i]最远能推到的位置是j1和j2的最大值!
int j1 = i - 1 - dp[i - 1];//这是可以优化的位置
int j2 = map[str[i]];//上次出现的位置,-1也没事,--1=1
dp[i] = i - Math.max(j1, j2);//看看i开始最远能推过去的位置
max = Math.max(max, dp[i]);//每个结果都给max更新
//最后别忘加入stri的位置i
map[str[i]] = i;
}
return max;
}
自己线下测试:
public static void test(){
Solution solution = new Solution();
System.out.println(solution.lengthOfLongestSubstring2("aabcdb"));
System.out.println(solution.lengthOfLongestSubstringReview("aabcdb"));
}
public static void main(String[] args) {
test();
}
问题不大:
4
4
所以情况就是这样了
精简一下代码:把dp省掉
但是真的需要把dp把每一个位置都存下来吗???
其实不需要,因为每次来求dp[i]时,咱只需要看i-1处和i的字符的状况
咱们用一个变量j2把i-1最远能推到的位置记住,省去dp了
重新改一个代码:
class Solution {
//复习:优化算法,用哈希表记忆上次字符出现的位置
public int lengthOfLongestSubstring(String s) {
if (s.compareTo("") == 0 || s.length() == 0) return 0;
//(1)用一个哈希表map将每个字符上次出现的位置i记忆下来,既然是遇到字符,就不用真的是哈希表
//完全可以用数组替代,只不过字符就是ASCII码而已
int[] map = new int[256];
for (int i = 0; i < 256; i++) {
map[i] = -1;//最开始默认为-1,好计算结果
}
//(2)还得记录一个结果,dp数组,省了
char[] str = s.toCharArray();
int N = str.length;
int max = 0;//结果
int j1 = -1;//i-1能推过去的最远位置,下次更新就作为i的推到的最远位置了
//每个i结尾,求一个答案,看看子串长度多少?
for (int i = 0; i < N; i++) {
//i-1最远能推到的位置是j1,和当前i字符上次出现的位置,最大值下次就是i的最远推到的位置了
j1 = Math.max(j1, map[str[i]]);//看看i开始最远能推过去的位置
max = Math.max(max, i - j1);//i-j1就是咱们要的长度结果
//最后别忘加入stri的位置i
map[str[i]] = i;
}
return max;
}
}
问题不大:
线下测试:
public static void test(){
Solution solution = new Solution();
System.out.println(solution.lengthOfLongestSubstring2("aabcdb"));
System.out.println(solution.lengthOfLongestSubstringReview("aabcdb"));
System.out.println(solution.lengthOfLongestSubstringReview2("aabcdb"));
}
public static void main(String[] args) {
test();
}
4
4
4
总结
提示:重要经验:
1)本题是一个非常非常经典的题目,基本每个月都会有大厂考!!!
2)最重要的就是理解,来到i位置,以i结尾的达标子串,最远能推过去的位置是j1还是j2,j1是i-1位置最远能推过去的位置,j2是上次i位置字符出现的位置
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。