Read/Write Pointers (In-Place Array Modification, Dutch National Flag)
You're given an already-sorted (smallest to largest) integer array nums. Rewrite it in-place so every value appears just once, keeping the surviving values in their original relative order.
Say k is the count of distinct values in nums. Once the duplicates are gone, hand back that count k, and make sure the first k slots of nums hold those distinct values in sorted order — whatever sits past index k - 1 is never inspected.
Custom Judge:
We compare the k you return with the length of the expected trimmed array, then check that each of the first k slots of nums holds the expected value for that position. Anything after slot k - 1 is ignored.
Read-Write Pointers
reader -> writer -> (Compaction Frontier)| 1 | from typing import Callable, List, TypeVar |
| 2 | Â |
| 3 | Item = TypeVar("Item") |
| 4 | Â |
| 5 | Â |
| 6 | def read_and_write(items: List[Item], keep: Callable[[Item, int], bool]) -> int: |
| 7 | """Read-Write Pointers: move every kept item to the front of items, in order, in place. |
| 8 | keep(value, writer) returns True to keep value. writer is the number of items kept so far. |
| 9 | items[:writer] holds those items, so keep may compare value with items[writer - 1]. |
| 10 | Precondition: keep reads only value and items[:writer], never a slot at writer or later. |
| 11 | Returns writer. The slots from writer on still hold old values. Move Zeroes writes its zeros there afterwards. |
| 12 | N = len(items). T = the cost of one keep call. |
| 13 | Time: O(N * T) | Space: O(1) |
| 14 | """ |
| 15 | writer = 0 # items[:writer] holds the kept items, in order |
| 16 | # 1. Read Every Item, Write Only The Kept Ones: |
| 17 | # writer never gets ahead of reader, so items[writer] = items[reader] always lands on a slot whose old value has already been read, never one still waiting to be read. |
| 18 | for reader in range(len(items)): |
| 19 | if keep(items[reader], writer): |
| 20 | items[writer] = items[reader] |
| 21 | writer += 1 |
| 22 | Â |
| 23 | return writer # The number of kept items |
nums[0:writer] holds, in order, exactly the kept elements among nums[0:reader].writer marks where the next kept element goes; everything before it is final. reader visits every element and copies it to writer only when it should be kept.
Since writer never passes reader, a write never lands on an element that still has to be read, so the array can be rewritten in place. For sorted deduplication, start both at 1, because nums[0] is always kept.
The trap is comparing with the wrong slot: check nums[reader] against nums[writer - 1], the last element kept, not nums[writer], the next free slot, which still holds an old value; on LeetCode Example 2 of Remove Duplicates from Sorted Array (LC 26), [0,0,1,1,1,2,2,3,3,4], the wrong slot keeps 7 values instead of 5.
See it run
Read-Write Pointers, step by step
reader 1writer 1
Each box is one entry of nums, with its index above it. Copies are made inside the same array.
kepta final value before writerwriterthe next free slotreaderthe entry being readreader, writerreader and writer on the same entry
Start: nums[0] is always kept, so writer = 1 and reader = 1. Everything before writer is final.
Gotcha 1 of 4
- The trap
Gotcha 1: Comparing with
items[writer]instead ofitems[writer - 1]Mistakeif nums[reader] != nums[writer]:Fixif nums[reader] != nums[writer - 1]:What happens:
nums[writer - 1]is the last value kept.nums[writer]is the next free slot. It still holds an old value from the input. Comparing with that old value can keep a duplicate. It can also drop a value that is new.Example: Remove Duplicates from Sorted Array (LC 26), Example 2: nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]Mistake: returns 7Fix: returns 5
- You compare or pair elements from the two opposite ends, as in Valid Palindrome (LC 125), instead of keeping or dropping each element in order. See Converging Pointers
- You need one value at a given rank, as in Kth Largest Element in an Array (LC 215), not the array compacted or split by a fixed rule. See Quickselect
Theory
The question we build
What we know
nums is sorted in ascending order.
Equal values are always adjacent, so a value only needs to be compared with the last kept value to know whether it is a duplicate.
What the problem gives us
The input is nums, modified in place.
Return the number of unique values, with that many unique values occupying the front of nums, in order.
The question we build
For each position reader, we ask: "does nums[reader] equal the last value already kept?"
If it is new, where should it go?
It goes right after the last kept value, at the first free slot.
Narrowing down to the template
Is nums[reader] kept, and where does it go?
The first free slot is writer, and writer never gets ahead of reader, so a write never overwrites a value that still has to be read.
So the pass can run on nums itself, with no second array: writer only ever moves forward, one slot at a time, never back.
That is exactly Read-Write Pointers: reader visits every value, and writer marks the boundary between the kept values and the rest.
Step 1: A fresh list
Build the kept values into a new list. Correct: each new value is compared with the new list's last entry. States the contract in one listing: keep a value only when it differs from the one before it, then copy the result back.
| 1 | def remove_duplicates_brute(nums): |
| 2 | if not nums: |
| 3 | return 0 |
| 4 | kept = [nums[0]] # A fresh list, not written in place |
| 5 | for i in range(1, len(nums)): |
| 6 | if nums[i] != kept[-1]: |
| 7 | kept.append(nums[i]) |
| 8 | for i, value in enumerate(kept): |
| 9 | nums[i] = value # Copy the fresh list back over nums |
| 10 | return len(kept) |
Cost: O(N) time, O(N) extra space for the fresh list.
Step 2: Compact in place
The fresh list's last entry is always nums[writer - 1], since writer counts the values kept so far.
So there is no need for a second list: write each kept value straight into nums[writer], because writer never gets ahead of reader.
Comparing against nums[writer] instead of nums[writer - 1] reads a slot that still holds an old, unread value.
| 1 | New line: def removeDuplicates(nums: List[int]) -> int: |
| 2 | if not nums: |
| 3 | return 0 |
| 4 | |
| 5 | New line: writer = 1 # Seed: nums[0] is unconditionally unique |
| 6 | New line: # 1. Read Every Item, Write Only The Kept Ones: |
| 7 | New line: # nums is sorted, so a value is new exactly when it differs from nums[writer - 1], the last value kept; writer never gets ahead of reader, so the write never overwrites an unread value. |
| 8 | New line: for reader in range(1, len(nums)): |
| 9 | New line: if nums[reader] != nums[writer - 1]: |
| 10 | New line: nums[writer] = nums[reader] |
| 11 | New line: writer += 1 |
| 12 | |
| 13 | New line: return writer # nums[0..writer-1] holds compacted unique elements |
Cost: O(N) time, O(1) extra space.
Step 3: The named template
The loop does not change between problems.
Only keep changes.
| 1 | New line: from typing import Callable, List, TypeVar |
| 2 | |
| 3 | New line: Item = TypeVar("Item") |
| 4 | |
| 5 | New line: def read_and_write(items: List[Item], keep: Callable[[Item, int], bool]) -> int: |
| 6 | New line: """Read-Write Pointers: move every kept item to the front of items, in order, in place. |
| 7 | New line: keep(value, writer) returns True to keep value. writer is the number of items kept so far. |
| 8 | New line: items[:writer] holds those items, so keep may compare value with items[writer - 1]. |
| 9 | New line: Precondition: keep reads only value and items[:writer], never a slot at writer or later. |
| 10 | New line: Returns writer. The slots from writer on still hold old values. Move Zeroes writes its zeros there afterwards. |
| 11 | New line: N = len(items). T = the cost of one keep call. |
| 12 | New line: Time: O(N * T) | Space: O(1) |
| 13 | New line: """ |
| 14 | New line: writer = 0 # items[:writer] holds the kept items, in order |
| 15 | # 1. Read Every Item, Write Only The Kept Ones: |
| 16 | New line: # writer never gets ahead of reader, so items[writer] = items[reader] always lands on a slot whose old value has already been read, never one still waiting to be read. |
| 17 | New line: for reader in range(len(items)): |
| 18 | New line: if keep(items[reader], writer): |
| 19 | New line: items[writer] = items[reader] |
| 20 | writer += 1 |
| 21 | |
| 22 | return writer # The number of kept items |
Cost: O(N * T), where T is the cost of one keep call.
| Before | After | What changed |
|---|---|---|
if nums[reader] != nums[writer - 1]: | if keep(items[reader], writer): | The comparison moves inside keep, which also receives writer so it can look at items[writer - 1]. |
nums[writer] = nums[reader] | items[writer] = items[reader] | Same write, generic names. |
writer += 1 | writer += 1 | Identical. |
return writer | return writer | Identical. |
Practice
Read/Write Pointers (In-Place Array Modification, Dutch National Flag)
You're given an already-sorted (smallest to largest) integer array nums. Rewrite it in-place so every value appears just once, keeping the surviving values in their original relative order.
Say k is the count of distinct values in nums. Once the duplicates are gone, hand back that count k, and make sure the first k slots of nums hold those distinct values in sorted order — whatever sits past index k - 1 is never inspected.
Custom Judge:
We compare the k you return with the length of the expected trimmed array, then check that each of the first k slots of nums holds the expected value for that position. Anything after slot k - 1 is ignored.
Read-Write Pointers
reader -> writer -> (Compaction Frontier)| 1 | from typing import Callable, List, TypeVar |
| 2 | Â |
| 3 | Item = TypeVar("Item") |
| 4 | Â |
| 5 | Â |
| 6 | def read_and_write(items: List[Item], keep: Callable[[Item, int], bool]) -> int: |
| 7 | """Read-Write Pointers: move every kept item to the front of items, in order, in place. |
| 8 | keep(value, writer) returns True to keep value. writer is the number of items kept so far. |
| 9 | items[:writer] holds those items, so keep may compare value with items[writer - 1]. |
| 10 | Precondition: keep reads only value and items[:writer], never a slot at writer or later. |
| 11 | Returns writer. The slots from writer on still hold old values. Move Zeroes writes its zeros there afterwards. |
| 12 | N = len(items). T = the cost of one keep call. |
| 13 | Time: O(N * T) | Space: O(1) |
| 14 | """ |
| 15 | writer = 0 # items[:writer] holds the kept items, in order |
| 16 | # 1. Read Every Item, Write Only The Kept Ones: |
| 17 | # writer never gets ahead of reader, so items[writer] = items[reader] always lands on a slot whose old value has already been read, never one still waiting to be read. |
| 18 | for reader in range(len(items)): |
| 19 | if keep(items[reader], writer): |
| 20 | items[writer] = items[reader] |
| 21 | writer += 1 |
| 22 | Â |
| 23 | return writer # The number of kept items |
nums[0:writer] holds, in order, exactly the kept elements among nums[0:reader].writer marks where the next kept element goes; everything before it is final. reader visits every element and copies it to writer only when it should be kept.
Since writer never passes reader, a write never lands on an element that still has to be read, so the array can be rewritten in place. For sorted deduplication, start both at 1, because nums[0] is always kept.
The trap is comparing with the wrong slot: check nums[reader] against nums[writer - 1], the last element kept, not nums[writer], the next free slot, which still holds an old value; on LeetCode Example 2 of Remove Duplicates from Sorted Array (LC 26), [0,0,1,1,1,2,2,3,3,4], the wrong slot keeps 7 values instead of 5.
See it run
Read-Write Pointers, step by step
reader 1writer 1
Each box is one entry of nums, with its index above it. Copies are made inside the same array.
kepta final value before writerwriterthe next free slotreaderthe entry being readreader, writerreader and writer on the same entry
Start: nums[0] is always kept, so writer = 1 and reader = 1. Everything before writer is final.
Gotcha 1 of 4
- The trap
Gotcha 1: Comparing with
items[writer]instead ofitems[writer - 1]Mistakeif nums[reader] != nums[writer]:Fixif nums[reader] != nums[writer - 1]:What happens:
nums[writer - 1]is the last value kept.nums[writer]is the next free slot. It still holds an old value from the input. Comparing with that old value can keep a duplicate. It can also drop a value that is new.Example: Remove Duplicates from Sorted Array (LC 26), Example 2: nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]Mistake: returns 7Fix: returns 5
- You compare or pair elements from the two opposite ends, as in Valid Palindrome (LC 125), instead of keeping or dropping each element in order. See Converging Pointers
- You need one value at a given rank, as in Kth Largest Element in an Array (LC 215), not the array compacted or split by a fixed rule. See Quickselect
Theory
The question we build
What we know
nums is sorted in ascending order.
Equal values are always adjacent, so a value only needs to be compared with the last kept value to know whether it is a duplicate.
What the problem gives us
The input is nums, modified in place.
Return the number of unique values, with that many unique values occupying the front of nums, in order.
The question we build
For each position reader, we ask: "does nums[reader] equal the last value already kept?"
If it is new, where should it go?
It goes right after the last kept value, at the first free slot.
Narrowing down to the template
Is nums[reader] kept, and where does it go?
The first free slot is writer, and writer never gets ahead of reader, so a write never overwrites a value that still has to be read.
So the pass can run on nums itself, with no second array: writer only ever moves forward, one slot at a time, never back.
That is exactly Read-Write Pointers: reader visits every value, and writer marks the boundary between the kept values and the rest.
Step 1: A fresh list
Build the kept values into a new list. Correct: each new value is compared with the new list's last entry. States the contract in one listing: keep a value only when it differs from the one before it, then copy the result back.
| 1 | def remove_duplicates_brute(nums): |
| 2 | if not nums: |
| 3 | return 0 |
| 4 | kept = [nums[0]] # A fresh list, not written in place |
| 5 | for i in range(1, len(nums)): |
| 6 | if nums[i] != kept[-1]: |
| 7 | kept.append(nums[i]) |
| 8 | for i, value in enumerate(kept): |
| 9 | nums[i] = value # Copy the fresh list back over nums |
| 10 | return len(kept) |
Cost: O(N) time, O(N) extra space for the fresh list.
Step 2: Compact in place
The fresh list's last entry is always nums[writer - 1], since writer counts the values kept so far.
So there is no need for a second list: write each kept value straight into nums[writer], because writer never gets ahead of reader.
Comparing against nums[writer] instead of nums[writer - 1] reads a slot that still holds an old, unread value.
| 1 | New line: def removeDuplicates(nums: List[int]) -> int: |
| 2 | if not nums: |
| 3 | return 0 |
| 4 | |
| 5 | New line: writer = 1 # Seed: nums[0] is unconditionally unique |
| 6 | New line: # 1. Read Every Item, Write Only The Kept Ones: |
| 7 | New line: # nums is sorted, so a value is new exactly when it differs from nums[writer - 1], the last value kept; writer never gets ahead of reader, so the write never overwrites an unread value. |
| 8 | New line: for reader in range(1, len(nums)): |
| 9 | New line: if nums[reader] != nums[writer - 1]: |
| 10 | New line: nums[writer] = nums[reader] |
| 11 | New line: writer += 1 |
| 12 | |
| 13 | New line: return writer # nums[0..writer-1] holds compacted unique elements |
Cost: O(N) time, O(1) extra space.
Step 3: The named template
The loop does not change between problems.
Only keep changes.
| 1 | New line: from typing import Callable, List, TypeVar |
| 2 | |
| 3 | New line: Item = TypeVar("Item") |
| 4 | |
| 5 | New line: def read_and_write(items: List[Item], keep: Callable[[Item, int], bool]) -> int: |
| 6 | New line: """Read-Write Pointers: move every kept item to the front of items, in order, in place. |
| 7 | New line: keep(value, writer) returns True to keep value. writer is the number of items kept so far. |
| 8 | New line: items[:writer] holds those items, so keep may compare value with items[writer - 1]. |
| 9 | New line: Precondition: keep reads only value and items[:writer], never a slot at writer or later. |
| 10 | New line: Returns writer. The slots from writer on still hold old values. Move Zeroes writes its zeros there afterwards. |
| 11 | New line: N = len(items). T = the cost of one keep call. |
| 12 | New line: Time: O(N * T) | Space: O(1) |
| 13 | New line: """ |
| 14 | New line: writer = 0 # items[:writer] holds the kept items, in order |
| 15 | # 1. Read Every Item, Write Only The Kept Ones: |
| 16 | New line: # writer never gets ahead of reader, so items[writer] = items[reader] always lands on a slot whose old value has already been read, never one still waiting to be read. |
| 17 | New line: for reader in range(len(items)): |
| 18 | New line: if keep(items[reader], writer): |
| 19 | New line: items[writer] = items[reader] |
| 20 | writer += 1 |
| 21 | |
| 22 | return writer # The number of kept items |
Cost: O(N * T), where T is the cost of one keep call.
| Before | After | What changed |
|---|---|---|
if nums[reader] != nums[writer - 1]: | if keep(items[reader], writer): | The comparison moves inside keep, which also receives writer so it can look at items[writer - 1]. |
nums[writer] = nums[reader] | items[writer] = items[reader] | Same write, generic names. |
writer += 1 | writer += 1 | Identical. |
return writer | return writer | Identical. |