Hoi hoi!
Iβm @nyaomaru, a frontend engineer who struggles to make game sounds. πΏ
Have you used DSA View View already? ππ
DSA View View allows you to understand DSA by visualizing how your implementation actually runs.
In the previous article, we looked at:
- Two Sum
- Binary Search
- Bubble Sort
This time, let's try three more classic problems:
- Valid Parentheses
- Reverse Linked List
- Maximum Depth of Binary Tree
These problems introduce three very different ideas:
Stack
Pointer manipulation
Recursion
And all three can become confusing when we only stare at the final code.
So let's view what actually happens. π
I'm still learning DSA too, so let's learn together! πΈ
π₯ Valid Parentheses
Let's start with Valid Parentheses.
Suppose we have this string:
()[]{}
Every opening bracket has a matching closing bracket.
So this is valid. β
But this
([)]
is not valid. β
Why?
Because the brackets close in the wrong order.
(
[
)
]
The [ should be closed before the (.
So how can we keep track of that order?
Use a Stack
A stack follows a simple rule:
The last thing we put in is the first thing we take out.
This is called LIFO (Last In / First Out)
Imagine stacking plates.
π½οΈ β remove first
π½οΈ
π½οΈ
The last plate we put on top is the first one we can take.
Parentheses work in the same way.
If we see:
(
[
{
then the brackets must close in reverse order
}
]
)
So a stack is a very natural fit.
Here is the implementation:
function isValid(s: string): boolean {
const stack: string[] = [];
const pairs: Record<string, string> = {
")": "(",
"]": "[",
"}": "{",
};
for (const char of s) {
if (char === "(" || char === "[" || char === "{") {
stack.push(char);
continue;
}
const target = stack.pop()!;
if (target !== pairs[char]) {
return false;
}
}
return stack.length === 0;
}
The important part is what happens to stack.
Let's use
([])
At first
stack = []
We see
(
It's an opening bracket.
Push it.
stack = ["("]
Next,
[
Push again.
stack = ["(", "["]
Then,
]
This is a closing bracket.
What should it close?
[
And what is currently on top of the stack?
[
Perfect. β
So we remove it.
stack = ["("]
Finally,
)
It should close
(
The top of the stack is also
(
Pop!
stack = []
We reached the end with an empty stack.
Valid! π
What About an Invalid Example?
Consider
([)]
We start the same way.
(
β
stack = ["("]
[
β
stack = ["(", "["]
Then we find
)
A ) needs
(
But the top of our stack is
[
They don't match.
expected: (
actual: [
So we immediately know the string is invalid.
Complexity
We walk through the string once.
Time: O(n)
Space: O(n)
In the worst case, the stack may contain all opening brackets.
π Let's View View It
This is where the stack becomes much easier to understand.
When we only read.
stack.push(char);
And
stack.pop();
It can be easy to lose track of what is actually inside the stack.
Especially with something like.
({[]})
What is on top right now?
Which opening bracket are we trying to close?
Instead of remembering everything in our head, we can follow the stack changing step by step.
Conceptually, we can see:
(
β
[(]
{
β
[(, {]
[
β
[(, {, []
]
β
[(, {]
}
β
[(]
)
β
[]
That's the whole idea.
Remember the opening brackets, and always match the most recent one first.
Stack suddenly feels much less mysterious. π₯πΈ
π Reverse Linked List
Next, let's reverse a linked list.
Suppose we have
1 β 2 β 3 β 4 β 5
We want
5 β 4 β 3 β 2 β 1
At first glance, this sounds simple.
Just reverse it!
But linked lists are a little different from arrays.
With an array, the values live in positions like
0 1 2 3 4
β β β β β
1 2 3 4 5
A linked list instead consists of nodes pointing to the next node.
1 β 2 β 3 β 4 β 5 β null
Each arrow matters. To reverse the list, we need to reverse those arrows.
1 β 2 β 3 β 4 β 5
And this is where things can get confusing.
Because if we change an arrow too early...
we may lose the rest of the list. πΏ
Three Important Variables
A common iterative solution uses three variables
prev
current
next
Here is the implementation:
function reverseList(head: ListNode | null): ListNode | null {
let prev: ListNode | null = null;
let current = head;
while (current !== null) {
const next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
}
It's short.
But there is a lot happening inside these few lines.
Let's follow it carefully.
We start with
1 β 2 β 3 β null
And
prev = null
current = 1
Step 1: Save the Next Node
First
const next = current.next;
So
next = 2
Why do we need this?
Because we're about to change
1 β 2
If we change that arrow without remembering 2, we lose access to the rest of the list.
So first
Save where we need to go next.
Step 2: Reverse the Arrow
Now
current.next = prev;
Originally
1 β 2
But prev is
null
So now
1 β null
The first arrow has been reversed.
Step 3: Move prev
Next
prev = current;
So
prev = 1
Step 4: Move current
Finally
current = next;
We saved 2 earlier.
So now
current = 2
Our state looks like this
null β 1 2 β 3 β null
β β
prev current
Then we do exactly the same thing again.
Save:
next = 3
Reverse
2 β 1
Move
prev = 2
current = 3
Now
null β 1 β 2 3 β null
β β
prev current
One more time
next = null
Reverse
3 β 2
Move
prev = 3
current = null
And now
null β 1 β 2 β 3
β
prev
The loop stops because
current === null
And prev is the new head.
So
return prev;
Done! π
Complexity
We visit each node once.
Time: O(n)
Space: O(1)
We don't create another linked list.
We only move a few pointers.
π Let's View View It
This is exactly the kind of code that I find difficult to understand by reading alone.
These four lines:
const next = current.next;
current.next = prev;
prev = current;
current = next;
look simple.
But when I first see code like this, my brain starts asking.
Wait.
- Where is current now?
- Did we lose next?
- Which arrow changed?
- What exactly does prev point to?
πΏ
When we visualize each step, we can actually follow the pointers moving.
prev current
β β
null 1 β 2 β 3
βββ
null β 1 2 β 3
β β
prev current
βββ
null β 1 β 2 3
β β
prev current
βββ
null β 1 β 2 β 3
β
prev
The algorithm becomes much simpler when we stop thinking of it as four mysterious assignments.
It's really just
Save next
β
Reverse arrow
β
Move prev
β
Move current
β
Repeat
Nice! ππΈ
π³ Maximum Depth of Binary Tree
Finally, let's look at a tree.
Consider this binary tree.
3
/ \
9 20
/ \
15 7
What is its maximum depth?
The longest path from the root to a leaf contains three nodes:
3
β
20
β
15
So the answer is.
3
How can we calculate that?
Think About a Smaller Tree
Suppose we are standing at one node.
We don't really need to understand the entire tree at once.
We only need to ask.
How deep is the left subtree?
How deep is the right subtree?
Then choose the larger one.
And add 1 for the current node.
That's exactly what this implementation does.
function maxDepth(root: TreeNode | null): number {
if (root === null) {
return 0;
}
const leftDepth = maxDepth(root.left);
const rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
The important idea is
Math.max(leftDepth, rightDepth) + 1;
But recursion can feel strange.
When we call
maxDepth(root.left);
where does the current function go?
And how do all those calls eventually become one number?
Let's follow a small example.
1
/ \
2 3
/
4
We start at
1
But before 1 can know its depth, it asks its left child
maxDepth(2)
Node 2 asks
maxDepth(4)
Node 4 has no children.
So both sides eventually reach
null
And
maxDepth(null);
returns
0
Therefore node 4 can calculate
max(0, 0) + 1
= 1
Now we return to node 2.
Its left side has depth
1
Its right side is null
0
So
max(1, 0) + 1
= 2
Now return to node 1.
Eventually its right subtree also returns
1
So node 1 gets
leftDepth = 2
rightDepth = 1
And calculates
max(2, 1) + 1
= 3
Answer
3
π
The Interesting Part: Going Down and Coming Back Up
This is what makes recursion interesting.
First, the function calls go down the tree.
1
β
2
β
4
β
null
But the answers are built while returning back up.
null β 0
4 β 1
2 β 2
1 β 3
So recursion isn't only
Keep calling the same function.
There are really two directions.
Go down
β
Reach the base case
β
Return values back up
The base case here is
if (root === null) {
return 0;
}
Without it, the recursion would have no place to stop.
Complexity
Every node is visited once.
Time: O(n)
The recursive call stack depends on the height of the tree.
Space: O(h)
where h is the height of the tree.
For a balanced tree, that is roughly
O(log n)
In the worst case, if the tree looks like a linked list
1
\
2
\
3
\
4
the depth can become
O(n)
π Let's View View It
Recursion is probably my favorite example for visualization.
Because the final implementation is tiny
const leftDepth = maxDepth(root.left);
const rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
But a lot is hidden inside those function calls.
When reading the code, it can feel like
maxDepth()
inside maxDepth()
inside maxDepth()
inside maxDepth()
...
Where are we now? πΏ
When we step through the execution, we can follow both parts
Going down
1
β
2
β
4
β
null
and then
Coming back
null β 0
β
4 β 1
β
2 β 2
β
1 β 3
That makes the recursive idea much easier to see.
Ask the smaller subproblems for their answers, then use those answers to build the current answer.
π³πΈ
π§ What Did We Actually Learn?
These three problems look completely different.
But each one introduces a very useful way of thinking.
Valid Parentheses
Use a stack when the most recent item needs to be handled first.
What was the last thing I opened?
Reverse Linked List
When changing references, save what you still need before breaking the old connection.
Where do I need to go next before I change this pointer?
Maximum Depth of Binary Tree
Break a problem into smaller versions of the same problem.
Can I get the answers from my children and build my answer from them?
This is one reason I like learning these problems together.
The implementations are not very large.
But each one introduces a completely different mental model
Stack
Pointer
Recursion
And those mental models are much harder to learn than the syntax itself.
Sometimes the code tells us what happens.
But I also want to see how it happens.
I want to view it. ππ
π― Conclusion
In this article, we looked at:
- Valid Parentheses with a stack
- Reverse Linked List with pointer manipulation
- Maximum Depth of Binary Tree with recursion
And more importantly, we followed the state while each algorithm was running.
We watched change the stack.
stack.push() / stack.pop()
We watched move through a linked list.
prev
current
next
And we watched recursive calls travel down a tree and return their answers back up.
This is exactly the kind of thing I built DSA View View for.
You can write or load a TypeScript implementation, run it with your own inputs, and move backward and forward through the runtime.
If you are learning DSA too, try viewing one of these problems step by step.
Especially if a solution feels like
I understand every line individually... but somehow I still don't understand the whole thing. πΏ
Seeing the runtime may connect those pieces together.
And if there is a DSA problem you want me to cover next, please let me know in the comments!
I still have many algorithms to learn myself. πΈ
Let's train our DSA muscles together! πͺ
If you like DSA View View, please give it a star β
nyaomaru
/
dsa-view-view
DSA View View allows you to understand DSA to see the data flow. ππ Of course, it's free.
DSA View View
DSA View View turns TypeScript algorithm functions into step-by-step visual stories. ππ
Write code, run it with structured inputs, and see the arrays, matrices trees, lists, stacks, pointers, and return values move as the function executes.
It is built for those moments when reading the code is not enough and you want to view why the answer changes.
Why Try It?
-
π§ Step through real TypeScript
Paste or edit a function, validate it, then run the exact code in the browser. -
π§© Views that match the data
Arrays become bars, matrices become grids, trees become node graphs, linked lists become chains, and two-pointer area problems get their own visual view. -
π³ DSA-friendly inputs out of the box
TreeNode,ListNode,MinHeap,MaxHeap,PriorityQueue, nested arrays matrices, strings, numbers, and class-style inputs are supported without ceremony. -
π 39 built-in examples
Search by name, browseβ¦
See you in the next article!







Top comments (2)
the framing is the part i think is doing the most work here. three problems chosen so each one carries a different idea, stack then pointer then recursion, instead of three problems that happen to be on the same list. that ordering is the lesson and the visualizer is what makes it stick.
one thing i noticed running the parentheses one, and it might be the best argument for your tool that exists in the post.
the pop is written stack.pop()! and that ! tells typescript undefined cannot happen here. for the input ")" it does happen. the stack is empty, pop returns undefined, and the answer still comes out right because undefined !== "(" is true, so it returns false either way.
so the assertion is wrong and the program is correct, and nothing in the code or the types will ever tell you. but a step through view would show target = undefined sitting right there. that is a state the annotation said does not exist. watching your implementation actually run catches the thing that reading it cannot, which is exactly what you built this for.
small idea for that view: at the step where it fails, show target and pairs[char] next to each other rather than only the stack. for ([)] the thing a learner needs is which comparison disagreed, and the stack alone shows the symptom instead of the mismatch.
nice work, looking forward to the next three.
Thank you so much for such a thoughtful comment! πΈ
Type-level guarantees only go so far. What actually happens at runtime can still be different depending on the real input and state.
That is exactly one of the reasons I built DSA View View, and why I designed it so we can step through the runtime one state at a time. So Iβm really happy that this part stood out to you!
And I really like your idea about showing
targetandpairs[char]side by side at the failing step. That would make the actual mismatch much easier to understand than showing only the stack.Iβd love to try incorporating that. Thank you for the valuable feedback!
Looking forward to learning together again! πΈ