一.題目鏈接:https://leetcode.com/problems/longest-substring-without-repeating-characters
二.題目大意:
給定一個字符串,返回它最長的無重復的子串的長度。
三.題解
該題目,我用了三種方法:
1.思路一:暴力解決,直接用兩層for循環對目標字符串所有的子串進行判斷。代碼如下:
class Solution { public: int lengthOfLongestSubstring(string s) { int len = s.length(); string str;//表示最長子串 int max_len = 0; for(int i = 0; i < len; i++) { string t_str;//中間變量; for(int j = i ; j < len; j++) { t_str = t_str + s[j]; if(isUnique(t_str) == 1) { if(t_str.length() > max_len) max_len = t_str.length(); } else break; } } return max_len; } //判斷字符串是否包含重復元素(利用了哈希表,所以時間復雜度為O(n)) int isUnique(string s) { int len = s.length(); unordered_map<char,int> _map; unordered_map<char,int>::iterator it; for(int i = 0 ; i < len; i++) { it = _map.find(s[i]); if(it == _map.end()) _map.insert(pair<char,int>(s[i],1)); else return 0; } return 1; } };
isUnique()用於判斷字符串是否包含重復的元素,由於用的是unordered_map(由哈希表實現,時間復雜度為O(1)),該函數的時間復雜度的為O(n);lengthOfLongestSubstring(string s)方法的時間復雜度為O(n2),故總的時間復雜度為O(n3),空間復雜度為O(n)。
提交結果:超時!
2.對方法一進行改,發現子串判斷是否包含重復元素的過程可以在遍歷過程中實現,所以就沒必要單獨定義一個方法了。代碼如下:
class Solution { public: int lengthOfLongestSubstring(string s) { int len = s.length(); string str;//表示最長子串 int max_len = 0; for(int i = 0; i < len; i++) { string t_str;//中間變量; unordered_map<char,int> _map; unordered_map<char,int>::iterator it; for(int j = i ; j < len; j++) { it = _map.find(s[j]); if(it == _map.end()) { t_str = t_str + s[j]; _map.insert(pair<char,int>(s[j],1)); if(t_str.length() > max_len) max_len = t_str.length(); } else break; } } return max_len; } };
這種方法,把unordered_map查詢和插入的過程融合到for循環中,總的時間復雜度為O(n2),相比方法一時間上縮小了一個數量級。
提交結果:Accepted,耗時786ms.
3.看提交結果只打敗了0.02%cpp提交。還可不可以再進行改進呢?
思路三:對於一個字符串,例如qpxrjxkltzyx,我們從頭開始遍歷時,如果遇到一個之前出現過的字符的話,例如:qpxrjx,那么從第一個x到第二個x的這段子串,一定不包含在我們的最終結果之中,我們需要記錄的子串為:qpxrj;而且下一次的起始位置是第一個x之后的那個位置;這樣指導最后一個字符;這實質可以看成一種貪心(子問題的性質決定了父問題的性質)。如圖所示:
代碼如下:
class Solution { public: int lengthOfLongestSubstring(string s) { vector<int>dict(256,-1);//此處的vector相當於一個哈希表 int max_len = 0; int len = s.size();//string的size()方法和length()方法內部實現其實完全一樣,只不過容器都有size()方法,此處是為了滿足不同人的習慣 int start = -1;//起始位置 for(int i = 0; i < len; i++) { //遇到重復出現的字符,則start的位置變成重復的那個字符之前出現的位置 if(dict[s[i]] > start) start = dict[s[i]];//本來start應該為dict[s[i]]+1的,但后面計算長度時,還需在加個1,相當於i-start+1,所以1相當於抵消了 max_len = max(max_len, i - start);//每次迭代長度都更新 dict[s[i]] = i; } return max_len; } };
需要注意的點都在注釋中說明了,尤其需要注意的是start的賦值此處為了簡便,把1去掉了;每次迭代長度更新相當於記錄了當前子串的最大長度。此外start的初始值為-1,因為位置0處的字符也要考慮。i一直是增加的,只是start的位置在變化。
該算法的時間復雜度為O(n),空間復雜度為O(1),相比方法2,時間又縮短了一個數量級。
提交結果:Accepted,耗時18ms.
4.此外,如果本題要求的結果是最長無重復子串的話怎么辦?可以設置兩個標記s和e,用於記錄每次更新max_len的i和start,最終的子串即以位置s開以位置e結尾的子串。
5.《劍指offer(第二版)》面試題48,給出了利用動態規划的思想求解,具體思路參考《劍指offer》,具體代碼如下:
class Solution { public: int lengthOfLongestSubstring(string str) { int max_len = 0;//最大值 int cur_len = 0;//以當前位置的字符結尾的不含重復字符的字符串的最大長度,對應f(i) int pos[255] = {0};//記錄字符上一次出現的位置 memset(pos,-1,sizeof(pos)); int len = str.size(); for(int i = 0; i < len; i++) { int pre_index = pos[str[i]]; if(pre_index < 0 || i - pre_index > cur_len) cur_len++; else { cur_len = i - pre_index; } pos[str[i]] = i; if(cur_len > max_len) max_len = cur_len; } return max_len; } };
此方法的時間復雜度同思路三一樣,都是O(n),需要注意的是:
如果令dp[i]表示以第i個字符為結尾的不包含重復字符的子串的最長長度,那實際上cur_len這個變量就表示的是dp[i],即這段代碼中沒有顯示的利用dp[i]的形式,因為沒必要(我們求得是最大值,當前的結果只受前一個結果的影響,所以一個變量就能解決,沒必要把所有的結果都存起來)。