DEV Community

Cover image for Is Learning DSA Boring? Let's Use DSA View View πŸ‘€πŸ‘€ (Two Sum, Binary Search, and Bubble Sort)
nyaomaru
nyaomaru

Posted on

Is Learning DSA Boring? Let's Use DSA View View πŸ‘€πŸ‘€ (Two Sum, Binary Search, and Bubble Sort)

Hoi hoi!

I’m @nyaomaru, a frontend engineer who dislikes crowded places, so I'm planning to take a quiet vacation in September. 🏝️

Have you used DSA View View already? πŸ‘€πŸ‘€

DSA View View allows you to understand DSA by visualizing how your implementation actually runs.

But just introducing the tool is not enough.

Can it actually help us understand DSA?

In this article, we'll walk through three classic problems:

  • Two Sum
  • Binary Search
  • Bubble Sort

We'll first understand the algorithm, and then see what is actually happening with DSA View View.

Let's learn together! 😸


πŸ—ΊοΈ Two Sum

Let's start with a very famous problem.

Given an array of numbers and a target value, find the indices of two numbers whose sum equals the target.

For example,

nums = [2, 7, 11, 15];
target = 9;
Enter fullscreen mode Exit fullscreen mode

The answer is

[0, 1];
Enter fullscreen mode Exit fullscreen mode

Because

2 + 7 = 9
Enter fullscreen mode Exit fullscreen mode

Simple!

So, how should we find them? πŸ€”

Brute Force

The easiest approach is probably checking every possible pair.

function twoSum(nums: number[], target: number): number[] {
  for (let i = 0; i < nums.length; i++) {
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[i] + nums[j] === target) {
        return [i, j];
      }
    }
  }

  return [];
}
Enter fullscreen mode Exit fullscreen mode

This works.

But if the array becomes large, we may need to compare a lot of pairs, right?

The time complexity is O(nΒ²)

Can we avoid checking the same values again and again?

Yes.

Let's use a Map.

function twoSum(nums: number[], target: number): number[] {
  const seen = new Map<number, number>();

  for (let i = 0; i < nums.length; i++) {
    const current = nums[i];
    const need = target - current;

    if (seen.has(need)) {
      return [seen.get(need)!, i];
    }

    seen.set(current, i);
  }

  return [];
}
Enter fullscreen mode Exit fullscreen mode

The important part is this πŸ‘‡

const need = target - current;
Enter fullscreen mode Exit fullscreen mode

Instead of asking

Which two numbers should I combine?

we ask

What number do I need to complete the target?

Let's follow the example.

At first

current = 2
target = 9

need = 9 - 2
     = 7
Enter fullscreen mode Exit fullscreen mode

Have we already seen 7?

No.

So we remember 2.

seen = {
  2 β†’ 0
}
Enter fullscreen mode Exit fullscreen mode

Next

current = 7
target = 9

need = 9 - 7
     = 2
Enter fullscreen mode Exit fullscreen mode

Have we already seen 2?

Yes! πŸ‘€πŸ‘€

seen = {
  2 β†’ 0
}
Enter fullscreen mode Exit fullscreen mode

So

return [0, 1];
Enter fullscreen mode Exit fullscreen mode

Done!

Because we only need to walk through the array once, the time complexity becomes.

Time:  O(n)
Space: O(n)
Enter fullscreen mode Exit fullscreen mode

πŸ‘€ Let's View View It

The implementation is quite small.

But when I was first learning this pattern, this part still felt a little magical.

if (seen.has(need))
Enter fullscreen mode Exit fullscreen mode
  • Where did need come from?
  • What is inside seen at this moment?
  • Why does checking the previous values solve the problem?

This is exactly where visualization helps.

With DSA View View, we can move through the runtime one step at a time and inspect how the values change.

2
↓
Need 7
↓
Remember 2
↓
7
↓
Need 2
↓
Found 2!
πŸŽ‰
Enter fullscreen mode Exit fullscreen mode

Now the Map is not just some mysterious trick.

We can actually follow the idea.

Remember what we have already seen, and check whether the value we need is there.

Nice! 😸


πŸ” Binary Search

Next is Binary Search.

Suppose we have this sorted array

[1, 3, 5, 7, 9, 11, 13]
Enter fullscreen mode Exit fullscreen mode

And we want to find

