Problem overview
The method isValid receives a string, input, 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 right order.
Some examples:
"()","()[]{}"and"{[]}"are valid."(]"is not: the(is closed by the wrong type."([)]"is not: both pairs have the right types, but they cross. The(would have to close before the[that was opened after it.
The statement asks for O(n) time and O(n) extra space.
Approach: the latest opening bracket must close first
Counting brackets isn't enough, because "([)]" has equal counts of every type and is still invalid. What matters is order. When a closing bracket arrives, it has to match the most recent opening bracket that is still open. That is exactly the last-in, first-out rule of a stack.
So the scan works like this:
- On an opening bracket, push it on the stack. It waits for its partner.
- On a closing bracket, pop the top of the stack and check that this opening bracket is the right partner. If the stack is empty, or the partner is wrong, the string is invalid.
- At the end, the stack must be empty. Anything left was opened and never closed.
On "{[]}": push {, push [. The ] pops [, which matches. The } pops {, which matches. The stack is empty, so the result is true. On "([)]": push (, push [. The ) pops [, whose partner is ], not ), so the result is false right away.
Step by step
- Map each opening bracket to its closing bracket. The object
closingBracketsis typed{ [key: string]: string }, an index signature meaning any string key maps to a string value. Its three entries are(to),{to}and[to]. - Create the stack: an empty
string[]. Its last element is the top, sincepushandpopwork at the end of an array. - Loop over the characters with
for (const bracket of input). - Detect opening brackets. The test
bracket in closingBracketsis true exactly when the character is a key, which means it is an opening bracket. - Push it onto the stack.
- Otherwise it is a closing bracket. The
elsebranch handles it. - Guard against an empty stack. If
stack.length === 0, returnfalse: there is no opening bracket for this one to close. - Pop the partner into
openBracketwithpop()!. The exclamation mark tells TypeScript the value isn'tundefined, which the empty check above guarantees. - Compare. If
bracketis notclosingBrackets[openBracket], returnfalse, because the types don't match. - Check what is left. After the loop, return
stack.length === 0. Leftover opening brackets make the string invalid.
The solution in TypeScript
class Solution {
isValid(input: string): boolean {
const closingBrackets: { [key: string]: string } = {
"(": ")",
"{": "}",
"[": "]",
};
const stack: string[] = [];
for (const bracket of input) {
if (bracket in closingBrackets) {
stack.push(bracket);
} else {
if (stack.length === 0) {
return false;
}
const openBracket = stack.pop()!;
if (bracket !== closingBrackets[openBracket]) {
return false;
}
}
}
return stack.length === 0;
}
}
The TypeScript details worth a closer look:
for...ofon a string. It visits the string one character at a time, which is whybracketis a one-character string. Afor...inloop would give indexes instead.- The
inoperator tests keys."(" in closingBracketsistrue, and")" in closingBracketsisfalse, because)is a value, not a key. One caution:inalso finds inherited names such as"toString". That doesn't matter here, since the input has only bracket characters. - The index signature.
{ [key: string]: string }describes an object with any number of string keys, each holding a string. That letsclosingBrackets[openBracket]use a variable as the key, and it is typedstring. - Non-null assertion. With strict null checking on,
pop()is typedstring | undefined, and!removes theundefinedpart for the compiler. It's an assurance, not a check: at run time, an empty array still givesundefined. Thestack.length === 0check above it is what makes the code safe. - Strict comparison.
!==compares two strings without conversion. - Trailing comma. The last entry of the object literal ends with a comma. That is allowed and makes adding entries cleaner.
Complexity
Time: O(n). Each character is looked at once. Pushing, popping, the key test and the object lookup are all constant time on average.
Space: O(n). In the worst case, such as a string made only of opening brackets, the stack holds every character. The object has three entries, which is O(1).
Edge cases and common mistakes
- Only a closing bracket.
")"hits the empty-stack check and returnsfalseimmediately. - Only opening brackets.
"(("passes the loop and fails the final test: the stack still holds two items. - Crossed pairs.
"([)]"is the reason the type check on the popped bracket exists. Counting only the number of each bracket would accept it. - Same count, wrong order.
")("has one of each, but the closing bracket comes first, so the stack is empty when it arrives. - Forgetting the final check. Returning
trueat the end of the loop would accept"(((". - Popping before the empty check. On an empty array,
pop()givesundefined, and the comparison afterwards would look upclosingBrackets[undefined], which isn't a bracket. The empty check comes first for a reason. - An empty string. The constraints say the input has at least one character, but the code would return
truefor"", since nothing is left open.
Testing
"()"givestrue."()[]{}"givestrue."(]"givesfalse."([)]"givesfalse."{[]}"givestrue."(("givesfalse, and")"givesfalse.