最近刷了很多算法题,这些解题过程也拓展了自己的思路,是个适合记录的素材。所以决定在继技术知识点详解的【一文系列】之后,开启新坑——【力扣算法系列】,来记录力扣刷题过程。
分享题目不确定,目前打算只分享我认为值得分享的题目,或者你们有什么想要我分享的题目,我愿意去研究透彻之后出分析详解!分享的过程也是自我学习的过程!
那么今天,我们分享第一道题目:无重复字符的最长子串
题目
无重复字符的最长子串
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
来源:https://leetcode.cn/problems/longest-substring-without-repeating-characters/description/
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。提示:
0 <= s.length <= 5 * 104
s 由英文字母、数字、符号和空格组成
解题
暴力法
这是最容易想到的方法,当然也是最废的方法。不过我们在解题的时候,可以先尝试暴力解法,然后再一步步着手优化,或想出更好的方法,这也是一种解题思路。在这道题目中,暴力解法就是逐个检查所有的子字符串,看它是否不含有重复的字符。
public static int lengthOfLongestSubstring(String s) {
char[] chars = s.toCharArray();
Map<Character, Integer> subChars = new HashMap<Character, Integer>(chars.length);
int maxSize = 0;
int repeatIndex = -1;
int i = 0;
while (i < chars.length) {
for (int j = i; j < chars.length; j++) {
char c = chars[j];
if (subChars.containsKey(c)) {
repeatIndex = subChars.get(c);
break;
}
subChars.put(c, j);
}
if (repeatIndex != -1) {
// 找到重复元素,从第一个重复元素的下一个下标开始遍历
i = repeatIndex + 1;
repeatIndex = -1;
} else {
i++;
}
maxSize = Math.max(maxSize, subChars.size());
subChars.clear();
float middleSize = chars.length / 2f;
if (maxSize >= middleSize && i >= middleSize) {
// 最大子串长度超过一半了,可以不用继续遍历了
break;
}
}
return maxSize;
}
滑动窗口
滑动窗口是字符串类问题的最常用的方法。在这道题目中,我们按区间进行字符串搜索,遇到重复字符,就从左区间开始删除,直到删除掉重复字符为止。
在这道题目中,滑动窗口其实相当于就是一个队列,比如例题中的 abcabcbb:
-
进入这个队列(窗口)为 abc,暂时满足题目要求
-
当再进入 a,队列变成了 abca
-
这时候不满足要求,所以,我们要移动这个队列,把队列的左边的元素移出就行了,一直到满足题目要求。
一直维持这样的队列,找出队列出现最长的长度时候,我们就求得最终答案。
public int lengthOfLongestSubstring(String s) {
int len = s.length();
// 最大长度
int maxLength = 0;
// 左指针
int left = 0;
// 右指针
int right = 0;
// 子串队列
Set<Character> subChars = new HashSet<>();
while (left < len && right < len) {
if (subChars.contains(s.charAt(right))) {
// 子串重复,字符串从队首开始移除
subChars.remove(s.charAt(left));
// 移动左指针
left++;
} else {
// 子串不重复,字符入队尾
subChars.add(s.charAt(right));
right++;
// 记录最长不重复长度
maxLength = Math.max(maxLength, right - left);
}
}
return maxLength;
}
优化的滑动窗口
在滑动窗口时,上述的方法最多需要执行 2n 个步骤。事实上,它可以被进一步优化为仅需要 n 个步骤。我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。 当我们找到重复的字符时,我们可以立即跳过该窗口。
public static int lengthOfLongestSubstring(String s) {
int len = s.length();
// 最大长度
int maxLength = 0;
// 使用Map结构缓存字符和索引的映射
Map<Character, Integer> map = new HashMap<>();
// 滑动窗口
for (int right = 0, left = 0; right < len; right++) {
char c = s.charAt(right);
if (map.containsKey(c)) {
// 找到重复子串,左指针跳到最后的重复位置
left = Math.max(map.get(c), left);
}
int curLength = right - left + 1;
// 取最大长度
maxLength = Math.max(maxLength, curLength);
// 缓存字符和下标映射
map.put(c, right + 1);
}
return maxLength;
}
优化的滑动窗口2
针对上述优化方式,我们还可以进一步优化,不过这个方法相对有些投机取巧,各位看看就行
public static int lengthOfLongestSubstring(String s) {
// Java(假设字符集为 ASCII 128)
// 以前我们没有对字符串 s 所使用的字符集进行假设。
// 当我们知道该字符集比较小的时侯,我们可以用一个整数数组作为直接访问表来替换 Map。
int len = s.length();
// 最大长度
int maxLength = 0;
// 使用128位数组缓存下标
int[] index = new int[128];
// 滑动窗口
for (int right = 0, left = 0; right < len; right++) {
char c = s.charAt(right);
// 取index对应字符的缓存下标
// 如果没有,则是默认值0
// 若一旦有非零值,则代表有相同字符缓存到index数组中,则下标可跳到最后重复为止,再计算出不重复长度
left = Math.max(index[c], left);
int curLength = right - left + 1;
// 取最大长度
maxLength = Math.max(maxLength, curLength);
// 缓存字符和下标映射
index[c] = right + 1;
}
return maxLength;
}
这道题的解法中,暴力解法一定是不可取的,其实能写出滑动窗口解法就差不多了,优化方式都是加分项了。当然了,解题方法一定不止于此,大家可以继续深究!
更多技术干货,欢迎关注我!