Problem overview
You get a string input made only of the characters (, ), {, }, [ and ]. The function returns true if the brackets are valid, which needs two things:
- every opening bracket is closed by a bracket of the same type;
- brackets are closed in the correct order, so the most recently opened one is closed first.
So "()", "()[]{}" and "{[]}" are valid. "(]" is not, because the types differ. "([)]" is not either: each bracket has a partner of the right type, but the ) arrives while [ is still open, so the order is wrong.
The lesson asks for O(n) time and O(n) extra space.
Approach
Read the string from left to right and ask one question at each closing bracket: which opening bracket does it have to close? It has to be the most recent one that is still open. "The most recent one that is still open" is exactly what the top of a stack gives you:
- an opening bracket is pushed on the stack and waits for its partner;
- a closing bracket pops the top and must match it;
- at the end, nothing may be left on the stack.
Each character is handled once, with constant-time stack operations, so the time is O(n). In the worst case, for example a string of only opening brackets, the stack holds all n characters, so the space is O(n).
Step by step
- A map of pairs.
closingBracketsis a read-only map from each opening bracket to the closing bracket that matches it.mapOfbuilds it from pairs written askey to value. The map also serves as the test for "is this an opening bracket?": opening brackets are its keys. - A stack.
stackis an emptyArrayDeque<Char>. The end of the deque is the top of the stack. It holds opening brackets that still wait for their partner. - Visit each character with
for (bracket in input). - Is it an opening bracket?
closingBrackets.containsKey(bracket)is true for(,{and[. - Push it with
stack.addLast(bracket). - Otherwise it is a closing bracket. The
elsebranch deals with it. - Nothing to close. If
stack.isEmpty(), a closing bracket has no opening partner, as in")", so returnfalse. - Pop the partner.
stack.removeLast()returns the most recent opening bracket, the only one this closing bracket is allowed to close. It goes intoopenBracket. - Compare. The expected closing bracket is
closingBrackets[openBracket]. If the currentbracketdiffers from it, returnfalse. - Leftovers. After the loop,
stack.isEmpty()tells whether every opening bracket got closed. Return that value directly.
The solution in Kotlin
class Solution {
fun isValid(input: String): Boolean {
val closingBrackets = mapOf(
'(' to ')',
'{' to '}',
'[' to ']'
)
val stack = ArrayDeque<Char>()
for (bracket in input) {
if (closingBrackets.containsKey(bracket)) {
stack.addLast(bracket)
} else {
if (stack.isEmpty()) {
return false
}
val openBracket = stack.removeLast()
if (bracket != closingBrackets[openBracket]) {
return false
}
}
}
return stack.isEmpty()
}
}
Kotlin details worth knowing:
'('with single quotes is aChar;"("with double quotes is aString. The map and the stack both useChar, and iterating over aStringgivesChars.a to bcreates aPair.mapOf(...)returns a read-onlyMap<Char, Char>, which is all this code needs because the pairs never change.ArrayDequeis Kotlin's own class and needs no import.addLastpushes andremoveLast()pops at the same end.closingBrackets[openBracket]has the typeChar?, because a map lookup givesnullfor a missing key. Comparing aCharwith aChar?using!=is allowed, andnullis simply never equal to a real character. For this input the key is always present, because only opening brackets are pushed.removeLast()on an emptyArrayDequethrowsNoSuchElementException. TheisEmpty()check before it is what prevents that.
Complexity
Time: O(n). Each character is pushed at most once and popped at most once, and each push, pop and map lookup takes constant time on average.
Space: O(n). The stack grows with the number of unclosed opening brackets, up to n for a string such as "(((((".
Edge cases and common mistakes
- Only a closing bracket. For
")"the stack is empty when the closing bracket arrives, and the function returnsfalse. Without the empty check,removeLast()would throw. - Unclosed brackets. For
"(("the loop ends without a mismatch, but the stack still holds two items. The finalstack.isEmpty()catches this; returningtrueat the end would be wrong. - Right types, wrong order. For
"([)]"the)pops[, which does not match, so the answer isfalse. - Odd length. A string of odd length can never be valid, because brackets come in pairs. The stack discovers this on its own, but you could return
falseearly as a shortcut. - Counting instead of stacking. A counter per bracket type accepts
"([)]", since it ignores order. The stack is what enforces the order.
Testing
"()","()[]{}"and"{[]}"returntrue."(]","([)]",")"and"(("returnfalse.