这是一枚大二菜鸟的成长反思博客
终于忍不住,在这六月将之的一天开始写起了博客。数据结构计算机算法杂七杂八学了一堆但是都只是浅尝,觉得还是要写博客反刍学习到的知识。
感谢大佬 @Java3y 的推荐,于是决定选择掘金这样一个技术氛围清爽的平台作。 废话不多说,今天做了一道LeetCode上的题目,险些翻车。将体会思路记录下来
无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
``
我们先理清题目要求
1、找字串
2、字串内部没有重复的字符
看完题目是不是觉得很简单。我们马上动手有了第一次的代码
public static int lengthOfLongestSubstring(String s) {
// 截取的字符串
String check;
// 最长无重复字符串
String ans = new String("");
int maxLen = -1;
for (int i = 0; i < s.length(); i++) {
for (int j = i + 1; j <= s.length(); j++) {
//截取字符串s[i,j)
check = s.substring(i, j);
boolean flag = true;
for (int k = 0; k < check.length(); k++) {
char ch = check.charAt(k);
/*
* 如果从头开始找与最后开始找到的位置一样那么有重复
*/
if (check.indexOf(ch) != check.lastIndexOf(ch))
flag = false;
}
if (flag && check.length() > maxLen) {
ans = check;
maxLen = check.length();
}
}
}
return ans.length();
}
回顾一下我们的代码,发现这样的思路实在太暴力了,而且太慢了。时间复杂度:O(n^3)以上。那我们该如何考虑优化它呢。
优化思路——使用HashMap提高检查重复字串的速度
我们注意到之前的暴力法中检查字符串之间重复字串速度太慢,于是我们考虑使用HashMap O(1) 的时间来完成检查。
再然后将for循环中的length()单独拎出来,减少每次调用函数的时间
public static int violentSolution(String s) {
//最长字串长度
int maxLen = 0;
int len = s.length();
for (int i = 0; i < len; i++) {
for (int j = i + 1; j <= len; j++) {
//使用HashMap检查重复
Map<Character, Boolean> map = new HashMap<>();
boolean flag = true;
for (int k = i; k < j; k++) {
Character ch = s.charAt(k);
//map中有当前元素重复
if (map.containsKey(ch)) {
flag = false;
break;
}
map.put(ch, true);
}
if (flag) {
maxLen = Math.max(maxLen, j - i);
}
}
}
return maxLen;
}
虽然已经可以了。但是我们不能满足在这里,于是继续思索还有没有别的更快的思路呢。
优化思路——使用HashSet的滑动窗口
在暴力法中,我们会反复检查一个子字符串是否含有有重复的字符,但这是没有必要的。如果从索引 i到 j-1之间的字串s[i,j)已经被检查为没有重复字符。我们只需要检查 s[j]对应的字符是否已经存在于子字符串s[i,j)中
通过使用 HashSet 作为滑动窗口,我们可以用 O(1)的时间来完成对字符是否在当前的子字符串中的检查。
public static int moveWindow(String s) {
Set<Character> set = new HashSet<>();
int len = s.length();
int ans = 0;
/*
* 滑动窗口思想:
* 1、使用两个边界值:i,j,将字符存储在窗口[i,j)中。
* 2、滑动右侧j,知道找到一个s[j]使得恰好s[j]在这个set中,此时不重复最长
* 3、依次按照1、2处理后续的i,直到到达到边界
*/
int i = 0, j = 0;
while (i < len && j < len) {
Character ch = s.charAt(j);
//到达边界
if(set.contains(ch)) {
set.remove(s.charAt(i));
i++;
}
else {
set.add(ch);
j++;
ans = Math.max(ans, j - i);
}
}
return ans;
}
提交,正确完美。接下来让我们进行复杂度的分析
时间复杂度:O(2n) = O(n)O(2n)=O(n),在最糟糕的情况下,每个字符将被 ii 和 jj 访问两次。
空间复杂度:O(min(m, n))O(min(m,n)),与之前的方法相同。滑动窗口法需要 O(k)O(k) 的空间,其中 kk 表示 Set 的大小。而 Set 的大小取决于字符串 nn 的大小以及字符集 / 字母 mm 的大小。