LeetCode #3 Longest Substring Without Repeating Characters 不带重样儿的最长连续子串 MAP DP
Description
Given a string, find the length of the longest substring without repeating characters.
Examples:
Given
"abcabcbb"
, the answer is"abc"
, which the length is 3.Given
"bbbbb"
, the answer is"b"
, with the length of 1.Given
"pwwkew"
, the answer is"wke"
, with the length of 3. Note that the answer must be a substring,"pwke"
is a subsequence and not a substring.
思路
思考了一遍题目,发现其实这题需要的子串其实可以想象成一个队列,每次入队时都需要查看队里有没有重复的字符,若有则将重复的字符及其之前的字符给删去,这样就可以保证队内没有重复的字符了。那么问题就来了,如何去模拟这样一个队列?
我们可以用1个 set 和 两个哨兵 i j 去模拟一个队列。set (集合)是一个 key 与 value 必须相等的数据结构,用它的原因是它可以用红黑树或哈希表实现,查找效率高一些。哨兵 i 表示子串左端,哨兵 j 表示子串右端,每一轮扫描都在 set 的键中查找 s[j] 是否存在,若不存在则在 set 中增加元素 s[j] 且 j++,若存在则在集合 set 中删去元素 s[i] 且 i++,这样就实现了一个队列的操作,一段出,一段入。
如何求最长子串呢?在每一轮中实时比较子串长度并更新记录 res即可。
//Runtime: 45 ms #include<set> using std::set; class Solution { public: int lengthOfLongestSubstring(string s) { set<char> mySet; int res = 0, temp = 0; int i = 0,j = 0; int n = s.size(); while (j < n) { if (mySet.find(s[j]) != mySet.end()) { mySet.erase(s[i]); ++i; } else { mySet.insert(s[j]); ++j; } temp = mySet.size(); if (res < temp) { res = temp; } } mySet.clear(); return res; } };
好了,AC。但是,Run Time 的排名是在 26%,显然是优化不够,比平均水平都慢。虽然我用高级数据结构 set 去模拟队列,但队列的本质意味着事实上它还是一个暴力 O(n^2) 的过程,时间复杂度在样例小的情况下与用数组并没有什么不同。如何才能优化的好一些呢?改变策略。一种全新的策略可以大幅度减少时间的浪费。
参考当时数据结构课上学的 KMP 算法,我们可以用类似的方法把”忽略的信息“给利用起来,以此提高查找效率。
本题中的”忽略的信息“是指重复字符的索引。经过分析我们发现,无重复字符的子串的长度其实是最近一次发生字符重复问题时重复字符的位置的下一位为起始地址,开始往后计算,直到下一次发生字符重复问题时重复字符的位置为止。
举个例子,有一字符串 a t m m z u x t a ,当我们扫描到第四个字符时出现了字符重复问题,字符 m 重复了,那我们下一次计算长度就可以以第一个 m 的位置的下一位即第二个 m 为起点往后计算,而不需要考虑其他的操作,这样只需要一个 for 循环就可以解决,肯定比上面一个 伪 O(n) 的 while 循环要快上不少。
那么现在策略的重点在于如何在扫描到重复字符 m 的时候把它上一次出现的 m 的索引给存储起来呢?这就需要一本字典 Map,遍扫描边存储每个字符的索引,每当发生字符重复问题时就将重复字符的索引更新,这样就把之前我们说过的”忽略“的信息给利用起来了。
我们再定义一个变量 start ,每次出现字符重复的情况时,就将重复字符下一位的索引存储到 start,那么之后在 Map 里查找是否出现重复字符的时候就直接从 start 的位置向后查找就好了,而不在乎 start 之后的字符是否与 start 之前的字符发生重复现象。
用这样的办法,时间复杂度是 O(n),比前一个 O(n^2) 的算法在时间上要节省了 1/4。
//Runtime: 32 ms
#include<unordered_map> using std::unordered_map; class Solution { public: int lengthOfLongestSubstring(string s) { unordered_map<char, int> um; int res = 0, start = 0, temp; int i; int n = s.size(); if (n == 0) { return 0; } for (i = 0; i < n; ++i) { char ch = s[i]; if ((um.find(ch) != um.end()) && (um[ch] >= start)) { start = um[ch] + 1; um[ch] = i; } else { um[ch] = i; temp = i - start + 1; if (res < temp) { res = temp; } } } um.clear(); return res; } };
其实还有一种更厉害的处理策略是动态规划,能够以数量级地降低时间复杂度。动态规划思想的厉害之处是在于尽可能地利用之前的计算结果,得出递推关系,时间复杂度是 O(n),而且不用 Hash 这么庞大的数据结构去查找。我还没有学,在这也不误人子弟了(逃