11
Enter fullscreen mode Exit fullscreen mode

Of course, we could start from 1 and check every number.

1 β†’ 3 β†’ 5 β†’ 7 β†’ 9 β†’ 11
Enter fullscreen mode Exit fullscreen mode

That works.

But Binary Search does something smarter.

Instead of checking from the beginning, it checks the middle.

[1, 3, 5, 7, 9, 11, 13]
          ↑
         mid
Enter fullscreen mode Exit fullscreen mode

Our middle value is 7.

We are looking for 11.

11 > 7
Enter fullscreen mode Exit fullscreen mode

Because the array is sorted, we already know something very useful.

Everything on the left side of 7 is also smaller than 11.

So, we don't need that half anymore. πŸ‘‹

[1, 3, 5, 7, 9, 11, 13]
             β””β”€β”€β”€β”€β”€β”€β”€β”˜
               search
Enter fullscreen mode Exit fullscreen mode

Now we check the middle of the remaining range.

[9, 11, 13]
     ↑
    mid
Enter fullscreen mode Exit fullscreen mode

And

11 === 11
Enter fullscreen mode Exit fullscreen mode

Found it! πŸŽ‰

Here is the implementation.

function binarySearch(nums: number[], target: number): number {
  let left = 0;
  let right = nums.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);

    if (nums[mid] === target) {
      return mid;
    }

    if (nums[mid] < target) {
      left = mid + 1;
    } else {
      right = mid - 1;
    }
  }

  return -1;
}
Enter fullscreen mode Exit fullscreen mode

There are three important variables.

left
right
mid
Enter fullscreen mode Exit fullscreen mode

They represent the current search range.

For our example, they start like this

left = 0
right = 6
mid = 3

[1, 3, 5, 7, 9, 11, 13]
 ↑        ↑          ↑
left     mid       right
Enter fullscreen mode Exit fullscreen mode

Because

nums[mid] < target;
Enter fullscreen mode Exit fullscreen mode

we move left.

left = mid + 1;
Enter fullscreen mode Exit fullscreen mode

Now

[1, 3, 5, 7, 9, 11, 13]
             ↑   ↑   ↑
           left mid right
Enter fullscreen mode Exit fullscreen mode

And 11 is found.

Why Is Binary Search Fast?

This is the interesting part.

Each step removes about half of the remaining candidates.

If there are 1,000 values, we don't necessarily need 1,000 checks.

It becomes roughly

1000
↓
500
↓
250
↓
125
↓
...
Enter fullscreen mode Exit fullscreen mode

That's why Binary Search has

Time:  O(log n)
Space: O(1)
Enter fullscreen mode Exit fullscreen mode

But there is one very important condition.

The data must be sorted.

Without sorted data, we cannot safely throw away half of the search range.

πŸ‘€ Let's View View It

Binary Search is one of the algorithms that made me want a visualization tool in the first place.

The code itself is short

left = mid + 1;
Enter fullscreen mode Exit fullscreen mode

or

right = mid - 1;
Enter fullscreen mode Exit fullscreen mode

Easy.

But when learning it, I sometimes found myself thinking:

Wait... which part are we searching now? 😿

When we visualize left, mid, and right, the idea becomes much easier to follow.

We are not randomly changing three numbers.

We are continuously shrinking the search area.

β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ

       ↓

        β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ

       ↓

          β–ˆβ–ˆβ–ˆ

       ↓

           β–ˆ
Enter fullscreen mode Exit fullscreen mode

That's Binary Search!

Cut the unnecessary half.

Then cut it again. And again. And again.

Until we find the answer. βœ‚οΈπŸ˜Έ


🫧 Bubble Sort

Finally, let's sort something!

Consider this array.

[5, 1, 4, 2, 8];
Enter fullscreen mode Exit fullscreen mode

We want.

[1, 2, 4, 5, 8];
Enter fullscreen mode Exit fullscreen mode

Bubble Sort repeatedly compares two neighboring values.

If they are in the wrong order, it swaps them.

Let's look at the beginning.

[5, 1, 4, 2, 8]
 ↑  ↑
Enter fullscreen mode Exit fullscreen mode

Compare

5 > 1
Enter fullscreen mode Exit fullscreen mode

So swap them.

[1, 5, 4, 2, 8]
Enter fullscreen mode Exit fullscreen mode

Next

