大二菜鸟———无重复字符的最长子串

207 阅读3分钟

这是一枚大二菜鸟的成长反思博客

  终于忍不住,在这六月将之的一天开始写起了博客。数据结构计算机算法杂七杂八学了一堆但是都只是浅尝,觉得还是要写博客反刍学习到的知识。

  感谢大佬 @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 的大小