【力扣算法日记】无重复字符的最长子串
最近刷了很多算法题,这些解题过程也拓展了自己的思路,是个适合记录的素材。所以决定在继技术知识点详解的【一文系列】之后,开启新坑——【力扣算法系列】,来记录力扣刷题过程。
分享题目不确定,目前打算只分享我认为值得分享的题目,或者你们有什么想要我分享的题目,我愿意去研究透彻之后出分析详解!分享的过程也是自我学习的过程!
那么今天,我们分享第一道题目:无重复字符的最长子串
题目
无重复字符的最长子串
给定一个字符串 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:
一直维持这样的队列,找出队列出现最长的长度时候,我们就求得最终答案。
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; }
这道题的解法中,暴力解法一定是不可取的,其实能写出滑动窗口解法就差不多了,优化方式都是加分项了。当然了,解题方法一定不止于此,大家可以继续深究!
更多技术干货,欢迎关注我!