Problem overview
The method isValid receives a string input made only of the characters (, ), {, }, [ and ]. It returns true when every opening bracket is closed by a bracket of the same type, and the brackets are closed in the right order.
"()", "()[]{}" and "{[]}" are valid. "(]" is not, because the types don't match. "([)]" is not either: each bracket has a partner of the right type, but the pairs cross, and the ) arrives while the [ is still open.
Approach: the most recent opening bracket must close first
Look at a valid string such as {[()]}. The first bracket to be closed is the last one that was opened, the (. After it, the [ is the last unclosed one, and so on. Brackets nest like boxes inside boxes, and the innermost box closes first. That is the last in, first out behaviour of a stack.
So the algorithm scans the string once:
- an opening bracket is pushed on the stack and waits for its partner;
- a closing bracket must match the bracket on top of the stack, so pop it and compare;
- if the stack is empty when a closing bracket arrives, nothing is open for it, so the string is invalid;
- at the end, the stack must be empty, otherwise some opening bracket was never closed.
To compare, the code stores a map from each opening bracket to the closing one that matches it. Take "([)]": ( and [ are pushed. The ) arrives and pops [. The bracket that closes [ is ], not ), so the method returns false immediately.
Step by step
- Build the lookup map. Declare
closingBracketsas an immutableMapwithval, with one entry per opening bracket written askey -> value:'('to')','{'to'}'and'['to']'. - Create the stack. Declare
stackas avalholding an emptyscala.collection.mutable.Stack[Char](). Opening brackets wait on it, and the most recent is on top. - Loop over the characters with
for (bracket <- input). - Test for an opening bracket with
closingBrackets.contains(bracket). A character that is a key of the map is an opening bracket. - Push it with
stack.push(bracket). - Handle closing brackets in the
elsebranch, written as} else {on one line. - Reject a closing bracket with nothing open. If
stack.isEmpty, returnfalse. - Pop the partner into
val openBracket = stack.pop(). It is the only opening bracket this closing one may close. - Compare. If
bracket != closingBrackets(openBracket), returnfalse. The lookup always succeeds because everything on the stack is a key of the map. - Check for leftovers. After the loop, return
stack.isEmpty: any bracket still on the stack was never closed.
The solution in Scala
class Solution {
def isValid(input: String): Boolean = {
val closingBrackets = Map(
'(' -> ')',
'{' -> '}',
'[' -> ']'
)
val stack = scala.collection.mutable.Stack[Char]()
for (bracket <- input) {
if (closingBrackets.contains(bracket)) {
stack.push(bracket)
} else {
if (stack.isEmpty) {
return false
}
val openBracket = stack.pop()
if (bracket != closingBrackets(openBracket)) {
return false
}
}
}
return stack.isEmpty
}
}
The Scala details worth a closer look:
- Characters versus strings.
'('with single quotes is aChar, and"("with double quotes is aString. The map's keys and values and the stack's elements areChars, which matches whatfor (bracket <- input)produces: looping over aStringyields its characters. ->builds a tuple.'(' -> ')'is the pair('(', ')'), andMap(...)turns pairs into entries. The defaultMapis immutable, which suits a fixed table.- Looking up in a map.
closingBrackets(openBracket)returns the value, and throwsNoSuchElementExceptionfor a missing key. For a key that might be missing, useget, which returns anOption. Here every popped value was pushed only aftercontainssucceeded. pop()on an empty stack throws. That is why the code testsstack.isEmptybefore popping.!=compares values. ForChars,bracket != closingBrackets(openBracket)compares the characters themselves.returninsidefor. The two earlyreturn falselines leave the whole method. Scala 3.5 accepts this and prints a warning that non-local returns are no longer supported; the code still compiles and works.
Complexity
Time: O(n). Each character is visited once, and each map lookup, push and pop takes constant time on average.
Space: O(n). A string made only of opening brackets, like "(((((", puts every character on the stack.
Edge cases and common mistakes
- A closing bracket first.
")"and"]["returnfalse, thanks to the empty-stack check. - Unclosed brackets.
"(","(("and"(()"returnfalsebecause the stack isn't empty at the end. A solution that only checks the pairs it sees would accept"(("wrongly. - Extra closing brackets.
"))"returnsfalseon its first character. - Crossing pairs.
"([)]"returnsfalse. Counting the brackets of each type would say it is valid, which is why a count is not enough. - Deep nesting.
"{[()]}"returnstrue. - Odd length. A valid string always has an even length, because every bracket has a partner. The method doesn't check this first, but it gives the right answer anyway.
- The empty string. It returns
true, since the stack is empty at the end. The constraints say the input has at least one character. - Popping before checking. Calling
stack.pop()on an empty stack throws an exception instead of returningfalse.
Testing
"()"givestrue."()[]{}"givestrue."(]"givesfalse."([)]"givesfalse."{[]}"givestrue."(()"givesfalse.")("givesfalse.
