開啟章節選單
堆疊經典問題
常見括號匹配
給定一個包含 (
, )
, {
, }
, [
, ]
的字串,判斷這個字串中的括號是否匹配。若匹配則輸出 yes
,否則輸出 no
。
匹配的意思是每個左括號都有對應的右括號,且括號之間的內容也是匹配的,就像這樣:
() 是匹配的
([]) 是匹配的
{()} 是匹配的
{[()]} 是匹配的
{[}] 不是匹配的
{[} 不是匹配的
首先我們可以思考看看最暴力的作法怎麼做,就是一直從字串左邊往右邊掃描,遇到一個合法的括號就把它移除,直到字串變成空字串,這樣就可以確定括號是匹配的。
發現中間有一個 {} 直接消除掉
({}) -> ()
消除完後發現還有一個 () 直接消除掉
() -> ""
最後發現字串是空的,括號是匹配的
這個作法的問題在於每次消除一個括號都要從頭開始掃描一次字串,還要考慮到把字串後面的內容往前搬花費的時間,非常的慢。不過既然每次都要重新掃一次、移除字串在把後面的內容往前搬動,不如就直接邊掃邊刪除吧?
作法具體來說就是,我們可以用一個堆疊來記錄目前還沒有被匹配的括號,當掃到一個右括號時,就可以從堆疊中取出一個左括號來匹配,如果堆疊是空的,或是取出的左括號和右括號不匹配,就可以直接判定這個字串不合法。
來看看這個範例,首先我們會有一個空堆疊與一個字串 [(){[]}]
:
接著我們從左到右掃描字串,遇到左括號就放入堆疊:
繼續掃描,我們遇到的還是左括號,所以繼續放入堆疊:
再繼續掃描,遇到右括號 )
,這時候我們可以從堆疊中取出一個左括號來匹配:
匹配成功的話,就可以把左括號從堆疊中移除:
繼續掃描,遇到左括號 {
,放入堆疊:
再繼續掃描,遇到左括號 [
,放入堆疊:
再繼續掃描,遇到右括號 ]
,這時候我們可以從堆疊中取出一個左括號來匹配,配對成功,移除左括號:
再繼續掃描,遇到右括號 }
,這時候我們可以從堆疊中取出一個左括號來匹配,配對成功,移除左括號:
再繼續掃描,遇到右括號 ]
,這時候剛好把最後一個左括號取出來配對成功,移除左括號:
掃完整個字串後,堆疊是空的,這表示括號是匹配的。
剛剛作法的邏輯大致上就是這樣:
- 如果遇到左括號,就放入堆疊。
- 如果遇到右括號,就從堆疊中取出一個左括號來匹配,如果堆疊是空的,或是取出的左括號和右括號不匹配,就表示這個字串不合法。
- 如果掃描完整個字串後,堆疊是空的,就表示括號是匹配的。
這個作法的時間複雜度是 O(n),其中 n 是字串的長度,因為我們只需要掃描一次字串,並且每次操作堆疊的時間複雜度是 O(1)。
程式碼如下:
string s = "[(){[]}]"; stack<char> stk; // 掃描字串 for (char c : s) { // 遇到左括號就放入堆疊 if (c == '(' || c == '{' || c == '[') { stk.push(c); // 遇到右括號就從堆疊中取出一個左括號來匹配 } else { // 如果堆疊是空的,就表示這個字串不合法 if (stk.empty()) { cout << "no" << endl; exit(0); } char t = stk.top(); stk.pop(); // 如果取出的左括號和右括號不匹配,就表示這個字串不合法 if ( (c == ')' && t != '(') || (c == '}' && t != '{') || (c == ']' && t != '[') ) { cout << "no" << endl; exit(0); } } } if (stk.empty()) { cout << "yes" << endl; } else { cout << "no" << endl; }