# 【C++】競程筆記:Stack 考題總整理 [TOC] Stack in STL: - 先引入標頭檔 `<stack>`。 - 建立一個堆疊:`stack <T> st;`。T 為資料型態。 - 堆入堆疊:`st.push(123);`。根據當初宣告的資料型態丟入對應值。 - 存取堆疊頂端:`st.top();`。回傳堆疊頂端的值。 - 移除堆疊頂端的值:`st.pop();`。 - 檢查堆疊是否為空:`st.empty();`。 - 回傳堆疊大小:`st.size()`。 :::info 需要特別注意的點:每次要做 `pop()` 的時候,都要檢查 stack 是否為空,否則會 segmentation fault。 ::: ## 1. 基礎操作 ### 輔助堆疊:155. Min Stack Problem Source:https://leetcode.com/problems/min-stack/description/ 題目有個函式叫做 `getMin()`,除 `top()` 要回傳頂端值外, `getMin()` 也要回傳堆疊中的最小值。 解法:在現有堆疊 s 的基礎上再多加一個堆疊 ms,表示要存放最小值的堆疊。 在實作 `push(int val)` 時,先 push 進去 s,之後檢查 ms 是否為空,或是 `ms.top()` 頂端值比當前 val 還大,才將 val push 進去 ms。 實作 `pop()` 時,檢查 `s.top()` 是否跟 `ms.top()` 相等(檢查要丟掉的值是否為當前最小值),是的話就做 `ms.pop()`。 `getMin()` 直接回傳 `ms.top()` 即可。 Code: ```cpp= class MinStack { private: stack <int> s, ms; public: MinStack() { } void push(int val) { s.push(val); if (ms.empty() || val <= ms.top()){ // <= 也要處理重複最小值 ms.push(val); } } void pop() { if (s.top() == ms.top()){ ms.pop(); } s.pop(); } int top() { return s.top(); } int getMin() { return ms.top(); } }; ``` ### 844. Backspace String Compare Problem Source:https://leetcode.com/problems/backspace-string-compare/ 這題用字串模擬 stack 會更好,主要利用兩個操作: - `s.push_back()` - `s.pop_back()` ```cpp= class Solution { public: string build(string str){ string result = ""; for (char c : str){ if (c != '#'){ result.push_back(c); } else{ if (!result.empty()){ result.pop_back(); } } } return result; } bool backspaceCompare(string s, string t) { return build(s) == build(t); } }; ``` ## 2. (經典必考題)符號配對與消除 只要一看到題目是有關「括號匹配」、「相鄰消除」的題目,一定要想到 Stack!!! 只要一看到題目是有關「括號匹配」、「相鄰消除」的題目,一定要想到 Stack!!! 只要一看到題目是有關「括號匹配」、「相鄰消除」的題目,一定要想到 Stack!!! 很重要所以說三次XD(老梗 主要的解題思路: 1. 遇到左括號或元素:放入 Stack。 2. 遇到右括號或相同元素:檢查 Stack 頂端是否匹配。若匹配則 `pop()`,不匹配則報錯或繼續堆疊。 ### 20. Valid Parentheses 最經典的括號配對必考題。其他題你可以不刷,但這題必須刷起來。 Problem Source:https://leetcode.com/problems/valid-parentheses/description/ 先碰左括號,將所有左括號 push 進去 stack。 之後要分別判斷三種右括號,但基本上模式都一樣,先看 stack 是不是空的,再來看頂端是否為相應的左括號。 Code: ```cpp= class Solution { public: bool isValid(string s) { stack <char> st; for (char c : s){ if (c == '(' || c == '{' || c == '['){ st.push(c); } else if (c == ')'){ if (st.empty() || st.top() != '('){ return false; } st.pop(); } else if (c == '}'){ if (st.empty() || st.top() != '{'){ return false; } st.pop(); } else if (c == ']'){ if (st.empty() || st.top() != '['){ return false; } st.pop(); } } return st.empty(); } }; ``` ### 1047. Remove All Adjacent Duplicates In String Problem Source:https://leetcode.com/problems/remove-all-adjacent-duplicates-in-string/description/ 這題屬於消除相鄰問題,若相鄰元素是相同的則消除。 解題思路是用字串當作 stack,程式中會用到的操作如下: - `str.empty();`:檢查是否為空。 - `str.back();`:作用同 `st.top();`。 - `str.push_back(x);`:作用同 `st.push(x);`。 - `str.pop_back();`:作用同 `st.pop();`。 若遇到相同的元素,也就是當 `str.back() == c`,那麼就 `pop()`。 反之,遇到不同元素就直接 `push()` 即可。 Code: ```cpp= class Solution { public: string removeDuplicates(string s) { string result = ""; for (char c : s){ if (!result.empty() && result.back() == c){ result.pop_back(); } else{ result.push_back(c); } } return result; } }; ``` ## 3. 單調堆疊(Monotonic Stack) 當題目的問題是「右邊第一個比它大的數」或「左邊第一個比它小的數」時,就是單調堆疊了。 主要解題思路:保持 Stack 內的元素是遞增或遞減。當新 push 進去的元素破壞了這個順序,就找到了某個邊界或目標值。 ### 739. Daily Temperatures Problem Source:https://leetcode.com/problems/daily-temperatures/description/ 這題被認為是練習單調堆疊的入門題。 題目給你一個陣列叫 temperatures,裡面放的是每天不同的溫度,然後求在第 i 天時,要幾天後天氣才會回溫?然後回傳一個 answer 陣列。 這題的 stack 要專門存放 index,因為題目問的是要等幾天,而不是什麼溫度之類的,所以這是一個計算距離的問題(`currIndex - prevIndex`)。 演算法流程主要跑一個 for loop,遍歷 temperatures,每次檢查 stack 是否不為空,並且今天的溫度大於 `Stack.top()`,如果成立就表示那天回溫了,然後就把那天 `pop()`、計算天數差、繼續檢查 stack 下個元素。 Code: ```cpp= class Solution { public: vector<int> dailyTemperatures(vector<int>& temperatures) { int n = temperatures.size(); vector <int> ans(n, 0); stack <int> st; for (int i = 0; i < n; ++i){ while (!st.empty() && temperatures[i] > temperatures[st.top()]){ int prevIndex = st.top(); st.pop(); ans[prevIndex] = i - prevIndex; } st.push(i); } return ans; } }; ``` ### 496. Next Greater Element I Problem Source:https://leetcode.com/problems/next-greater-element-i/description/ 這題也是單調堆疊的經典題。 題目給你兩個陣列 `num1` 跟 `num2`,`num1` 的元素會對應到 `num2`,對應完後,如果那個元素的下一個元素比他大,則回傳那個比他大的元素。~~這可能解釋的蠻爛的~~,沒關係,例子就在範例測資裡面。 `num1 = [4, 1, 2], num2 = [1, 3, 4, 2]` 首先從 4 開始,對應到 `num2[2]`,他的下一個元素是 2,沒有比他大,輸出 -1。 接下來是 1,很明顯 3 > 1,所以輸出 3。 最後是 2,而 2 沒有下一個元素,也輸出 -1。 最後得到輸出陣列 `[-1, 3, -1]`。 解題思路: 建立 `stack <int> stk` 跟 `unordered_map <int, int> nextGreater`。 `stk` 用來維護單調堆疊,在這題是單調遞減堆疊(從底部到頂部越來越小)。 `nextGreater` 用來快速建立一個查詢表,因為題目有兩個陣列,一個是專門拿來查詢的 `num1`,另一個是被查詢、拿來計算的 `num2`。 只要將有找到下個較大值的 `num`(`num1` 中的元素)紀錄在表中,之後透過 `nextGreater.count(num)` 查詢數量是否 >= 1,就可以表示有沒有該元素。 在維護單調堆疊的思路是: - 遇到小的數:直接 `push`,因為沒遇到比它大的。 - 遇到大的數:新的數是 Stack 頂部那些小數的 Next Greater Element。 - 把比目前這個數小的都 `pop`。 - 最後再把這個新的大數 `push` 進去。 Code: 若有找到下一個較大值 $x_2$ ,則將較小的值 $x_1$ 映射到那個下一個較大值 $x_2$ 。以利後續查表使用。 ```cpp= class Solution { public: vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) { stack <int> stk; unordered_map <int, int> nextGreater; for (int num : nums2){ while (!stk.empty() && stk.top() <= num){ int smallerVal = stk.top(); stk.pop(); nextGreater[smallerVal] = num; } stk.push(num); } vector <int> result; for (int num : nums1){ result.push_back(nextGreater.count(num) ? nextGreater[num] : -1); } return result; } }; ``` ### 503. Next Greater Element II Problem Source:https://leetcode.com/problems/next-greater-element-ii/description/ 是上一題的進階題,題目中從原本兩個陣列換成一個陣列,但這一個陣列是循環陣列,如 `[1, 2, 1]` 接下來會是 `[1, 2, 1, 1, 2, 1]` 結束。 遇到循環就要想到 `%` 運算子,透過 `nums[i % n]` 來存取這個循環陣列,也讓迴圈遍歷 $2 \times n$ 遍。 這邊堆疊改成堆入 index 的方式做。 Code: `if (i < n) stk.push(i);` 只將前 n 個元素的 index 放進 stack,因為第二輪的目的是從剩下的元素找答案。 ```cpp= class Solution { public: vector<int> nextGreaterElements(vector<int>& nums) { int n = nums.size(); stack <int> stk; vector <int> result(n, -1); for (int i = 0; i < 2 * n; ++i){ int num = nums[i % n]; while (!stk.empty() && nums[stk.top()] < num){ int index = stk.top(); stk.pop(); result[index] = num; } if (i < n) stk.push(i); } return result; } }; ``` ## 4. 運算原理 ### 150. Evaluate Reverse Polish Notation Problem Source:https://leetcode.com/problems/evaluate-reverse-polish-notation/description/ Reverse Polish Notation(逆波蘭表示式),簡稱 RPN。一般人在寫運算式的時候,通常會用的叫做中綴表示式(Infix Notation),就是 $3 \times 3$ ,運算子會放在兩數之間。 但是對於電腦而言,中綴表示式是比較麻煩的事情。因為有**括號**和**運算優先級**(先乘除後加減)的問題。如 $(1 + 2) \times 3$ ,電腦必須先找到括號,算完裡面的,再算外面的。 RPN 就是要解決這個問題,讓電腦好運算。RPN 的寫法就是會將運算子放在後面:`3 3 +`,就是看到 `3` 跟另一個 `3`,最後有個 `+` 就知道要把它們加起來。 而 `(1 + 2) * 3` 會寫成 `1 2 + 3 *`,就是看到 1 跟 2,後面有個 `+`,將 1 跟 2 加起來,變成 3,變成 3 之後又遇到一個 3,後面有個 `*` ,最後把兩個 3 乘起來得到 9。 RPN 的優點就是不需要括號、不需要考慮優先級,由左讀到右,看到符號就計算,因此可用 stack 實作。 解題思路: 建立 `stack <int> stk`,遍歷 `tokens`,如果遇到數字,就直接 push 進去,然後記得 `stoi(s)` 轉成整數,反之,遇到運算子就做計算,做完之後再把它 push 回去。 在最後 `return stk.top()` 即可。 Code: 記得 stack 的先進後出(FILO)特性: ```cpp= long long num2 = stk.top(); stk.pop(); long long num1 = stk.top(); stk.pop(); ``` ```cpp= class Solution { public: int evalRPN(vector<string>& tokens) { stack <int> stk; for (auto& s : tokens){ if (s == "+" || s == "-" || s == "*" || s == "/"){ long long num2 = stk.top(); stk.pop(); long long num1 = stk.top(); stk.pop(); if (s == "+") stk.push(num1 + num2); else if (s == "-") stk.push(num1 - num2); else if (s == "*") stk.push(num1 * num2); else if (s == "/") stk.push(num1 / num2); } else{ stk.push(stoi(s)); } } return stk.top(); } }; ``` ### 224. Basic Calculator Problem Source:https://leetcode.com/problems/basic-calculator/description/ 在不使用內建函式如 `eval()` (Python 的函式,可以計算字串中的運算式)的情況下,求加減、括號運算。 因為只有加減法,沒有乘除法優先級的問題,可把整個算式看成是一連串的數字加總。 比較難的地方在於**括號**跟**正負號**的問題。如 `1 - (2 + 3)`,2 + 3 的結果是正的,但括號前面有負號,所以又變成負的。 解題思路: 用兩個全域變數 `result` 跟 `sign`,分別表示計算結果跟正負符號的表示。 `sign = -1` 是負的,`sign = 1` 為正。(設成這樣是為了要讓計算結果正確) 接下來如果遇到數字,就根據 `sign` 去做運算:`result += sign * num` 若遇到 `+` 或 `-`,就更新 `sign` 變數。 遇到左括號 `(`,就先把目前的 `result` 跟 `sign` push 進去 stack,然後重置 `result = 0, sign = 1`。 遇到右括號 `)`,就表示括號裡面的算完了,而現在的 `result` 結果是括號內的計算結果,再從 stack 中取出先前存好的狀態,然後對 `result` 做相加。 Code: 以下這段是專門處理數字的部分,如果不加 while 迴圈,遇到 `s="1234+....."` 這個字串的話,會變成 `1 + 2 + 3 + 4`,而不是要專門拿來處理的 `1234` 這個數字。 最後 `i--` 是因為 while 迴圈的 `i++` 會停在運算子的位置,要把它移回來,好讓下一次 for 迴圈能判斷到。 ```cpp= if (isdigit(c)){ long long num = 0; while (i < s.length() && isdigit(s[i])){ num = num * 10 + (s[i] - '0'); i++; } i--; result += num * sign; } ``` ```cpp= class Solution { public: int calculate(string s) { stack <int> stk; int result = 0, sign = 1; for (int i = 0; i < s.length(); ++i){ char c = s[i]; if (isdigit(c)){ long long num = 0; while (i < s.length() && isdigit(s[i])){ num = num * 10 + (s[i] - '0'); i++; } i--; result += num * sign; } else if (c == '+'){ sign = 1; } else if (c == '-'){ sign = -1; } else if (c == '('){ stk.push(result); stk.push(sign); result = 0; sign = 1; } else if (c == ')'){ int prev_sign = stk.top(); stk.pop(); int prev_result = stk.top(); stk.pop(); result = prev_result + (result * prev_sign); } } return result; } }; ```