[1, 5, 4, 2, 8]
    ↑  ↑
Enter fullscreen mode Exit fullscreen mode

Again

5 > 4
Enter fullscreen mode Exit fullscreen mode

Swap!

[1, 4, 5, 2, 8]
Enter fullscreen mode Exit fullscreen mode

And continue.

[1, 4, 5, 2, 8]
       ↑  ↑

5 > 2
Enter fullscreen mode Exit fullscreen mode

Swap!

[1, 4, 2, 5, 8]
Enter fullscreen mode Exit fullscreen mode

Eventually, larger values move toward the end of the array.

They kind of...

bubble up. 🫧

That's why it is called Bubble Sort.

Here is a simple implementation:

function bubbleSort(nums: number[]): number[] {
  for (let i = 0; i < nums.length - 1; i++) {
    for (let j = 0; j < nums.length - i - 1; j++) {
      if (nums[j] > nums[j + 1]) {
        [nums[j], nums[j + 1]] = [nums[j + 1], nums[j]];
      }
    }
  }

  return nums;
}
Enter fullscreen mode Exit fullscreen mode

We repeatedly compare

nums[j];
Enter fullscreen mode Exit fullscreen mode

And

nums[j + 1];
Enter fullscreen mode Exit fullscreen mode

and swap them when necessary.

After one full pass, the largest remaining value reaches its correct position near the end.

So on the next pass, we don't need to check that position again.

That's why the inner loop contains.

nums.length - i - 1;
Enter fullscreen mode Exit fullscreen mode

Complexity

Bubble Sort isn't very fast for large arrays.

Its time complexity is

Time:  O(nΒ²)
Space: O(1)
Enter fullscreen mode Exit fullscreen mode

So I probably won't suddenly replace production sorting with Bubble Sort tomorrow. 😸

But as a learning example, I really like it.

Why?

Because you can see the algorithm working.

πŸ‘€ Let's View View It

This is probably the most visually satisfying one of the three.

Instead of only reading

[nums[j], nums[j + 1]] = [nums[j + 1], nums[j]];
Enter fullscreen mode Exit fullscreen mode

we can follow the values moving through the array.

[5, 1, 4, 2, 8]

 ↓ swap

[1, 5, 4, 2, 8]

    ↓ swap

[1, 4, 5, 2, 8]

       ↓ swap

[1, 4, 2, 5, 8]
Enter fullscreen mode Exit fullscreen mode

Then another pass begins.

The code contains nested loops, indexes, comparisons, and swaps.

But visually, the basic rule is extremely simple:

Compare neighbors. If the left one is bigger, swap them.

Repeat. Repeat. Repeat.

Sorted! πŸŽ‰


🧠 What Did We Actually Learn?

These three problems look quite different.

But each one introduces a useful way of thinking.

Two Sum

Remember information from previous steps.

Have I already seen what I need?
Enter fullscreen mode Exit fullscreen mode

Binary Search

Use what we already know to remove impossible candidates.

Can I safely discard half of the search space?
Enter fullscreen mode Exit fullscreen mode

Bubble Sort

Break a larger problem into many small comparisons.

Are these two values in the correct order?
Enter fullscreen mode Exit fullscreen mode

This is one of the things I find interesting about learning DSA.

At first, the implementation can look like a collection of indexes, loops, conditions, and mysterious variables.

But behind the code, there is usually a much simpler idea.

And sometimes I don't fully understand that idea just by staring at the code.

I want to view it. πŸ‘€πŸ‘€


🎯 Conclusion

In this article, we looked at three classic algorithms:

  • Two Sum with a Map
  • Binary Search
  • Bubble Sort

And more importantly, we looked at how the data changes while they run.

I think this is where visualization can be especially useful.

  • Reading the final implementation tells us what the code is.
  • Stepping through it helps us understand why it works.

That's exactly why I built DSA View View.

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 taking a problem you already solved and viewing it step by step.

You may notice something you didn't notice when only reading the code. πŸ‘€

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 ⭐

GitHub logo 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

DSA View View turns TypeScript algorithm functions into step-by-step visual stories. πŸ‘€πŸ‘€

DSA View View TV

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.

DSA View View demo

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…

And my DSA View View has launched at TinyLaunch! πŸš€ Please take a loot πŸ‘‡

See you in the next article!

Top comments (0)