Problem overview
The method isValid(string input) receives a string made only of the characters (, ), {, }, [ and ]. It returns true when the brackets are valid, which needs two things: every opening bracket is closed by a bracket of the same type, and brackets are closed in the correct order, so the one opened last is closed first.
Examples: "()", "()[]{}" and "{[]}" are valid. "(]" is not, because the types differ. "([)]" is not either: each type is paired up, but the [ opened inside the parentheses is still open when the ) arrives, so the order is wrong.
Approach: a stack of unmatched opening brackets
The rule "last opened, first closed" is the behaviour of a stack. Read the string from left to right:
- An opening bracket cannot be checked yet, so push it on the stack.
- A closing bracket must match the most recent unmatched opening bracket, which is the top of the stack. If the stack is empty, there is nothing to close and the string is invalid. If the top is a different type, it is invalid. Otherwise pop the top and continue.
- At the end, the stack must be empty. Anything left on it was opened and never closed.
Trace "([)]": push (, push [. Then ) arrives and the top is [, whose partner is ], not ), so the answer is false. Trace "{[]}": push {, push [, ] matches the top [ and pops it, } matches { and pops it, and the stack ends empty, so the answer is true.
A lookup table from each opening bracket to its closing bracket keeps the matching test to one line, instead of a chain of comparisons.
A tempting alternative is to keep deleting adjacent pairs such as (), [] and {} from the string until nothing changes. It gives the right answer, but each deletion shifts the rest of the string, so it can take O(n²) time. The stack handles every character once.
Step by step
- Build the table. An
unordered_map<char, char>namedclosingBracketsmaps(to),{to}and[to]. - Create an empty
stack<char>. - Loop over the characters of
inputwith a range-basedfor. - Detect an opening bracket. A character is an opening bracket exactly when it is a key of the table, so
closingBrackets.count(bracket)is 1 for opening brackets and 0 for closing ones. - Push it when it is an opening bracket.
- Otherwise it is a closing bracket. If the stack is empty, return
false: nothing is open to close. - Read, then remove, the top. In C++,
pop()returns nothing, so copytop()intoopenBracketfirst and then callpop(). - Compare. If the current bracket is not
closingBrackets[openBracket], returnfalse. - After the loop, return
stack.empty().
The solution in C++
class Solution {
public:
bool isValid(string input) {
unordered_map<char, char> closingBrackets({
{'(', ')'},
{'{', '}'},
{'[', ']'}
});
stack<char> stack;
for (char bracket : input) {
if (closingBrackets.count(bracket)) {
stack.push(bracket);
} else {
if (stack.empty()) {
return false;
}
char openBracket = stack.top();
stack.pop();
if (bracket != closingBrackets[openBracket]) {
return false;
}
}
}
return stack.empty();
}
};
The C++ details:
pop()returnsvoid. That is why the code copiestop()into a variable before popping. Callingtop()orpop()on an empty stack is undefined behaviour, which is exactly what thestack.empty()check prevents.- The variable named
stackhides the type namestd::stackfrom that line to the end of the function. It compiles because the type is not needed again, but a name such asopenavoids the confusion. countas a condition. It returns 0 or 1 for a map, which converts tofalseortrue. WritingclosingBrackets[bracket]for the test would be a mistake, since[]inserts missing keys.- Passing by value.
string inputcopies the string. For a long string,const string&avoids that, but the solution works either way.
Complexity
Time: O(n). Each character is handled once, and every stack operation and table lookup is O(1) on average.
Space: O(n). In the worst case, a string of only opening brackets puts all n characters on the stack.
Edge cases and common mistakes
- A closing bracket first.
"]"reaches the empty-stack check and returnsfalse. Without that check,top()on an empty stack is undefined behaviour. - Unclosed brackets.
"(("never fails inside the loop, but two brackets remain, sostack.empty()at the end returnsfalse. Returningtruestraight after the loop would be wrong. - Odd length. A string with an odd number of characters can never be valid. Checking
input.size() % 2up front is an easy early exit, although the stack handles it anyway. - Counting instead of stacking. Counting opening and closing brackets of each type accepts
"([)]". Order matters, which is why a stack is needed. - Empty string. The constraints promise at least one character; if an empty string is passed, the loop does not run and the function returns
true.
Testing
"()"givestrue."()[]{}"givestrue."{[()]}"givestrue."(]"givesfalse."([)]"givesfalse."("and"]"both givefalse.
