No history yet

Applications in Array Manipulation

Transcript

Beau

Alright Jo, so in the last section we kind of set the stage, right? This whole two-pointer technique... using two little index markers to crawl through data. I get the *what*, but I'm still a little fuzzy on the *why* and the *when*. Like, what are the actual problems where this thing is a game-changer?

Jo

That is the perfect question, because that's exactly where we're going. The theory is nice, but it's pretty useless without seeing it in action. And array manipulation is... it's like the natural habitat for the two-pointer technique. Let's start with a really classic one: removing duplicates from a sorted array.

Beau

Okay. So if I have an array like... I don't know... one, one, two, three, three, three, four. The goal is to end up with just one, two, three, four at the beginning of that same array?

Jo

Exactly. And we want to do it 'in-place', meaning we're not creating a whole new array to store the result. We're modifying the existing one. This is where two pointers shine. We'll use what's often called a 'slow' pointer and a 'fast' pointer.

Beau

Slow and fast... Okay, I'm picturing a tortoise and a hare situation.

Jo

It's a pretty good analogy. So, think of the 'slow' pointer—we can call it `i`—as the gatekeeper for the clean, no-duplicates-allowed section of the array. It starts at the first element. The 'fast' pointer—let's call it `j`—is the explorer. It's going to run ahead through the whole array.

Beau

Okay, so `i` is at index zero, and `j` starts at index one, and it's just gonna... what, run to the end?

Jo

Exactly. So `j` moves along, one element at a time. The rule is simple: if the element at `j` is different from the element at `i`, it means we've found a new, unique number. When that happens, we do two things: first, we move our slow pointer `i` forward one step. Then, we copy the value from `j` into the new position of `i`.

Beau

Whoa, okay, hold on. Let's use my example. `[1, 1, 2, 3, 3, 3, 4]`. So `i` is at the first `1`. `j` is at the second `1`. They're the same, so... nothing happens? `j` just keeps moving?

Jo

Precisely. `j` moves on. Now `j` is at the element `2`. Is `2` different from what's at `i`, which is `1`? Yes. So, we increment `i`—it now points to the second spot in the array—and then we copy the `2` into that spot. The array now looks like `[1, 2, 2, 3, 3, 3, 4]`. But we only care about the part up to `i`.

Beau

I see! So `i` is essentially overwriting the first duplicate `1` with the new number `2`. And our clean section is now `[1, 2]`. Okay... `j` keeps going. It lands on the first `3`. That's different from `i`'s value, which is `2`. So we move `i` forward and copy the `3` over. The clean part is `[1, 2, 3]`.

Jo

You've got it. Then `j` goes over the next two `3`s, sees they're not different from what's now at `i`'s position—which is a `3`—and does nothing. Finally, `j` hits the `4`, we do the increment-and-copy, and we're done. The slow pointer `i` has built the unique array for us at the start. The number of unique elements is just `i` plus one.

Beau

That's... really clever. Because you're only walking through the array once. If I tried to do this without this trick, I'd probably be creating a new array and checking if each element is already in there... which sounds way slower.

Jo

Exactly. This is O(n) time complexity because the fast pointer just makes one pass. And, importantly, O(1) space complexity. We didn't need any extra storage besides a couple of integer variables for our pointers.

Beau

Okay, that's one cool trick. What else is in the playbook? Are they all this 'slow runner, fast runner' model?

Jo

Not at all. Let's switch gears to a different pattern. Imagine you need to partition an array around a pivot value. Let's say, you want to move all numbers less than 10 to the left side of the array, and all numbers greater than or equal to 10 to the right side. The final order on each side doesn't matter.

Beau

This sounds familiar... is this related to sorting algorithms, like Quicksort?

Jo

It's the very heart of Quicksort's partition step! Instead of a slow and fast pointer moving in the same direction, for this we use two pointers that start at opposite ends of the array and move toward each other. A 'left' pointer starting at index zero, and a 'right' pointer starting at the very last index.

Beau

Okay, so they're walking towards each other. What are they looking for?

Jo

The left pointer moves forward as long as it sees numbers that are already in the right place—in our case, numbers less than 10. The moment it finds a number that's 10 or greater, it stops. It's found an element that's on the wrong side.

Beau

And I'm guessing the right pointer does the opposite? It moves backward looking for a number that's less than 10?

Jo

Exactly. It moves left as long as the numbers are 10 or greater. As soon as it finds a small number, it stops. Now, you have the left pointer stopped on a 'large' number and the right pointer stopped on a 'small' number. What do you think we do?

Beau

You... swap them.

Jo

You swap them. And then both pointers continue on their journey inward, looking for the next pair of misplaced elements. You keep doing this—find, find, swap, repeat—until the pointers cross each other. Once they cross, you know every element to the left of the meeting point is small and every element to the right is large. Partition complete.

Beau

That's elegant. Again, you're doing it in-place, no extra memory, and you're only making one pass through the array, just from two different directions. That's really powerful.

Jo

It is. It's a fundamental building block. And it leads to our third big example: merging two sorted arrays. Let's say you have array A and array B, both already sorted, and you want to create a new array C that contains all their elements, also sorted.

Beau

Okay, so this time we're not working in-place. We have to create a new array, C, that's big enough to hold both.

Jo

Correct. But we still use two pointers. This time, one pointer—let's call it `pA`—starts at the beginning of array A. The other, `pB`, starts at the beginning of array B.

Beau

And they're going to have a race?

Jo

Kind of a very polite race. At each step, you compare the element `pA` is pointing to with the element `pB` is pointing to. Whichever one is smaller is the next smallest element overall, right? Because both arrays are sorted.

Beau

Oh, that makes sense. Yeah. If A starts with `[3, 7]` and B starts with `[2, 8]`, the smallest of all has to be either the `3` or the `2`. It's the `2`.

Jo

So you take the `2`, copy it into the first spot in your new array C, and—this is the key—you only advance the pointer from the array you took from. So `pB` moves forward. `pA` stays put.

Beau

Ah! And now you compare again. `pA` is on `3`, and `pB` has moved to `8`. The `3` is smaller. So you copy `3` to array C, and advance `pA`. And you just keep doing that.

Jo

Until one of the pointers runs off the end of its array. Once that happens, you know that all the remaining elements in the other array are larger than anything you've added so far. So you can just copy the rest of that array over directly. And you're done. You've merged two sorted arrays in linear time.

Beau

So across all three of these—removing duplicates, partitioning, and merging—the big advantage seems to be this efficiency. You're avoiding nested loops. You're not doing a loop inside another loop, which would get really slow with big arrays. You're just... walking.

Jo

That's the core insight. By using the existing order of the data, or by cleverly creating a new order, two pointers let you solve the problem in a single pass. It turns a potential O(n-squared) problem into an O(n) solution. And in the world of algorithms, that is a massive, massive win.