06.04 第 038 ~ 050 题(第 13 ~ 16 天) 分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 class Solution {public : ListNode* addTwoNumbers (ListNode* l1, ListNode* l2) { ListNode* dummy = new ListNode (0 ); ListNode* cur = dummy; int carry = 0 ; while (l1 && l2){ int sum = l1->val + l2->val + carry; carry = sum / 10 ; ListNode* node = new ListNode (sum % 10 ); cur->next = node; cur = cur->next; l1 = l1->next; l2 = l2->next; } while (l1){ int sum = l1->val + carry; carry = sum / 10 ; ListNode* node = new ListNode (sum % 10 ); cur->next = node; cur = cur->next; l1 = l1->next; } while (l2){ int sum = l2->val + carry; carry = sum / 10 ; ListNode* node = new ListNode (sum % 10 ); cur->next = node; cur = cur->next; l2 = l2->next; } if (carry){ ListNode* node = new ListNode (carry); cur->next = node; } return dummy->next; } };
时间复杂度:O(max(m, n))
空间复杂度:O(1),注意返回值不计入空间复杂度。
分析 用一个辅助栈来实现最小值的存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class MinStack {public : stack<int > stk; stack<int > minStk; MinStack () { minStk.push (INT_MAX); } void push (int val) { stk.push (val); minStk.push (min (minStk.top (), val)); } void pop () { stk.pop (); minStk.pop (); } int top () { return stk.top (); } int getMin () { return minStk.top (); } };
如果我们想去掉辅助栈 minStk,可以使用一个变量来记录当前的最小值,然后在插入或删除时进行动态更新。我们可以在栈中保存上一个最小值。,从而在每次插入、删除、获取最小值时都只需要操作一个栈。以下是实现思路:
在 push 操作时,如果新插入的元素小于或等于当前最小值,则将当前最小值入栈,并更新最小值为新元素。
在 pop 操作时,如果弹出的元素等于当前最小值,则需要将最小值恢复为栈中前一个最小值(即上次的最小值)。
在 getMin 操作时直接返回当前最小值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class MinStack {public : stack<int > stk; int minVal; MinStack () { minVal = INT_MAX; } void push (int val) { if (val <= minVal) { stk.push (minVal); minVal = val; } stk.push (val); } void pop () { if (stk.top () == minVal) { stk.pop (); minVal = stk.top (); } stk.pop (); } int top () { return stk.top (); } int getMin () { return minVal; } };
分析 用栈来实现,如果是左括号就入栈,如果是右括号,检查栈顶元素是否匹配,如果不匹配,返回false。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 class Solution {public : bool isValid (string s) { stack<char > stk; for (char ch : s){ if (ch == '(' || ch == '[' || ch == '{' ){ stk.push (ch); } else { if (!stk.empty () && check (stk.top (), ch)){ stk.pop (); } else { return false ; } } } if (stk.empty ()){ return true ; } return false ; } bool check (char ch1, char ch2) { if (ch1 == '(' && ch2 == ')' ){ return true ; } if (ch1 == '[' && ch2 == ']' ){ return true ; } if (ch1 == '{' && ch2 == '}' ){ return true ; } return false ; } };
[!NOTE] 今天刷的题目都比较简单,需要注意的是最小栈那道题,在评论区看到有面试的时候问空间复杂度如何优化。
分析 用一个栈,保存这些(进行乘除运算后的)整数的值。对于加减号后的数字,将其直接压入栈中;对于乘除号后的数字,可以直接与栈顶元素计算,并替换栈顶元素为计算后的结果。
具体来说,遍历字符串 s,并用变量 preSign 记录每个数字之前的运算符,对于第一个数字,其之前的运算符视为加号。每次遍历到数字末尾时,根据 preSign 来决定计算方式:
加号:将数字压入栈;
减号:将数字的相反数压入栈;
乘除号:计算数字与栈顶元素,并将栈顶元素替换为计算结果。
代码实现中,若读到一个运算符,或者遍历到字符串末尾,即认为是遍历到了数字末尾。处理完该数字后,更新 preSign 为当前遍历的字符。
遍历完字符串 s 后,将栈中元素累加,即为该字符串表达式的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution {public : int calculate (string s) { vector<int > stk; char preSign = '+' ; int num = 0 ; int n = s.length (); for (int i = 0 ; i < n; i++){ if (isdigit (s[i])){ num = num * 10 + int (s[i] - '0' ); } if (!isdigit (s[i]) && s[i] != ' ' || i == n-1 ){ switch (preSign){ case '+' : stk.push_back (num); break ; case '-' : stk.push_back (-num); break ; case '*' : stk.back () *= num; break ; default : stk.back () /= num; } preSign = s[i]; num = 0 ; } } return accumulate (stk.begin (), stk.end (), 0 ); } };
分析 使用两个栈,inStack 用于输入,outStack 用于输出。
push:放入inStack.
pop:如果 outStack 输出栈为空,将 inStack 输入栈元素依次取出,按顺序压入 outStack 栈。这样 outStack 栈的元素顺序和之前 inStack 元素顺序相反,outStack 顶层元素就是要取出的队头元素,将其移出,并返回该元素。如果 outStack 输出栈不为空,则直接取出顶层元素。
peek:如果 outStack 输出栈为空,将 inStack 输入栈元素依次取出,按顺序压入 outStack 栈。这样 outStack 栈的元素顺序和之前 inStack 元素顺序相反,outStack 顶层元素就是要取出的队头元素,无需移出,只返回该元素。如果 outStack 输出栈不为空,则直接返回顶层元素。
empty:如果 inStack 和 outStack 都为空,则队列为空,否则队列不为空。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 class MyQueue {public : stack<int > inStack; stack<int > outStack; MyQueue () { } void push (int x) { inStack.push (x); } int pop () { if (outStack.empty ()){ while (!inStack.empty ()){ outStack.push (inStack.top ()); inStack.pop (); } } int num = outStack.top (); outStack.pop (); return num; } int peek () { int num; if (outStack.empty ()){ while (!inStack.empty ()){ outStack.push (inStack.top ()); inStack.pop (); } } return outStack.top (); } bool empty () { if (inStack.empty () && outStack.empty ()){ return true ; } return false ; } };
分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 class Solution {public : string decodeString (string s) { vector<string> cStack; stack<int > nStack; int num = 0 ; string res = "" ; for (char ch : s){ if (isdigit (ch)){ num = num * 10 + int (ch - '0' ); } else if (ch == '[' ){ cStack.push_back (res); nStack.push (num); res = "" ; num = 0 ; } else if (ch == ']' ){ string curStr = cStack.back (); cStack.pop_back (); int curNum = nStack.top (); nStack.pop (); res = curStr + copyStr (res, curNum); } else { res += ch; } } return res; } string copyStr (string &str, int n) { string result = "" ; for (int i = 0 ; i < n; ++i) { result += str; } return result; } };
动态规划
划分阶段:按照最长有效括号子串的结束位置进行阶段划分。
定义状态:dp[i]表示以s[i]为结尾的最长有效括号子串长度。
状态转移方程:
如果s[i] == '('
,则以s[i]为结尾的字符串不可能是有效括号子串,dp[i] = 0
。
如果s[i] == ')'
,需要考虑 s[i - 1] 来判断是否能够构成有效括号对:
如果s[i-1] == '(
,dp[i] 取决于「以字符 s[i - 2] 为结尾的最长有效括号长度」 + 「s[i - 1] 与 s[i] 构成的有效括号对长度(2)」,即 dp[i] = dp[i - 2] + 2
。如果i-2 < 0
,则dp[i] = 2
。
如果s[i-1] == ')'
,以 s[i - 1] 为结尾的最长有效长度为 dp[i - 1],则我们需要看 i - 1 - dp[i - 1] 位置上的字符 s[i - 1 - dp[i - 1]]是否与 s[i] 匹配。
如果 s[i - 1 - dp[i - 1]] == '('
,则说明 s[i - 1 - dp[i - 1]]
与 s[i] 相匹配,此时我们需要看以 s[i - 1 - dp[i - 1]] 的前一个字符 s[i - 2 - dp[i - 1]]
为结尾的最长括号长度是多少,将其加上 s[i - 1 - dp[i - 1]]与 s[i]
,从而构成更长的有效括号对:即dp[i] = dp[i - 1] + dp[i - dp[i - 1] - 2] + 2
,如果 s[i - dp[i - 1] - 2]
不存在,即i - dp[i - 1] - 2 < 0
,则dp[i] = dp[i-1] + 2
。
初始条件:dp[0] = 0
。
返回结果:max(dp[i])。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 class Solution {public : int longestValidParentheses (string s) { int n = s.length (); vector<int > dp (n) ; int res = 0 ; for (int i = 1 ; i < n; i++){ if (s[i] == '(' ){ dp[i] = 0 ; } else if (s[i-1 ] == '(' ){ if (i - 2 < 0 ){ dp[i] = 2 ; } else { dp[i] = dp[i-2 ] + 2 ; } } else if (i-dp[i-1 ] > 0 && s[i-dp[i-1 ]-1 ] == '(' ){ int index = i - dp[i-1 ] - 2 ; if (index < 0 ){ dp[i] = dp[i-1 ] + 2 ; } else { dp[i] = dp[index] + dp[i-1 ] + 2 ; } } res = max (res, dp[i]); } return res; } };
栈
定义一个变量 ans 用于维护最长有效括号的长度,初始时,ans = 0。
定义一个栈用于判定括号对是否匹配(栈中存储的是括号的下标),栈底元素始终保持「最长有效括号子串的开始元素的前一个元素下标」。
初始时,我们在栈中存储 -1 作为哨兵节点,表示「最长有效括号子串的开始元素的前一个元素下标为 -1」。
然后从左至右遍历字符串。
如果遇到左括号,则入栈;
如果遇到右括号,将栈顶弹出,弹出后:
如果栈为空,说明刚刚弹出的不是左括号,而是最长有效括号子串的开始元素的前一个元素下标,此时无法完成合法匹配。将当前右括号的坐标 i 压入栈中,充当「下一个有效括号子串的开始元素前一个下标」。
栈不为空,说明匹配,当前合法匹配的长度为i-stack.top()
。然后更新最大长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution {public : int longestValidParentheses (string s) { stack<int > stack; stack.push (-1 ); int res = 0 ; for (int i = 0 ; i < s.length (); i++){ if (s[i] == '(' ){ stack.push (i); } else { stack.pop (); if (stack.empty ()){ stack.push (i); } else { int len = i - stack.top (); res = max (res, len); } } } return res; } };
相向双指针 对前后缀分解方法的空间优化,某个位置能接多少水,取决于它前面的所有木板的最高值,及后面所有木板的最高值,两者之间的最小值-它自身的高度,就是这个位置能接的水。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : int trap (vector<int >& height) { int n = height.size (); int preMax = 0 ; int sufMax = 0 ; int left = 0 ; int right = n - 1 ; int res = 0 ; while (left < right){ preMax = max (preMax, height[left]); sufMax = max (sufMax, height[right]); res += preMax < sufMax ? preMax - height[left++] : sufMax - height[right--]; } return res; } };
单调栈 如果当前元素小于栈顶元素,无法接水,所以入栈;如果当前元素大于等于栈顶元素,不断出栈,出栈后如果栈为空,表明只是两根相邻的柱子,无法接水,否则可以计算面积,高度取当前栈顶(索引为left)和当前元素的较小值,宽是i - left - 1.然后将当前元素入栈。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution {public : int trap (vector<int >& height) { int ans = 0 ; stack<int > stack; for (int i = 0 ; i < height.size (); i++){ while (!stack.empty () && height[i] >= height[stack.top ()]){ int bottomHeight = height[stack.top ()]; stack.pop (); if (stack.empty ()){ break ; } int left = stack.top (); int h = min (height[left], height[i]) - bottomHeight; ans += h * (i - left - 1 ); } stack.push (i); } return ans; } };
分析 和用两个栈实现队列类似。
push:一个队列为主队列,一个为辅助队列,当入栈操作时,先将主队列内容导入辅助队列,然后将入栈元素放入主队列队头位置,再将辅助队列内容,依次添加进主队列即可。
pop:直接返回。
top:直接返回。
empty:主队列为空。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class MyStack {public : queue<int > queue1; queue<int > queue2; MyStack () { } void push (int x) { queue2.push (x); while (!queue1.empty ()){ queue2.push (queue1.front ()); queue1.pop (); } swap (queue1, queue2); } int pop () { int ans = queue1.front (); queue1.pop (); return ans; } int top () { return queue1.front (); } bool empty () { return queue1.empty (); } };
时间复杂度:入栈操作的时间复杂度为O(n)。出栈、取栈顶元素、判断栈是否为空的时间复杂度为O(1)。
空间复杂度:O(n)
对撞指针 先进行排序。固定一个元素x,然后将left指针指向它的下一个元素y,right指向末尾,三数相加大于target则向左移动right,否则向右移动left,直至两指针相撞,然后移动x,再进行对撞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : vector<vector<int >> threeSum (vector<int >& nums) { sort (nums.begin (), nums.end ()); int n = nums.size (); vector<vector<int >> ans; for (int i = 0 ; i < n-2 ; i++){ if (i > 0 && nums[i] == nums[i - 1 ]) continue ; int target = 0 - nums[i]; int left = i + 1 , right = n - 1 ; while (left < right){ int sum = nums[left] + nums[right]; if (sum == target){ ans.push_back ({nums[i], nums[left], nums[right]}); for (left += 1 ; left < right && nums[left] == nums[left-1 ]; left++); for (right -=1 ; right > left && nums[right] == nums[right+1 ]; right--); } else if (sum > target){ right--; }else { left++; } } } return ans; } };
时间复杂度:$O(n^2)$
空间复杂度:O(1)
分析 将当前数组视为哈希表。一个长度为 n 的数组,对应存储的元素值应该为 [1, n + 1] 之间,其中还包含一个缺失的元素。
遍历一遍数组,将当前元素放到其对应位置上(比如元素值为 1 的元素放到数组第 0 个位置上、元素值为 2 的元素放到数组第 1 个位置上,等等)。实际上这样操作后,比n大的元素不会被操作,或者说,当数组中存在一个比n大的元素时,必然有至少一个值小于等于n,它一定会被找到。
然后再次遍历一遍数组。遇到第一个元素值不等于下标 + 1 的元素,就是答案要求的缺失的第一个正数。
如果遍历完没有在数组中找到缺失的第一个正数,则缺失的第一个正数是 n + 1。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : int firstMissingPositive (vector<int >& nums) { int n = nums.size (); for (int i = 0 ; i < n; i++){ while (nums[i] >= 1 && nums[i] <= n && nums[i] != nums[nums[i]-1 ]){ int index1 = i; int index2 = nums[index1] - 1 ; swap (nums[index1], nums[index2]); } } for (int i = 0 ; i < n; i++){ if (nums[i] != i + 1 ){ return i + 1 ; } } return n + 1 ; } };
哈希表
先将数组存储到集合中进行去重,用变量curLen和ans记录当前序列和最长序列的长度。
遍历集合,判断当前元素num的num-1是否在集合中,如果不在集合中,说明num是序列的起始,如果在集合中,直接跳过。
对于起始元素num,判断num+1,num+2,…是否在集合中,并更新序列长度,最后更新ans。
返回ans。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : int longestConsecutive (vector<int >& nums) { int n = nums.size (); unordered_set<int > set; for (int i = 0 ; i < n; i++){ set.insert (nums[i]); } int ans = 0 ; for (const int & num : set){ if (set.count (num-1 ) == 0 ){ int curNum = num + 1 ; int curLen = 1 ; while (set.count (curNum) != 0 ){ curLen++; curNum++; } ans = max (ans, curLen); } } return ans; } };
总结 主要涉及栈、队列、哈希表等内容,注意单调栈的部分,之后需要更多的练习。
单调栈
单调栈视频讲解