[LeetCode] Valid Parenthesis String 驗證括號字串
Given a string containing only three types of characters: '(', ')' and '*', write a function to check whether this string is valid. We define the validity of a string by these rules:
- Any left parenthesis
'('
must have a corresponding right parenthesis')'
. - Any right parenthesis
')'
must have a corresponding left parenthesis'('
- Left parenthesis
'('
must go before the corresponding right parenthesis')'
. '*'
could be treated as a single right parenthesis')'
or a single left parenthesis'('
or an empty string.- An empty string is also valid.
Example 1:
Input: "()" Output: True
Example 2:
Input: "(*)" Output: True
Example 3:
Input: "(*))" Output: True
Note:
- The string size will be in the range [1, 100].
這道題讓我們驗證括號字串,跟之前那道Valid Parentheses有些類似。不同之處在於這道題不只有小括號,還存在星號,星號可以當左括號,右括號,或空來使用,問我們能不能得到一個合法的括號字串。那麼我們想,如果不存在星號,那麼這題是不是異常的簡單,我們甚至連stack都可以不用,直接用一個變數,遇到左括號,自增1,遇到右括號,如果此時計數器已經為0了,直接返回false,否則自減1,一旦計數器出現了負數,立即返回false,最後還要看變數是否為0即可。但是由於星號的存在,這道題就變的複雜了,由於星號可以當括號用,所以當遇到右括號時,就算此時變數為0,也可以用星號來當左括號使用。那麼星號什麼時候都能當括號來用嗎,我們來看兩個例子 *) 和 *( ,在第一種情況下,星號可以當左括號來用,而在第二種情況下,無論星號當左括號,右括號,還是空,*( 都是不對的。當然這種情況只限於星號和左括號之間的位置關係,而只要星號在右括號前面,就一定可以消掉右括號。那麼我們使用兩個stack,分別存放左括號和星號的位置,遍歷字串,當遇到星號時,壓入星號棧star,當遇到左括號時,壓入左括號棧left,當遇到右括號時,此時如果left和star均為空時,直接返回false;如果left不為空,則pop一個左括號來抵消當前的右括號;否則從star中取出一個星號當作左括號來抵消右括號。當迴圈結束後,我們希望left中沒有多餘的左括號,就算有,我們可以嘗試著用星號來抵消,當star和left均不為空時,進行迴圈,如果left的棧頂左括號的位置在star的棧頂星號的右邊,那麼就組成了 *( 模式,直接返回false;否則就說明星號可以抵消左括號,各自pop一個元素。最終退出迴圈後我們看left中是否還有多餘的左括號,沒有就返回true,否則false,參見程式碼如下:
解法一:
class Solution { public: bool checkValidString(string s) { stack<int> left, star; for (int i = 0; i < s.size(); ++i) { if (s[i] == '*') star.push(i); else if (s[i] == '(') left.push(i); else { if (left.empty() && star.empty()) return false; if (!left.empty()) left.pop(); else star.pop(); } } while (!left.empty() && !star.empty()) { if (left.top() > star.top()) return false; left.pop(); star.pop(); } return left.empty(); } };
如果你覺得上面的解法邏輯稍稍複雜了一些,我們來看一種邏輯無比簡單的解法。既然星號可以當左括號和右括號,那麼我們就正反各遍歷一次,正向遍歷的時候,我們把星號都當成左括號,此時用經典的驗證括號的方法,即遇左括號計數器加1,遇右括號則自減1,如果中間某個時刻計數器小於0了,直接返回false。如果最終計數器等於0了,我們直接返回true,因為此時我們把星號都當作了左括號,可以跟所有的右括號抵消。而此時就算計數器大於0了,我們暫時不能返回false,因為有可能多餘的左括號是星號變的,星號也可以表示空,所以有可能不多,我們還需要反向q一下,哦不,是反向遍歷一下,這是我們將所有的星號當作右括號,遇右括號計數器加1,遇左括號則自減1,如果中間某個時刻計數器小於0了,直接返回false。遍歷結束後直接返回true,這是為啥呢?此時計數器有兩種情況,要麼為0,要麼大於0。為0不用說,肯定是true,為啥大於0也是true呢?因為之前正向遍歷的時候,我們的左括號多了,我們之前說過了,多餘的左括號可能是星號變的,也可能是本身就多的左括號。本身就多的左括號這種情況會在反向遍歷時被檢測出來,如果沒有檢測出來,說明多餘的左括號一定是星號變的。而這些星號在反向遍歷時又變做了右括號,最終導致了右括號有剩餘,所以當這些星號都當作空的時候,左右括號都是對應的,即是合法的。你可能會有疑問,右括號本身不會多麼,其實不會的,如果多的話,會在正向遍歷中被檢測出來,參見程式碼如下:
解法二:
class Solution { public: bool checkValidString(string s) { int left = 0, right = 0, n = s.size(); for (int i = 0; i < n; ++i) { if (s[i] == '(' || s[i] == '*') ++left; else --left; if (left < 0) return false; } if (left == 0) return true; for (int i = n - 1; i >= 0; --i) { if (s[i] == ')' || s[i] == '*') ++right; else --right; if (right < 0) return false; } return true; } };
下面這種方法是用遞迴來寫的,思路特別的簡單直接,感覺應該屬於暴力破解法。使用了變數cnt來記錄左括號的個數,變數start表示當前開始遍歷的位置,那麼在遞迴函式中,首先判斷如果cnt小於0,直接返回false。否則進行從start開始的遍歷,如果當前字元為左括號,cnt自增1;如果為右括號,若cnt此時小於等於0,返回false,否則cnt自減1;如果為星號,我們同時遞迴三種情況,分別是當星號為空,左括號,或右括號,只要有一種情況返回true,那麼就是true了。如果迴圈退出後,若cnt為0,返回true,否則false,參見程式碼如下:
解法三:
class Solution { public: bool checkValidString(string s) { return helper(s, 0, 0); } bool helper(string s, int start, int cnt) { if (cnt < 0) return false; for (int i = start; i < s.size(); ++i) { if (s[i] == '(') { ++cnt; } else if (s[i] == ')') { if (cnt <= 0) return false; --cnt; } else { return helper(s, i + 1, cnt) || helper(s, i + 1, cnt + 1) || helper(s, i + 1, cnt - 1); } } return cnt == 0; } };
下面這種解法是論壇上排第一的解法,感覺思路清新脫俗,博主研究了好久,參考了網友的留言才稍稍弄懂了一些,這裡維護了兩個變數low和high,其中low表示在有左括號的情況下,將星號當作右括號時左括號的個數(這樣做的原因是儘量不多增加右括號的個數),而high表示將星號當作左括號時左括號的個數。是不是很繞,沒辦法。那麼當high小於0時,說明就算把星號都當作左括號了,還是不夠抵消右括號,返回false。而當low大於0時,說明左括號的個數太多了,沒有足夠多的右括號來抵消,返回false。那麼開始遍歷字串,當遇到左括號時,low和high都自增1;當遇到右括號時,只有當low大於0時,low才自減1,保證了low不會小於0,而high直接自減1;當遇到星號時,只有當low大於0時,low才自減1,保證了low不會小於0,而high直接自增1,因為high把星號當作左括號。當此時high小於0,說明右括號太多,返回false。當迴圈退出後,我們看low是否為0,參見程式碼如下:
解法四:
class Solution { public: bool checkValidString(string s) { int low = 0, high = 0; for (char c : s) { if (c == '(') { ++low; ++high; } else if (c == ')') { if (low > 0) --low; --high; } else { if (low > 0) --low; ++high; } if (high < 0) return false; } return low == 0; } };
類似題目:
參考資料: