[Lesson Learn] LeetCode #3 Longest Substring Without Repeating Characters

来源:互联网 发布:golang.org上不去 编辑:程序博客网 时间:2024/04/30 15:05

首先先赞一下LeetCode,这道题暴力解法(会超时)原本的Java版参考答案中有一处错误,反馈后仅仅5分钟就解决了,算上读题理解代码的时间,意味着刚反馈就开始回应了。点赞。


首先想到的就是暴力破解,穷举各种可能性,然后逐一判断是否是不包含重复字母的子串,然后记录其中最长的子串的长度。

LeetCode上提供的修正了错误之后的暴力破解的代码如下:

public class Solution {    public int lengthOfLongestSubstring(String s) {        int n = s.length();        int ans = 0;        for (int i = 0; i < n; ++i)            for (int j = i + 1; j <= n; ++j)                if (allUnique(s, i, j)) ans = Math.max(ans, j - i);        return ans;    }    public boolean allUnique(String s, int start, int end) {        Set<Character> set = new HashSet<>();        for (int i = start; i < end; ++i) {            Character ch = s.charAt(i);            if (set.contains(ch)) return false;            set.add(ch);        }        return true;    }}
以上代码是完全遍历了所有的情况,一个不漏。

如果一个子集包含重复字母,则它的超集必然也是。所以可以在内层for循环的if判断后边加一句:

else break;
进行剪枝。

然而依旧超时。


每次判断是否是不包含重复字母的子串时,可以不用从头判断该子串的每一个字母,只需判断新加入的这一个字母是否在原子串中存在即可。

如果不在原子串中,则无事继续。如果已经在原来的子串中,则这两个字母之间的内容保留,而这两个重复字母则只能取其一。因为是从一端开始扫描过来的,假设是左端,那左端的所有信息已经全部掌握了,所以需要将左端的重复字母舍弃,保留右端的那个字母,然后继续向右扫描,直至结束。

为了迅速判断是否已经存在,要用到Map<Character, Integer>的containsKey()方法。为什么这里不是使用Set<Character>的contains()方法,而非得用Map的?Set和Map的区别在哪?或者换个类似的问题,集合和数组的区别在哪?一个无序一个有序,有序的数组的下标中可以包含位置信息。通过加一个Integer,来存储字母的位置信息。通过字母的位置信息,来判断一个字母是否符合要求。

假设用(i, j]来表示没有重复字母的子串中的字母的位置区间,j-i即中间所包含的数量,半开半闭区间的好处就在于,右侧减左侧即为差值,不用再考虑加一或者减一的问题。

读取的数据保存在字符串s中,下标从0开始。所以初始值,i为-1,j为0。用Map<Character, Integer>来保存已经扫描过的字母的信息,其中Integer保存的是已扫描部分中,值为Character的字母的最右边的位置。

public class Solution {    public int lengthOfLongestSubstring(String s) {        int n = s.length(), ans = 0;        Map<Character, Integer> map = new HashMap<>();        for (int j = 0, i = -1; j < n; ++j) {            char thisChar = s.charAt(j);            if (map.containsKey(thisChar)) {                i = Math.max(map.get(thisChar), i);            }            map.put(thisChar, j);            ans = Math.max(ans, j - i);        }        return ans;    }}

每一次循环读取一个值,设为thisChar。首先先判断已经扫描过的值中是否包含该值,然后由于map中保存的是该字母最右边的位置,i保存的是当前有效区间的左端点的位置,即可做出判断:如果map.get(thisChar) < i,即表示当前有效区间内并没有该值,i值不更新,将该字母的位置信息添加进map,j-i为当前有效区间的长度;如果map.get(thisChar) > i,即表示当前有效区间内有该值,将i更新为该值,同时将该字母在map中的位置信息进行更新,此时j指向的是该类字母在已扫描部分中最右边的那个字母,i指向的是该类字母在已扫描部分中从右边数第二个字母,(i, j)范围是没有重复字母的且与i和j处的字母不同,i和j位置是相同的字母,则(i, j]范围没有重复字母,且j-i即为长度。

if (map.containsKey(thisChar)) {i = Math.max(map.get(thisChar), i);}map.put(thisChar, j);
无论thisChar在map中是否存在,语句都一样。不存在则加入,存在则更新。

更新完当前有效区间之后,j-i即为当前有效区间的长度,将该值与历史最大值ans进行比较,更新ans的值。

ans = Math.max(ans, j - i);
如此循环下去,经过一次遍历,即可得到结果。


此题不再贴LeetCode官方提供的“参考答案”,因为并没有上边的代码容易理解。

半开半闭区间相比全开区间或者全闭区间的优势就在于,不用考虑加一或者减一的修正问题,而一旦一个地方进行修正,其他所有相关联的地方都要进行相应的修正不能遗漏,而且该加一还是该减一,稍微思路不清楚,就容易写错。



附:

LeetCode #3 题目

LeetCode #3 官方“参考答案”

0 1
原创粉丝点击