引言
在字符串类型的题目中,常常在解题的时候涉及到大量的字符串的两两比较,比如要统计某一个字符串出现的次数。如果每次比较都通过挨个字符比较的方式,那么毫无疑问是非常占用时间的,因此在一些情况下,我们可以将字符串本身作为hashmap的key,往往会大大节省时间。
这篇博文中涉及的另一个技巧,是使用窗口的思想,这种思想不单单在字符串类型的题目中使用。这个技巧简单说来就是维护两个int,start和end,作为窗口的左右端。在不断的向前移动end和start的时候,记录下窗口包含的最大长度和最小长度,时间复杂度为O(n),这种技巧在求最大或者最小子集的时候很常用。
例题 1, anagrams (string as key思想)
题目:
Given an array of strings, return all groups of strings that are anagrams.
Note: All inputs will be in lower-case.
class Solution { public: vector<string> anagrams(vector<string> &strs) { } };
题意本身并不是很清晰,开始我的代码总是报Output Limit Exceeded,后来搜了相关文章,明白了题目真正要求的输出格式。
For example:
Input: ["tea","and","ate","eat","den"]
Output: ["tea","ate","eat"]
开始,我的思路是,将每一个string 都和其他比较,互为anagram的就记录到vector<string> res中。最后返回res。
这样宏观上来需要O(n2)次,n是输入vector的元素个数;对于内部判断anagram,我自己用数组实现dictionary[26],记录每一个character出现的次数,两个string如果正好可以让dictionary的全部元素回归0,则互为anagram,这样内部判断的时间是O(m),m是string的长度。
写这段代码时,我对输出的理解还存在错误,以为对于所有anagram group,只要将这个group中的第一个放入返回的vector<string>中即可。所以下面代码中,如果res中后面的元素已经判定和res中靠前的string互为anagram,后面的元素会被从res中移除。
初次实现的代码如下:
class Solution { public: vector<string> anagrams(vector<string> &strs) { vector<string> res; if(strs.size() == 0) return res; dic = new int[26]; for(vector<string>::iterator it = strs.begin(); it < strs.end(); ++it){ res.push_back(*it); } for(int i = 0; i < res.size(); ++i){ for(int j = i+1; j < res.size(); ++j){ initDic(dic, 26, res[i]); int k = 0; for(; k < res[j].length(); ++k){ //判断 res[i] 和res[j] 是否为anagrams dic[res[j][k] - 'a']--; if(dic[res[j][k] - 'a'] < 0) break; } if(k == res[j].length() && judgeDic(dic, 26)){ res.erase(res.begin() + j); //移除和res中的元素互为anagram的 --j; } } } return res; } private: int* dic; void initDic(int* dic, int n, string str){ for(int i = 0; i < n; ++i){ dic[i] = 0; } for(int j = 0; j < str.length(); ++j){ dic[str[j] - 'a']++; } } bool judgeDic(int* dic, int n){ int i = 0; for(; i < n; ++i){ if(dic[i] != 0) break; } return (i == n); } };
这样做,超时。
原因就在于宏观上的O(n2),应该有优化的余地。Annie Kim's Blog中介绍了空间换时间的做法,即定义一个map<string, int>,然后遍历strs的元素,对于strs中的每一个string s,先将s的内容排序,再将排好序的s当作key。
这样虽然排序本身需要O(mlogm)的时间(m是string的长度),但是宏观上,只需要O(n)的时间(n是输入vector的元素个数),因为map的访问是O(1)。
因此整体上时间复杂度可能会下降(测试用例的n较大时)。
但是这个思路的缺点在于:因为是将string 排序后本身作为key,因此如果题目增加难度,比如string中包含标点和空格,那么这种方法就不能准确判断两个string是否anagram了。另外,如果string非常长,用来做key也不是很方便。
我结合我自己的思路做了一些修改,修改后的思路中,key不是排完序的string,而是依然利用我开始代码里面的dic[26]:先从头到尾扫一遍string,然后给dic对应位置+1,然后将dic元素本身的排列作为key。这样,(1) 在有空格和标点的情况下,依然可以判断两个string是否是anagram,如果有大写字母或者数字,只需要扩张dic的大小即可;而且Key的长度为定值,这里总是26。(2) 不再需要O(mlogm)的时间复杂度,需要O(m+26) = O(m)的复杂度。
实现代码如下:
class Solution { public: vector<string> anagrams(vector<string> &strs) { vector<string> res; if(strs.size() == 0) return res; map<string, int> rec; dic = new int[26]; for(int i = 0; i < strs.size(); ++i){ string key = generateKeyByDic(dic, 26, strs[i]); if(rec.find(key) == rec.end()){ rec.insert(make_pair(key, i)); }else{ if(rec[key] >= 0){ res.push_back(strs[rec[key]]); rec[key] = -1; } res.push_back(strs[i]); } } return res; } private: int* dic; string generateKeyByDic(int* dic, int n, string str){ for(int i = 0; i < n; ++i){ dic[i] = 0; } for(int j = 0; j < str.length(); ++j){ if(str[j] <= 'z' && str[j] >= 'a') dic[str[j] - 'a']++; } string key(26, '0'); for(int k = 0; k < 26; ++k){ key[k] = dic[k] + '0'; } return key; } };
100 / 100 test cases passed. Runtime: 224 ms
而是用sorted string做key的方法,数据是 100 / 100 test cases passed. Runtime: 228 ms
时间上并没有提高多少,原因应该是test case的string长度都不算大,故O(mlogm)和O(m+26) 差别不大。
不论是引用的思路,还是我的思路,核心都是使用了map<string, int>,当需要在一堆字符串中找出包含相同字符的 group,这种空间换时间的方法可以考虑。
例题 2, Longest Substring Without Repeating Characters (窗口思想)
Given a string, find the length of the longest substring without repeating characters. For example, the longest substring without repeating letters for "abcabcbb" is "abc", which the length is 3. For "bbbbb" the longest substring is "b", with the length of 1.
class Solution { public: int lengthOfLongestSubstring(string s) { } };
这道题需要使用窗口的思想,定义start,end作为窗口的两端,开始时start = end = 0;再定义一个Map,用来检测窗口中是否有重复字符。
这样可以在O(n)时间复杂度和O(n)空间复杂度下接触。当然如果字符类型只是限于ASCII表的话,空间复杂度就是constant了。
class Solution { public: int lengthOfLongestSubstring(string s) { int len = s.length(); if(len == 0) return 0; int start = 0, end = 0, max = 0; int* map = new int[256]; //自定义Map for(int i = 0; i < 256; ++i) map[i] = 0; while(end < len){ if(map[s[end] - '