Implement arrayBalancedBrackets(str) — return whether the brackets in str are properly opened and closed in the correct order. This is the classic Valid Parentheses problem, extended to three bracket types: (), [], and {}. A string is balanced when every opener has a matching closer of the same type, every closer appears in the right order, and nothing is left open at the end. Any character that isn't a bracket is ignored, so it works on real code-like strings.
// str: string — a string that may contain (), [], {} and any other characters.
// returns: boolean
// true if every bracket is matched and correctly nested; false otherwise.
function arrayBalancedBrackets(str): boolean;
// A nested mix of all three types, each closed in the right order.
arrayBalancedBrackets('([]{})');
// → true
// The ) closes before the [ does — the nesting crosses over.
arrayBalancedBrackets('([)]');
// → false
( can only be closed by a ), never by a ] or }. So '(]' is not balanced.'([)]' is false because the ( is still open when ) arrives.'(((' is false; '' (no brackets at all) is true.')', is not balanced.'a(b)c' is true — only the brackets are checked.You'll scan a string once and decide whether its brackets open and close in the correct order, using a stack to remember what's still waiting to be closed.
Picture a set of nested boxes. You can only close a box once everything you opened inside it is already closed — you can't seal the outer box while an inner one is still open. Brackets work the same way: when you write ([]), the [ opened most recently must be the first one closed. The last thing you opened is the first thing you have to close. Your job is to read the string left to right and confirm that every closer lines up with the most recent unmatched opener of the same type, and that nothing is left dangling at the end. Characters that aren't brackets — letters, spaces, digits — don't affect the answer, so 'a(b)c' is balanced.
The phrase "last opened, first closed" is the giveaway: that is exactly how a stack behaves — last in, first out (LIFO). So keep a stack of the openers you've seen but haven't matched yet. Each time you hit an opener, push it. Each time you hit a closer, the opener it must match is whatever is on top of the stack — pop it and check the types agree. When the string ends, a balanced string has popped everything back off, so the stack must be empty.
If you don't reach for a stack, the tempting trick is to keep deleting matched pairs. A balanced string must contain (), [], or {} somewhere adjacent; remove every such pair and repeat until nothing changes. If you're left with an empty string, it was balanced.
function arrayBalancedBrackets(str) {
let prev;
let current = str;
// Strip non-brackets up front so the replace logic only sees brackets.
current = current.replace(/[^()[\]{}]/g, '');
while (prev !== current) {
prev = current;
current = current.replace('()', '').replace('[]', '').replace('{}', '');
}
return current === '';
}
This actually returns the right answer. The problem is how it gets there. Each replace rescans the whole string, and the outer loop can run once per pair removed, so on a long balanced string it does on the order of n² work. It's also fiddly: you have to strip non-brackets first, and reasoning about "repeat until stable" is harder than it looks. There's a cleaner one-pass idea hiding here.
function arrayBalancedBrackets(str) {
// Maps each closer to the opener it must match. Membership in this object
// also tells us "is this character a closer?" in one lookup.
const closerToOpener = {
')': '(',
']': '[',
'}': '{',
};
const openers = new Set(['(', '[', '{']);
// The stack holds every opener we have seen but not yet matched, with the
// most recently opened bracket on top (the end of the array).
const stack = [];
for (const char of str) {
if (openers.has(char)) {
// An opener just defers the decision: push it and move on.
stack.push(char);
} else if (char in closerToOpener) {
// A closer must match the most recently opened bracket. If the stack is
// empty there is nothing to match, and if the top is the wrong opener
// the nesting is broken — either way the string is unbalanced.
if (stack.pop() !== closerToOpener[char]) {
return false;
}
}
// Any non-bracket character is ignored.
}
// Anything still on the stack is an opener that was never closed.
return stack.length === 0;
}
module.exports = { arrayBalancedBrackets };
The shift from the naive version is that the stack lets you decide each bracket the instant you read it, in a single left-to-right pass — no rescanning. The closerToOpener map does double duty: char in closerToOpener answers "is this a closer?", and closerToOpener[char] gives the exact opener it requires. The stack.pop() handles both failure cases at once: on an empty stack pop() returns undefined, which never equals an opener, so a stray closer fails; and when the top is the wrong type, the comparison fails too.
Trace arrayBalancedBrackets('([)]'), the wrong-order case.
stack = []
'(' → opener, push → stack = ['(']
'[' → opener, push → stack = ['(', '[']
')' → closer, needs '('
→ stack.pop() is '[' → '[' !== '(' → return false
The ) arrives while [ is still on top, because [ was opened more recently than (. A valid string would have closed the [ first. The pop returns [, the map says ) needs (, the two don't match, and we return false immediately — we never even look at the trailing ].
For contrast, arrayBalancedBrackets('(((') pushes three ( and the loop ends with stack = ['(', '(', '(']. Nothing ever popped them, so stack.length === 0 is false and the result is false — three openers, no closers.
true the moment the loop finishes, '(((' passes — every bracket you saw was fine, but three are still open. The final return stack.length === 0 is what catches unclosed openers; don't drop it.')' or }{ has a closer with nothing to match. [].pop() returns undefined, and undefined !== '(', so the comparison naturally returns false — but only if you compare against the popped value. If you peek without popping, guard the empty case explicitly.'([)]' is balanced — two openers, two closers — but it isn't. Balance is about order and type, not totals. The stack is what enforces order; a counter can't.'(]'. You must check the type of the opener the closer matches, which is exactly what closerToOpener[char] gives you.'a' would try to pop the stack. Gate the closer logic behind char in closerToOpener so only real brackets touch the stack.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Implement arrayBalancedBrackets(str) — return whether the brackets in str are properly opened and closed in the correct order. This is the classic Valid Parentheses problem, extended to three bracket types: (), [], and {}. A string is balanced when every opener has a matching closer of the same type, every closer appears in the right order, and nothing is left open at the end. Any character that isn't a bracket is ignored, so it works on real code-like strings.
// str: string — a string that may contain (), [], {} and any other characters.
// returns: boolean
// true if every bracket is matched and correctly nested; false otherwise.
function arrayBalancedBrackets(str): boolean;
// A nested mix of all three types, each closed in the right order.
arrayBalancedBrackets('([]{})');
// → true
// The ) closes before the [ does — the nesting crosses over.
arrayBalancedBrackets('([)]');
// → false
( can only be closed by a ), never by a ] or }. So '(]' is not balanced.'([)]' is false because the ( is still open when ) arrives.'(((' is false; '' (no brackets at all) is true.')', is not balanced.'a(b)c' is true — only the brackets are checked.You'll scan a string once and decide whether its brackets open and close in the correct order, using a stack to remember what's still waiting to be closed.
Picture a set of nested boxes. You can only close a box once everything you opened inside it is already closed — you can't seal the outer box while an inner one is still open. Brackets work the same way: when you write ([]), the [ opened most recently must be the first one closed. The last thing you opened is the first thing you have to close. Your job is to read the string left to right and confirm that every closer lines up with the most recent unmatched opener of the same type, and that nothing is left dangling at the end. Characters that aren't brackets — letters, spaces, digits — don't affect the answer, so 'a(b)c' is balanced.
The phrase "last opened, first closed" is the giveaway: that is exactly how a stack behaves — last in, first out (LIFO). So keep a stack of the openers you've seen but haven't matched yet. Each time you hit an opener, push it. Each time you hit a closer, the opener it must match is whatever is on top of the stack — pop it and check the types agree. When the string ends, a balanced string has popped everything back off, so the stack must be empty.
If you don't reach for a stack, the tempting trick is to keep deleting matched pairs. A balanced string must contain (), [], or {} somewhere adjacent; remove every such pair and repeat until nothing changes. If you're left with an empty string, it was balanced.
function arrayBalancedBrackets(str) {
let prev;
let current = str;
// Strip non-brackets up front so the replace logic only sees brackets.
current = current.replace(/[^()[\]{}]/g, '');
while (prev !== current) {
prev = current;
current = current.replace('()', '').replace('[]', '').replace('{}', '');
}
return current === '';
}
This actually returns the right answer. The problem is how it gets there. Each replace rescans the whole string, and the outer loop can run once per pair removed, so on a long balanced string it does on the order of n² work. It's also fiddly: you have to strip non-brackets first, and reasoning about "repeat until stable" is harder than it looks. There's a cleaner one-pass idea hiding here.
function arrayBalancedBrackets(str) {
// Maps each closer to the opener it must match. Membership in this object
// also tells us "is this character a closer?" in one lookup.
const closerToOpener = {
')': '(',
']': '[',
'}': '{',
};
const openers = new Set(['(', '[', '{']);
// The stack holds every opener we have seen but not yet matched, with the
// most recently opened bracket on top (the end of the array).
const stack = [];
for (const char of str) {
if (openers.has(char)) {
// An opener just defers the decision: push it and move on.
stack.push(char);
} else if (char in closerToOpener) {
// A closer must match the most recently opened bracket. If the stack is
// empty there is nothing to match, and if the top is the wrong opener
// the nesting is broken — either way the string is unbalanced.
if (stack.pop() !== closerToOpener[char]) {
return false;
}
}
// Any non-bracket character is ignored.
}
// Anything still on the stack is an opener that was never closed.
return stack.length === 0;
}
module.exports = { arrayBalancedBrackets };
The shift from the naive version is that the stack lets you decide each bracket the instant you read it, in a single left-to-right pass — no rescanning. The closerToOpener map does double duty: char in closerToOpener answers "is this a closer?", and closerToOpener[char] gives the exact opener it requires. The stack.pop() handles both failure cases at once: on an empty stack pop() returns undefined, which never equals an opener, so a stray closer fails; and when the top is the wrong type, the comparison fails too.
Trace arrayBalancedBrackets('([)]'), the wrong-order case.
stack = []
'(' → opener, push → stack = ['(']
'[' → opener, push → stack = ['(', '[']
')' → closer, needs '('
→ stack.pop() is '[' → '[' !== '(' → return false
The ) arrives while [ is still on top, because [ was opened more recently than (. A valid string would have closed the [ first. The pop returns [, the map says ) needs (, the two don't match, and we return false immediately — we never even look at the trailing ].
For contrast, arrayBalancedBrackets('(((') pushes three ( and the loop ends with stack = ['(', '(', '(']. Nothing ever popped them, so stack.length === 0 is false and the result is false — three openers, no closers.
true the moment the loop finishes, '(((' passes — every bracket you saw was fine, but three are still open. The final return stack.length === 0 is what catches unclosed openers; don't drop it.')' or }{ has a closer with nothing to match. [].pop() returns undefined, and undefined !== '(', so the comparison naturally returns false — but only if you compare against the popped value. If you peek without popping, guard the empty case explicitly.'([)]' is balanced — two openers, two closers — but it isn't. Balance is about order and type, not totals. The stack is what enforces order; a counter can't.'(]'. You must check the type of the opener the closer matches, which is exactly what closerToOpener[char] gives you.'a' would try to pop the stack. Gate the closer logic behind char in closerToOpener so only real brackets touch the stack.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.