3.无重复字符的最长子串
题目:给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例:输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3
思路1:遍历字符串的每个字符,如果字符在哈希表中没有,就添加到哈希表中去,直到已经在哈希表中存在的即找到重复的了,记录最长的字符长度,清空哈希表,然后再从第2个字符开始向后遍历,以此类推直到遍历到最后一个字符结束,时间复杂度为O(n²)。
class Solution { public int lengthOfLongestSubstring(String s) { HashSet<Character> set = new HashSet<>(); int maxLen = 0; for (int i = 0; i < s.length(); i++) { int j = i; for (; j < s.length(); j++) { if (set.contains(s.charAt(j))) { set.clear(); break; } set.add(s.charAt(j)); } maxLen = Math.max(maxLen, j - i); } return maxLen; } }
思路2:在思路1的基础上优化,假设从i遍历到k时,k重复了,按照上面的方法是j要从i+1重新开始遍历,实际上是没有必要的,因为i+1到k-1之间是没有重复的,只需要从k接着向后遍历就可以了,因此j可以从k的位置继续向后,只需要将i向后移动一个即可,但要注意的是这种情况我们复用了之前存储在哈希表中的值,因此当i向后移动一个时要将前面的从哈希表中去除。时间复杂度为O(n)。
class Solution { public int lengthOfLongestSubstring(String s) { HashSet<Character> set = new HashSet<>(); int maxLen = 0; int j = 0; for (int i = 0; i < s.length(); i++) { //当i向后移动一个,就将前面的字符从哈希表中移除 if (i != 0) { set.remove(s.charAt(i - 1)); } for (; j < s.length(); j++) { if (set.contains(s.charAt(j))) { break; } set.add(s.charAt(j)); } //可以用while替换上面的for循环 /*while (j < s.length()) { if (set.contains(s.charAt(j))) { break; } set.add(s.charAt(j)); j++; }*/ maxLen = Math.max(maxLen, j - i); } return maxLen; } }
思路三:在思路二的基础上,考虑用HashMap同时存储字符和它的下标,这样当遇到重复字符时,可以直接将i定位到这个重复字符的后面,而不是定位到i+1,但是同样的也要将前面的字符从哈希表中移除,因此还需要循环同样的次数去执行remove操作,但是整体的循环次数是减小的,省去了一些重复的判断过程。时间复杂度同样为O(n)。
class Solution { public int lengthOfLongestSubstring(String s) { HashMap<Character, Integer> map = new HashMap<>(); int maxLen = 0; int i = 0; int j = 0; //记录移动之前的i的位置 int index; while (true) { while (j < s.length()) { if (map.containsKey(s.charAt(j))) { break; } map.put(s.charAt(j), j); j++; } maxLen = Math.max(maxLen, j - i); if (j == s.length()) { break; } index = i; i = map.get(s.charAt(j)) + 1; //将前面的字符从哈希表中移除 for (int k = index; k < i; k++) { map.remove(s.charAt(k)); } } return maxLen; } }
更进一步地,还可以直接用数组来代替哈希表,因为char类型的数据可以转换成整数,从题目中给的输入来看,一个长度为128的int或short数组应该可以满足,而数组的下标就代表char类型的数据,元素的值代表在字符串中的索引,这样可以获得更高的效率、占用更小的空间。
总结:本题的最佳解决思路就是采用滑动窗口,思路二和三就是,把i当成左窗口边界,j当成右窗口边界,开始窗口长度为0,i、j都在下标为0的位置,此时j依次向右滑,当遇到重复字符的时候,就将i向右滑,只不过思路二是每次i向右滑一步,而思路三是i直接滑到重复字符的下一个,也就是让窗口中的内容满足要求。代码实现上,我觉得思路三更符合滑动串口的思路,而思路二实现起来则更简洁。
【最佳】后来在题解中看到一种更巧妙的解法,不需要remove map中的字符。
链接: 画解算法:3. 无重复字符的最长子串 - 无重复字符的最长子串 - 力扣(LeetCode) (leetcode-cn.com)
class Solution { public int lengthOfLongestSubstring(String s) { int len = s.length(); int maxLen = 0; HashMap<Character, Integer> map = new HashMap<>(); for (int left = 0, right = 0; right < len; right++) { if (map.containsKey(s.charAt(right))) { /*不能只是将left赋值为map.get(s.charAt(right)) + 1 因为如"abba"当left=2时,即在第2个b的位置时,right向后只像最后一个a时, 判断map.containsKey(s.charAt(right)会为true,而这个位置(0)其实是在当前的left=2前面 此时left=1,right=3,会得到错误的结果maxLen=right-left+1=3-1+1=3 因此要将left始终赋值为更大的那一个*/ left = Math.max(left, map.get(s.charAt(right)) + 1); } map.put(s.charAt(right), right); maxLen = Math.max(maxLen, right - left + 1); } return maxLen; } }