[LeetCode] 3. Longest Substring Without Repeating Characters 最长无重复字符的子串
1.暴力法:
本题让求给定字符串的最长的无重复字符的子串,首先想到暴力解法,穷举出字符串的所有子串,并判断每个子串是否是不重复子串,具体使用hashset或set判是否有重复字符;暴力法效率很差,时间O(n^3),空间O(n);参考代码如下:
1 class Solution { 2 public: 3 int lengthOfLongestSubstring(string s){ 4 int res = 0; 5 const int size = s.size(); 6 if(s.empty()) return 0; 7 if(size<=1) return size; 8 for(int i = 0;i<size;++i) 9 for(int j = i;j<size;++j) 10 { 11 int sub_seq = j-i+1; 12 if(isNoreapeatSubstring(i,j,s)){ 13 if(sub_seq>=res) 14 res = sub_seq; 15 } 16 } 17 return res; 18 } 19 20 bool isNoreapeatSubstring(int l,int h,string &s) 21 { 22 set<char> charSet; 23 for(int i =l;i<=h;i++) 24 { 25 if(charSet.find(s[i])==charSet.end()) 26 { 27 charSet.insert(s[i]); 28 } 29 else{ 30 return false; 31 } 32 } 33 return true; 34 } 35 };
2.动态规划
稍加思考,很容易发现本题是一个具有最优子结构的最优解问题,所以可以用动态规划的方法;
2.1 定义状态
dp[i]: 字符串s中,以字符s[i]结尾的最长的不重复子串的长度;
2.2 转态转移 (方程)
当s[i]和以s[i-1]结尾的最长不重复子串中所有的字符都不相同时,dp[i] = dp[i-1]+1;否则,找以s[i-1]结尾的最长不重复子串中和s[i]重复的最后的字符位置index,则
dp[i]=i-1-index+1; 动态规划的时间O(n^2),空间O(n^2);
参考代码如下:
1 class Solution { 2 public: 3 int lengthOfLongestSubstring(string s) { 4 const int size = s.size(); 5 if(s.empty()) return 0; 6 if(size<=1) return size; 7 int dp[size];//dp[i]表示以s[i]结尾的不重复子串 8 memset(dp,0,size); 9 int res = 1; 10 dp[0]=1;//dp初始值 11 for(int i = 1;i<size;++i) 12 { 13 bool reapeat = false; 14 int index = 0; 15 for(int j = i-1;j>= i-1-dp[i-1]+1;j--) 16 { 17 if(s[j]==s[i]) 18 { 19 reapeat = true; 20 index = j ;//最近的一次重复 21 break; 22 } 23 } 24 if(reapeat)//重复 25 { 26 dp[i]=i-1-index+1; 27 } 28 else //不重复 29 { 30 dp[i]=dp[i-1]+1; 31 } 32 if(dp[i]>=res) 33 res=dp[i]; 34 } 35 return res; 36 } 37 };
3.滑动窗口+hash表 法
可以在遍历字符串 的过程中维护一个滑动窗口,使得滑动窗口内的子串表示字符串的当前最长不重复子串,具体地,使用hashmap m记录字符串中字符和其最后一次出现的位置之间的映射;定义变量left,res,left表示滑动窗口的左边界的前一个位置,初始化为-1,res表示滑动窗口的长度最大值;
遍历到一个字符s[i]时,如果字符在hashmap中,且最后一次的位置在滑动窗口内(i>left),将滑动窗口的边界右移,left = m[s[i]];然后更新hash表,
使用滑动窗口+hash表的方法可以实现最好的效率,因为只需遍历一遍,所以时间O(n),因为字符的个数是一定的(最多128个)所有空间O(1),
class Solution { public: int lengthOfLongestSubstring(string s) { int res = 0,left = -1,str_len=s.size();// unordered_map<int,int> m;//映射字符中字符和其最后一次出现的位置 for(int i = 0;i< str_len;++i) { if(m.count(s[i])&&m[s[i]]>left){ left = m[s[i]]; } m[s[i]] = i;//更新字符最后一次出现的位置 res = max(res,i-left); } return res; } };
下面这种写法是上面解法的精简模式,这里我们可以建立一个 256 位大小的整型数组来代替 HashMap,这样做的原因是 ASCII 表共能表示 256 个字符,但是由于键盘只能表示 128 个字符,所以用 128 也行,然后全部初始化为 -1,这样的好处是不用像之前的 HashMap 一样要查找当前字符是否存在映射对了,对于每一个遍历到的字符,直接用其在数组中的值来更新 left,因为默认是 -1,而 left 初始化也是 -1,所以并不会产生错误,这样就省了 if 判断的步骤,其余思路都一样:
class Solution { public: int lengthOfLongestSubstring(string s) { vector<int> m(128, -1); int res = 0, left = -1; for (int i = 0; i < s.size(); ++i) { left = max(left, m[s[i]]); m[s[i]] = i; res = max(res, i - left); } return res; } };