All topics
20

Squeezing out duplicates in place

Tier 1Free

Where it runs

1. The problem

You have a sorted list where some values repeat: [1, 1, 2, 3, 3]. You want each value once: [1, 2, 3]. Same order, no repeats.

The simplest way is to make a new list and copy the values you keep into it. That works, but it takes twice the memory: for a while both lists sit in memory at once. Sometimes the list is huge. Sometimes it is a buffer (a stretch of memory that holds data) that the program reuses thousands of times a second. Then the second copy may cost more memory than you have.

The sorted order helps. In a sorted list, equal values always stand next to each other. So you can rewrite the list you already have and spend almost no extra memory.

2. In plain words

In a sorted list, repeats stand next to each other. You want one copy of each value, written into the same list.

Walk the list from left to right and look at every number. A number is new when it differs from the number before it. Copy each new number to the front of the list, into the first free place. Skip each repeat.

On [1, 1, 2, 3, 3] it goes like this. Keep the first 1. Skip the second 1. Write 2 into the 2nd place and 3 into the 3rd. Skip the last 3. The list now starts with 1, 2, 3, and you never needed a second list.

3. The idea

Take two markers. Both move the same way, at different speeds. The fast read marker (read) looks at every element. The slow write marker (write) remembers where the next kept value goes.

The list is sorted, so a value is new exactly when it differs from its left neighbour. So read compares the current value with the element before it. If the value is new, copy it into the write cell and move write one step forward. If it is a repeat, skip it: only read moves on.

In lesson 19 the two markers closed in from opposite ends. Here both move from left to right. The slow marker shows where to write, and it does not limit the search.

1 1 2 3 3 keep skip keep keep skip first 3 slots become [1, 2, 3], new length 3

Trace it on [1, 1, 2, 3, 3]. Start with write = 1: the first element always stays.

  • read=1: 1 == 1, a repeat, skip it.
  • read=2: 2 != 1, write 2 into cell 1, and write becomes 2. The array is now [1, 2, 2, 3, 3].
  • read=3: 3 != 2, write 3 into cell 2, and write becomes 3. The array is now [1, 2, 3, 3, 3].
  • read=4: 3 == 3, a repeat, skip it.

The first 3 cells are [1, 2, 3], and the new length is 3. Cells 3 and 4 still hold old values. Nobody checks them.

4. What it costs

Time is O(n): read visits each element once, and write only moves forward. Memory is O(1): the list is rewritten inside itself, with no extra structure.

You could put every kept value into a hash set (the Set from the lesson on sets) and build the list from it. That is also O(n) time, but it takes O(n) memory. On sorted input that memory is wasted: the comparison with the previous element tells you everything the set would.

5. Where this shows up

Analytics systems get events from at-least-once delivery. That kind of delivery guarantees an event arrives, but it sometimes sends the same event twice. Before passing a sorted buffer of events on, the system removes the repeats in place. Databases throw away rows marked for deletion during compaction (a pass that rewrites stored data more tightly). The uniq command removes repeats from a sorted stream of lines. Wherever a sorted buffer has to shrink without a second buffer, a write pointer does the job. Often this is on a hot path, that is, code that runs very often.

Remove repeats from a sorted array in place
compact([1, 1, 2, 2, 2, 3, 4]) // keep x when x !== arr[slow - 1]
arr (sorted)
1
1
2
2
2
3
4

slow
fast = -slow = 0arr[slow - 1] = -

Remove repeats from a sorted array in place. fast reads every value, slow marks the next free slot for a value we keep.

Speed race
Removing duplicates from the same 40 sorted numbers by shifting and by two pointers

Bars: the sorted numbers, most of them repeated. Both lanes keep one of each at the front and fill the freed tail with 0.

One operation is one comparison or one write. Every frame, each lane does the same number of operations, so the slow one falls behind.

Shift on every duplicate O(n²)
0 operations
Read and write pointers O(n)
0 operations

 

And with more numbers

Operations for n numbers, and roughly how long they take at 100 million simple operations a second. An estimate from the same count, not a measurement.

n = 1,000
Shift on every duplicate338.5K · 3.4 ms
Read and write pointers2K · under 1 ms
n = 100,000
Shift on every duplicate3.4B · 34 s
Read and write pointers200K · 2 ms
n = 10,000,000
Shift on every duplicate33.8T · 3.9 days
Read and write pointers20M · 200 ms

Recognise it

Two pointers (same direction)

Say these words and it is this pattern

  • remove duplicates in place, keep order
  • move/partition elements while preserving order
  • compact an array and use O(1) extra space
  • shift zeros or a target value to the end

Looks similar, isn't

n up to 1e6 gives O(n) time and O(1) space. The read pointer sees each element once.

On LeetCode

Problems on a sorted array or a string where you remove repeats or unwanted items in place and return the new length. A slow write pointer and a fast read pointer move in the same direction.

Your task (Tier 1)

Write the function dedupeSorted(nums).

The list is sorted, so equal numbers stand next to each other. Move the different numbers to the front of the list, in order, and return how many there are.

  • Input: nums, a list of numbers sorted from low to high. Example: [1, 1, 2, 3, 3].
  • Effect: the different numbers move to the front of nums, in order.
  • Return: how many different numbers there are (the new length). The tests check only the front part. They ignore whatever is left after it.

Steps:

  1. If the list is empty, return 0.
  2. Make a counter write = 1. The number at position 0 is already in place.
  3. Go through the list from position 1. When a number differs from the one right before it, copy it to nums[write] and add 1 to write.
  4. Return write.

Done when all tests pass:

  • [1, 1, 2, 3, 3] -> 3 (the front becomes 1, 2, 3)
  • [1, 2, 3] -> 3 (no repeats, nothing changes)
  • [5, 5, 5, 5] -> 1 (all the same, only the first stays)
  • [7] -> 1 (one number)
  • [] -> 0 (empty list, must not crash)

Practice

dedupeSorted

dedupeSorted(nums) -> number (new length; nums mutated in place so nums[0..len-1] is deduped)

export function dedupeSorted(nums) {
  // Your code here. The task is described in the box above the editor.
}

Running as a guest

The tests run right here, no account needed. Your code is kept in this browser only. A free account keeps it on every device, marks the step done and shows where you are in the course.

Hint ladder

Stuck? Reveal one rung at a time, but try it yourself first.

    We use necessary cookies to run the site (sign-in and language). If you accept, we also load Google Analytics to see which pages are used, and Google reCAPTCHA to keep spam off the contact and bug-report forms. Privacy policy