Table of contents
Why Data Structures?
When I first began solving LeetCode problems, I had very little knowledge of how to leverage data structures when solving algorithms. As a result, there were many problems labeled "Easy" that I simply could not solve. One of these problems was the well-known algorithm Valid Parentheses. In this post I'll highlight my eventual solution to this problem, and how the stack data structure helped me get to it. Example code is written in JavaScript and Ruby.
In the Valid Parentheses problem, you are asked to write an algorithm that accepts a string consisting only of opening and closing brackets ('('
, )
, '{'
, '}'
, '['
, ']'
) and return whether the input string's parentheses are in a valid order. Some examples of valid inputs include '(){}[]'
, '({})'
, '{[()()()]}'
. Invalid inputs include '{(})'
, ')('
, '[[['
.
My first attempt at solving this problem worked in the sense that I was able to return true
or false
for valid and invalid inputs, respectively. I always solve my algorithms in a REPL, so I was able to test my solution. But when I copied my solution into LeetCode and hit 'submit', I got the dreaded LeetCode Time Limt Exceeded
error. I quickly realized this was because my solution relied on a nested for
loop, and had a time complexity of a whopping O(n2) while the LeetCode tests expected O(n). Yikes! I needed to find a way to solve this problem with only one iteration of the string. So I started researching DSA topics, and learned about the stack data structure.
What Is A Stack?
A stack is a data structure closely related to Queues and Deques. The name "stack" is an appropriate one. Imagine a stack of paper. There a limited number of tasks one usually performs with a stack of paper. They typically include reading from the page at the top, removing a page from the top, and adding a new page to the top of the stack. Those same tasks are the only tasks one can perform with a stack in code.
To represent a stack with an array, all we need to do is limit ourselves to .push()
, .pop()
, and simply reading the last element of an array. The stack is often referred to as being a 'LIFO' (Last In, First Out) system, because the last element to be added is the only one which can be removed.
Thinking back to the Valid Parentheses problem, what we ultimately need to keep track of is the order of the parentheses as we iterate. More specifically, whenever we iterate over a closing bracket, we need to check if that bracket is a valid closing bracket for the most recent opening bracket. If we can do that, we can return false
if it's not. If we iterate over the whole input string and don't find any invalid closing brackets, the last thing we need to check is whether we have any unclosed opening brackets. A stack lets us do all of that in one pass. The last thing we need is to be able to know whether a given character is a closing bracket or opening bracket, for which we'll use a brackets
variable storing the string '{}[]()'
.
The Solution
JavaScript:
const isValid = s => {
const brackets = '{}[]()';
const stack = [];
for (const bracket of s) {
const bracketIdx = brackets.indexOf(bracket);
if (bracketIdx % 2 === 0) {
stack.push(bracketIdx + 1);
}
else {
if (stack.pop() !== bracketIdx) return false;
}
}
return stack.length === 0;
};
console.log(isValid('([]{})'));
// => true
console.log(isValid('([{]})'));
// => false
Ruby:
def is_valid(s)
brackets = '{}[]()'
stack = []
s.each_char do |bracket|
bracket_idx = brackets.index bracket
if bracket_idx % 2 == 0
stack << bracket_idx + 1
else
return false if stack.pop != bracket_idx
end
end
stack.size == 0
end
puts is_valid '([]{})'
# => true
puts is_valid '([{]})'
# => false
With this solution, the element in the stack's last index will always be the next valid closing bracket. By using a brackets
variable containing the string '{}[]()'
, we can always deduce whether a given bracket is an opening or closing bracket, using the indexes of the brackets
variable. Let's follow the flow with an input of '[])'
:
'['
is an opening bracket, so we store the index3
at the end of the stack. We use thebrackets
variable to find the index.']'
is a closing bracket, so we remove the last index of the stack and compare it with the our currentbracketIdx
. Since they match, we continue.')'
is a closing bracket. We remove the last index from the stack and compare it with the currentbracketIdx
. Since the stack is was empty, the last element in the stack was actuallynull
, which is not equal to ourbracketIdx
, we we returnfalse
.
By using a stack, I was able to find a solution with a time complexity of O(n), and finally got past LeetCode's Time Limit Exceeded
error. The experience of finding a new, unintuitive data structure and leveraging it to craft a solution really opened my eyes to what data structures can offer. Since my experience with Valid Parentheses, my whole approach to solving algorithms has shifted. Instead of going straight to coding when I think of a solution, I try to challenge myself to optimize.
Conclusion
Data structures can stand as powerful tools when writing algorithms. They often do not offer any extra direct functionality. A stack is just an array a programmer treats as having less capability. They do this by only using methods which add, read, or remove the last item of the array. But it's that limitation, or more specifically its implementation, that can come in handy when the order of iterated items matters in an algorithm.