leetcode 3. 无重复最长字串
题目描述
给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。
思路
用mp记录字符上次出现的位置,用last记录当前起始位置,遇到重复的字符计算ans,并更新last。
注意:这里由于有last限定了字符串的起始位置,因此每次判断时如果mp[s[i]]在last之前,就不用更新。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
vector<int> mp(129, -1);
int len = s.length(), ans = 0, last = 0, i = 0;
for(int i=0; i<len; ++i) {
if(mp[s[i]] != -1 && mp[s[i]] >= last) {
ans = max(ans, i-last);
last = mp[s[i]] + 1;
}
mp[s[i]] = i;
}
ans = max(ans, len-last);
return ans;
}
};
leetcode 10. 正则表达式匹配
题目描述
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
递归思路
边界情况:s空p空,返回true;s不空p空,返回false;
p[j+1]为‘*’:又可以分成两种情况
- p[j]在s出现0次,这种情况直接跳过p[j]和p[j+1]即可
- p[j]在s中出现n次,且n未知。由于n未知,则可以用递归的方式判断p[j]与s[i+1]的关系。(当然,继续进行递归的前提是p[j]==s[i],否则它们一个字符也匹配不上,只能看是不是情况1)
p[j+1]不为'*':这个时候只需要判断p[j]是否等于是s[i],如果是,递归p[j+1]和s[i+1]
class Solution {
public:
bool isMatch(string s, string p) {
return myisMatch(s.c_str(), p.c_str());
}
bool myisMatch(const char* s,const char* p) {
if(*p == '\0') return *s == '\0';
bool flag = *s && (*s == *p || *p == '.');
if(*(p+1) == '*') {
return myisMatch(s, p+2) || (flag && myisMatch(++s, p));
}
return flag && myisMatch(++s, ++p);
}
};
动态规划思路
事实上动规思路和递归如出一辙,都是上述三种情况,只不过动规存储了中间结果。
这题比较容易理解的方式是从后往前递归,只需要设置好dplen1=true即可。
class Solution {
public:
bool isMatch(string s, string p) {
int len1 = s.length(), len2 = p.length();
vector<vector<bool> > dp(len1+1, vector<bool>(len2+1, false));
dp[len1][len2] = true;
for(int i=len1; i>=0; --i) {
for(int j=len2-1; j>=0; --j) {
bool flag = i < len1 && (s[i] == p[j] || p[j] == '.');
if(j < len2 -1 && p[j+1] == '*') {
dp[i][j] = dp[i][j+2] || (flag && dp[i+1][j]);
}
else {
dp[i][j] = (flag && dp[i+1][j+1]);
}
}
}
return dp[0][0];
}
};
leetcode.678 有效的括号
题目描述
给定一个只包含三种字符的字符串:( ,) 和 *,写一个函数来检验这个字符串是否为有效字符串。有效字符串具有如下规则:
- 任何左括号 ( 必须有相应的右括号 )。
- 任何右括号 ) 必须有相应的左括号 ( 。
- 左括号 ( 必须在对应的右括号之前 )。
- * 可以被视为单个右括号 ) ,或单个左括号 ( ,或一个空字符串。
- 一个空字符串也被视为有效字符串。
该题的简化版(leetcode.20)是没有*的,因此只需要简单地用一个栈判断即可。如果只有(和),那可以连栈都不用。
但这题有*,因此肯定无法用一个栈解决问题。
双栈法
此题的解法仅适用于只有'('和')'的情况。因为只有一种括号类型,因此对于号匹配没有那么严苛的要求——即不要求在判断中一定要匹配相邻的)或(。
比如:s = (()。我们在判断时并不一定要要求和第二个(匹配、)和第一个(匹配,因为当前串中,和)实际是等价的,所以我们在判断时可以先不决定是否要匹配,而可以先把能匹配)的(全部匹配完。对于剩余的(和,只需要满足(的数量不比多,且对于每一个(,都有比它位置靠后的*相对应。
因此,有了双栈解法,思路如下:
- 遇见'('将index入左栈
- 遇见'*'将index入右栈
- 遇见')'出左栈——这一步即为了先将所有的)匹配掉;如果左栈为空,出右栈——已经没有'(',自然用来替,且这些肯定都在当前')'的左边;如果右栈也空,则无法匹配,返回false
- 遍历结束后,先判断左栈是否比右栈多,如果是,则表明没有足够的*来匹配剩余的(了,不满足条件,返回false
- 如果左栈不多于右栈,那剩下的步骤只需依次判断,对于左栈的每一个index,是否有右栈的更大的index来匹配它(这时候'*'代表')',自然下标必须比'('大)
class Solution {
public:
bool checkValidString(string s) {
stack<int> st1, st2;
int len = s.length();
for(int i = 0; i < len; ++i) {
if(s[i] == '(') {
st1.push(i);
}
else if(s[i] == '*') {
st2.push(i);
}
else {
if(!st1.empty()) {
st1.pop();
}
else if(!st2.empty()) {
st2.pop();
}
else return false;
}
}
if(st1.size() > st2.size()) return false;
while(!st1.empty() && !st2.empty()) {
if(st1.top() < st2.top()) {
st1.pop();
st2.pop();
}
else return false;
}
return st1.empty();
}
};
贪心法
贪心法只允许'('剩余,且只关心了'('的最多和最少剩余个数。最多剩余意味着将所有*都看成是'(',最少剩余意味着将所有'*'看成是')'。
在这个方法中,用low标示最少剩余,用high标识最多剩余;而为了解决问题,我们对low和high的意义做进一步改动——对于最少剩余的情况,由于可能出现(太少,因此如果此时还将*当成),必然是不合法的,即此时low会小于0,对于这些low小于0的情况,我们并不关心——因为我们对low的做法是将所有*都看成(,但我们也可以将*看成空字符啊,既然low<0已经不合法了,那我们不必再考虑,直接将*忽略一次就可以了。
这里可能会有些不理解,*不是也可以当成)来处理吗?的确,但这不是low关心的,而是high关心的,low只关心最差情况,用刚才的方式(忽略low<0)剪枝后,low变相地只是在观测是否存在*过多的情况而已。也就是,在字符串遍历结束之后,如果low还大于0,那说明最少剩余的情况下,仍无法匹配所有的'(',那匹配失败。
而对于high来说,它每次都将'*'当作'(',因此在遍历结束后,它是很有可能会大于0的。但这并不影响结果,因为当high大于0时,意味这很可能有过多的*被当成'(',只要low==0,即当我们将所有*都当空串时,没有'('结余,那串就是合法的。因此,对于high,我们在结束时并不关心它的大小。但在遍历过程中,需要用high来防止出现)过多的情况(这是动态的,即到达串的某一个位置,届时的状态可能会出现')'过多)。因为当high<0时,说明即便我们将'*'都看作'(',当前也没有多余的'('来匹配')'了。
代码逻辑:
遇'(':low++; high++;
遇')':若low>0则low--; high--;
遇'*':若low>0则low--; high++;
中途若high<0返回false;结束后若low>0返回false,low==0返回true
class Solution {
public:
bool checkValidString(string s) {
int low = 0, high = 0;
int len = s.length();
for(int i=0; i<len; ++i) {
if(s[i] == '(') {
++low;
++high;
}
else if(s[i] == ')') {
if(low > 0) --low;
--high;
}
else if(s[i] == '*') {
if(low > 0) --low;
++high;
}
if(high < 0) return false;
}
return low == 0;
}
};
76.最小覆盖字串
题目描述
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
思路1:滑动窗口
left,right分别标识滑动窗口的两端,以right为基准,每次向右扩张,每扩张一次,都做一次check(),判断当前窗口是否已经包含所有t中字符,并且在满足包含的前提下,使得left即左窗口尽可能向右收缩。
class Solution {
unordered_map<char, int> mp, cnt;
public:
bool check() {
for(const auto& c : mp) {
if(c.second > cnt[c.first]) {
return false;
}
}
return true;
}
string minWindow(string s, string t) {
int len1 = s.length(), len2 = t.length();
for(int i = 0; i < len2; ++i) {
++mp[t[i]];
}
int left = 0, right = -1, ansL = -1, ansLen = INT_MAX;
while(right < len1) {
if(mp.find(s[++right]) != mp.end()) {
++cnt[s[right]];
}
while(check() && left <= right) {
if(right - left + 1 < ansLen) {
ansL = left;
ansLen = right - left + 1;
}
if(mp.find(s[left]) != mp.end()) {
--cnt[s[left]];
}
++left;
}
}
return ansL == -1 ? "" : s.substr(ansL, ansLen);
}
};
思路2:滑动窗口-check()优化
思路一有一大缺陷——每次都要进行check()判断,这个做法效率极低。可以用一个hav来记录当前窗口中含有多少个t中所需字符。每次遍历时,若当前字符在t中且当前窗口含有的该字符的数量不足(或刚好满足),则++hav。并且,当hav满(即==t.size())后,并不会因为窗口缩小、所需字符减小而减小,因为在这个方法中,当且仅当s[left]冗余时才会缩小左窗口,如果s[left]对应字符数量刚好等于必需数量,则不移动窗口。用这样的方法,很好地解决了check的效率问题。
class Solution {
public:
string minWindow(string s, string t) {
int len1 = s.length(), len2 = t.length();
unordered_map<char, int> mp, cnt;
for(const auto& c : t) {
++mp[c];
}
int left = 0, right = 0, ansL = -1, ansLen = INT_MAX, hav = 0;
while(right < len1) {
++cnt[s[right]];
if(cnt[s[right]] <= mp[s[right]]) ++hav;
while(cnt[s[left]] > mp[s[left]]) --cnt[s[left++]];
if(hav == t.size()) {
if(right - left + 1 < ansLen) {
ansLen = right - left + 1;
ansL = left;
}
}
++right;
}
return ansL == -1 ? "" : s.substr(ansL, ansLen);
}
};
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。