Remove Duplicates from Sorted Array (LeetCode
Learn how to solve Remove Duplicates from Sorted Array using brute force and the optimal two-pointer in-place approach. Includes intuition, dry runs, Java code, correctness proof, complexity analysis, edge cases, interview tips, and production applications.
Introduction
Remove Duplicates from Sorted Array (LeetCode #26) is a foundational problem for learning the in-place Two Pointer write pattern.
It is frequently asked at companies like:
- Amazon
- Microsoft
- Meta
- Apple
- Netflix
- Adobe
- Goldman Sachs
Interviewers use this problem to evaluate whether a candidate can:
- Modify an array in-place without extra memory
- Use a slow/fast pointer pattern correctly
- Reason about in-place mutations
- Write clean, production-quality Java code
The key insight is:
Use a slow pointer that tracks the position of the next unique element, and a fast pointer that scans through the array. Whenever the fast pointer finds a new unique value, write it to the slow pointer position.
If the fast element equals the slow element:
Move fast pointer right (skip duplicate)
If the fast element is different from the slow element:
Advance slow pointer, then write fast value at slow position
This achieves in-place deduplication in a single pass with O(1) extra space.
Problem Statement
Given an integer array nums sorted in non-decreasing order, remove the duplicates in-place such that each unique element appears only once.
Return k — the number of unique elements.
The first k elements of nums must contain the unique elements in their original order.
The remaining elements beyond k do not matter.
You must do this with O(1) extra space.
Formula
After the algorithm completes:
nums[0..k-1] contains the unique elements in sorted order
k = number of unique elements
Example 1
Input
nums = [1, 1, 2]
Output
k = 2
nums = [1, 2, _]
Explanation:
Unique elements: 1, 2
The first two positions contain [1, 2]
The third position is irrelevant
Example 2
Input
nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
Output
k = 5
nums = [0, 1, 2, 3, 4, _, _, _, _, _]
Explanation:
Unique elements: 0, 1, 2, 3, 4
The first five positions contain [0, 1, 2, 3, 4]
Example 3
Input
nums = [1, 2, 3, 4, 5]
Output
k = 5
nums = [1, 2, 3, 4, 5]
Explanation:
No duplicates exist. Array is unchanged.
Important Problem Guarantees
The LeetCode problem guarantees:
1 <= nums.length <= 30,000-100 <= nums[i] <= 100- The array is sorted in non-decreasing order.
- The judge only checks the first
kelements and the return value. - Elements beyond index
k - 1are ignored.
These guarantees mean sorting is not required, and the in-place Two Pointer approach is the expected solution.
What Is the Slow-Fast Two Pointer Pattern?
The slow-fast pattern uses two pointers moving in the same direction at different speeds.
Slow Pointer → Tracks the write position (next unique slot)
Fast Pointer → Scans through the entire array
Visual:
graph TD
S["S"] --> F["F"]
F["F"] --> N_0_0_1_1_1_2_2_3_3_4["0 0 1 1 1 2 2 3 3 4"]
At every step:
nums[fast] == nums[slow] → fast++ (skip duplicate)
nums[fast] != nums[slow] → slow++, nums[slow] = nums[fast], fast++
Why Does the Sorted Array Matter?
Because the array is sorted, all duplicates of a value are adjacent.
[0, 0, 0, 1, 1, 2]
A single forward scan is enough — there is no need to check previously seen values.
If the array were unsorted:
[0, 1, 0, 2, 1]
A HashSet would be required to track seen values, adding O(n) space.
The sorted property makes the O(1) space solution possible.
Approach 1 — Brute Force (Extra Space)
Core Idea
Copy all unique values into a new array using a HashSet or by scanning for changes.
Then overwrite the original array with the unique values.
List → Collect uniques → Write back
Java Code — Brute Force
import java.util.ArrayList;
import java.util.List;
public class RemoveDuplicatesBruteForce {
public int removeDuplicates(int[] nums) {
List<Integer> unique = new ArrayList<>();
unique.add(nums[0]);
for (int i = 1; i < nums.length; i++) {
if (nums[i] != nums[i - 1]) {
unique.add(nums[i]);
}
}
for (int i = 0; i < unique.size(); i++) {
nums[i] = unique.get(i);
}
return unique.size();
}
}
Brute Force Complexity
| Complexity | Value |
|---|---|
| Time | O(n) |
| Space | O(n) |
The solution is linear in time but allocates an extra list proportional to the number of unique elements.
The problem explicitly requires O(1) extra space.
Why Brute Force Fails the Constraint
The extra list uses:
O(k) space
where k is the number of unique elements.
In the worst case:
k = n
(no duplicates — all elements are unique).
The extra list holds the entire array.
The Two Pointer approach avoids this by writing directly into the input array.
Approach 2 — Optimal Two Pointer Solution
Core Idea
Use a slow pointer slow that points to the last confirmed unique position.
Use a fast pointer fast that scans ahead.
When nums[fast] differs from nums[slow], it is a new unique value:
slow++;
nums[slow] = nums[fast];
Then advance fast regardless.
At the end, return slow + 1 (the count of unique elements).
Algorithm
graph TD
slow_0["slow = 0"] --> fast_1["fast = 1"]
fast_1["fast = 1"] --> Loop_while_fast_nums_length["Loop while fast nums.length:"]
Loop_while_fast_nums_length["Loop while fast nums.length:"] --> nums_fast_nums_slow["nums(fast) == nums(slow)"]
nums_fast_nums_slow["nums(fast) == nums(slow)"] --> No["No →"]
No["No →"] --> slow["slow++"]
slow["slow++"] --> nums_slow_nums_fast["nums(slow) = nums(fast)"]
nums_slow_nums_fast["nums(slow) = nums(fast)"] --> fast["fast++"]
fast["fast++"] --> Return_slow_1["Return slow + 1"]
Java Code — Optimal Solution
public class RemoveDuplicates {
public int removeDuplicates(int[] nums) {
int slow = 0;
for (int fast = 1;
fast < nums.length;
fast++) {
if (nums[fast] != nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}
}
LeetCode-Style Implementation
public class Solution {
public int removeDuplicates(int[] nums) {
int slow = 0;
for (int fast = 1;
fast < nums.length;
fast++) {
if (nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
}
}
return slow + 1;
}
}
Both implementations are equivalent. The expanded version is clearer for interviews; the compact version is idiomatic for production.
Two Pointer Flow
graph TD
Initialize_slow_0_fast_1["Initialize slow = 0, fast = 1"] --> fast_nums_length["fast nums.length?"]
fast_nums_length["fast nums.length?"] --> Yes["Yes ↓"]
Yes["Yes ↓"] --> nums_fast_nums_slow["nums(fast) == nums(slow)?"]
nums_fast_nums_slow["nums(fast) == nums(slow)?"] --> No["No →"]
No["No →"] --> slow["slow++"]
slow["slow++"] --> nums_slow_nums_fast["nums(slow) = nums(fast)"]
nums_slow_nums_fast["nums(slow) = nums(fast)"] --> fast["fast++"]
fast["fast++"] --> Repeat["Repeat"]
Detailed Dry Run
Input:
nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
Initial state:
slow = 0
fast = 1
Iteration 1
nums[fast] = 0
nums[slow] = 0
Equal → fast++
slow = 0
fast = 2
Iteration 2
nums[fast] = 1
nums[slow] = 0
Different → slow++ → slow = 1
nums[1] = 1
fast++
Array: [0, 1, 1, 1, 1, 2, 2, 3, 3, 4]
slow = 1
fast = 3
Iteration 3
nums[fast] = 1
nums[slow] = 1
Equal → fast++
slow = 1
fast = 4
Iteration 4
nums[fast] = 1
nums[slow] = 1
Equal → fast++
slow = 1
fast = 5
Iteration 5
nums[fast] = 2
nums[slow] = 1
Different → slow++ → slow = 2
nums[2] = 2
fast++
Array: [0, 1, 2, 1, 1, 2, 2, 3, 3, 4]
slow = 2
fast = 6
Iteration 6
nums[fast] = 2
nums[slow] = 2
Equal → fast++
slow = 2
fast = 7
Iteration 7
nums[fast] = 3
nums[slow] = 2
Different → slow++ → slow = 3
nums[3] = 3
fast++
Array: [0, 1, 2, 3, 1, 2, 2, 3, 3, 4]
slow = 3
fast = 8
Iteration 8
nums[fast] = 3
nums[slow] = 3
Equal → fast++
slow = 3
fast = 9
Iteration 9
nums[fast] = 4
nums[slow] = 3
Different → slow++ → slow = 4
nums[4] = 4
fast++
Array: [0, 1, 2, 3, 4, 2, 2, 3, 3, 4]
slow = 4
fast = 10
fast = 10 = nums.length → loop ends.
Return:
slow + 1 = 5
First 5 elements:
[0, 1, 2, 3, 4]
Dry Run Table
| fast | nums[fast] | nums[slow] | Action | slow | Array (first k) |
|---|---|---|---|---|---|
| 1 | 0 | 0 | skip | 0 | [0] |
| 2 | 1 | 0 | write, slow++ | 1 | [0, 1] |
| 3 | 1 | 1 | skip | 1 | [0, 1] |
| 4 | 1 | 1 | skip | 1 | [0, 1] |
| 5 | 2 | 1 | write, slow++ | 2 | [0, 1, 2] |
| 6 | 2 | 2 | skip | 2 | [0, 1, 2] |
| 7 | 3 | 2 | write, slow++ | 3 | [0, 1, 2, 3] |
| 8 | 3 | 3 | skip | 3 | [0, 1, 2, 3] |
| 9 | 4 | 3 | write, slow++ | 4 | [0, 1, 2, 3, 4] |
Return slow + 1 = 5.
Visual Pointer Movement
nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4]
S
F→ skip (0==0)
F→ write 1, S moves
F→ skip (1==1)
F→ skip (1==1)
F→ write 2, S moves
F→ skip (2==2)
F→ write 3, S moves
F→ skip (3==3)
F→ write 4, S moves
Result: [0, 1, 2, 3, 4, ...]
S at index 4
Return 5
Why This Works
The slow pointer always points to the last confirmed unique element.
The region nums[0..slow] is always a valid, sorted unique prefix.
When fast finds a value different from nums[slow]:
It is guaranteed to be larger (array is sorted)
It is guaranteed to be new (no duplicates are adjacent in the unique prefix)
Writing it at slow+1 extends the unique prefix correctly
No element in the unique prefix [0..slow] is ever overwritten with a wrong value.
Correctness Proof
Invariant: After each iteration, nums[0..slow] contains exactly the unique elements seen so far, in sorted order.
Base case: Before the loop, slow = 0 and nums[0..0] contains exactly the first element — trivially unique.
Inductive step:
- If
nums[fast] == nums[slow]: the value is a duplicate.slowdoes not move. The prefixnums[0..slow]is unchanged. Invariant holds. - If
nums[fast] != nums[slow]: since the array is sorted andfast > slow,nums[fast] > nums[slow]. It is a new unique value. Writing it atslow + 1extends the unique prefix by one. Invariant holds.
Termination: fast advances by 1 every iteration. Loop terminates when fast == nums.length.
Conclusion: At termination, nums[0..slow] contains all unique elements. Return value slow + 1 is correct.
Loop Invariant
At every iteration:
nums[0..slow] contains all unique elements
seen so far in their original sorted order
When the loop ends:
All elements have been visited.
nums[0..slow] is the complete unique prefix.
slow + 1 is the answer.
Complexity Analysis
| Complexity | Value |
|---|---|
| Time | O(n) |
| Space | O(1) |
fast traverses the array exactly once.
slow only advances when a unique value is found — at most n times.
Total operations: proportional to n.
No extra data structures are allocated.
Why the Complexity Is Not O(n²)
Although two pointers are used, they are not nested.
Both move left to right.
fast advances every iteration.
Total iterations equal n - 1.
O(n)
not:
O(n²)
Why Use slow = 0 and fast = 1?
slow = 0 seeds the unique prefix with the first element.
The first element is always unique by definition — there is no previous element to compare it against.
fast = 1 starts the scan at the second element.
This avoids a redundant comparison on the first iteration.
Production-Ready Implementation
import java.util.Objects;
public class RemoveDuplicatesSorted {
/**
* Removes duplicates from a sorted array in-place.
*
* The first k elements of nums will contain the unique
* elements in their original sorted order.
*
* @param nums non-null sorted integer array, length >= 1
* @return k, the number of unique elements
* @throws IllegalArgumentException if nums is null or empty
*/
public int removeDuplicates(int[] nums) {
Objects.requireNonNull(nums,
"nums must not be null");
if (nums.length == 0) {
throw new IllegalArgumentException(
"nums must not be empty");
}
int slow = 0;
for (int fast = 1;
fast < nums.length;
fast++) {
if (nums[fast] != nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}
}
Edge Cases
Single Element
Input
nums = [5]
Output
k = 1
nums = [5]
Loop never executes. Return slow + 1 = 1.
All Same Elements
Input
nums = [3, 3, 3, 3, 3]
Output
k = 1
nums = [3, _, _, _, _]
fast scans all — every element equals nums[slow]. slow never advances.
Return 1.
No Duplicates
Input
nums = [1, 2, 3, 4, 5]
Output
k = 5
nums = [1, 2, 3, 4, 5]
Every element is different. slow advances with every fast step.
Array is unchanged because every write is nums[slow] = nums[fast] where slow == fast - 1.
Two Elements — Duplicate
Input
nums = [7, 7]
Output
k = 1
nums = [7, _]
Two Elements — Unique
Input
nums = [3, 9]
Output
k = 2
nums = [3, 9]
Negative Numbers
Input
nums = [-4, -4, -2, 0, 0, 1]
Output
k = 4
nums = [-4, -2, 0, 1, _, _]
Negative values work identically — comparison is value-based.
All Unique with One Duplicate at End
Input
nums = [1, 2, 3, 4, 4]
Output
k = 4
nums = [1, 2, 3, 4, _]
Only the last duplicate is skipped.
Common Interview Mistakes
Mistake 1 — Starting slow and fast at the Same Index
❌ int slow = 0;
int fast = 0;
Comparing nums[fast] with nums[slow] when both are 0 always finds equality on the first step. This skips the write for the second element incorrectly.
Correct:
int slow = 0;
int fast = 1;
Mistake 2 — Returning slow Instead of slow + 1
❌ return slow;
slow is a zero-based index. The count of unique elements is slow + 1.
Mistake 3 — Writing Before Advancing slow
❌ nums[slow] = nums[fast];
slow++;
This overwrites nums[slow] (the last confirmed unique value) before advancing. The old value is lost.
Correct:
slow++;
nums[slow] = nums[fast];
Mistake 4 — Using a HashSet (Ignores the Sorted Property)
❌ Set<Integer> seen = new HashSet<>();
A HashSet uses O(n) space. The problem requires O(1) extra space.
The sorted array guarantees that duplicates are adjacent, making a HashSet unnecessary.
Mistake 5 — Using a Second Array
❌ int[] result = new int[nums.length];
This uses O(n) extra space and violates the in-place constraint.
Mistake 6 — Using nums.length - 1 as the Loop Bound
❌ for (int fast = 1; fast < nums.length - 1; fast++)
This misses the last element. Use fast < nums.length.
Mistake 7 — Not Handling a Single-Element Array
❌ // Assume length >= 2
A single-element array should return 1. The implementation must handle this.
With slow = 0 and fast = 1, the loop condition fast < 1 is false immediately — the correct value 1 is returned.
Mistake 8 — Confusing This with Remove Duplicates II
LeetCode #80 (Remove Duplicates II) allows each element to appear at most twice. The logic is different:
// LeetCode #80
if (nums[fast] != nums[slow - 1]) {
nums[++slow] = nums[fast];
}
Do not confuse the two problems.
Follow-Up 1 — Allow Each Element at Most Twice (LeetCode #80)
Generalize to allow k occurrences:
public int removeDuplicates(int[] nums) {
int slow = 1; // first two positions always kept
for (int fast = 2;
fast < nums.length;
fast++) {
// Compare with two positions back
if (nums[fast] != nums[slow - 1]) {
nums[++slow] = nums[fast];
}
}
return slow + 1;
}
For the general case of k allowed duplicates, compare nums[fast] with nums[slow - (k - 1)].
Follow-Up 2 — Remove a Specific Value In-Place (LeetCode #27)
Instead of removing duplicates, remove all occurrences of a given value:
public int removeElement(int[] nums, int val) {
int slow = 0;
for (int fast = 0;
fast < nums.length;
fast++) {
if (nums[fast] != val) {
nums[slow++] = nums[fast];
}
}
return slow;
}
The slow-fast pattern is identical — only the condition changes.
Follow-Up 3 — What If the Array Is Unsorted?
For an unsorted array, duplicates are not adjacent.
Two options:
Option A — Sort first:
Sort → O(n log n)
Apply Two Pointer → O(n)
Total → O(n log n)
Option B — Use a LinkedHashSet:
Preserve insertion order, deduplicate → O(n) time, O(n) space
For the LeetCode problem, the sorted guarantee is essential to achieving O(1) space.
Follow-Up 4 — Return the Unique Elements as a New Array
public int[] uniqueElements(int[] nums) {
int k = removeDuplicates(nums);
return Arrays.copyOfRange(nums, 0, k);
}
This allocates a new array of size k after the in-place pass.
Total space: O(k) for the output.
Follow-Up 5 — Count Total Duplicates Removed
public int countDuplicates(int[] nums) {
int k = removeDuplicates(nums);
return nums.length - k;
}
nums.length - k equals the number of elements removed.
Production Applications
The in-place deduplication pattern is used in real engineering systems.
Examples include:
- Database result set deduplication before pagination
- Log stream deduplication for idempotent event processing
- Sorted merge output deduplication in MapReduce pipelines
- Time-series data compression — removing repeated sensor readings
- IP packet deduplication in network processing pipelines
- Sorted cache invalidation list compaction
- Financial transaction deduplication in reconciliation batches
The sorted property is frequently available in these systems (sorted indexes, sorted logs, sorted merge outputs), making the O(1) Two Pointer approach directly applicable.
Real-World Example — Sorted Event Log Deduplication
A log pipeline receives sorted events. Duplicate event IDs must be removed before downstream processing.
public int deduplicateEventIds(int[] sortedIds) {
if (sortedIds == null
|| sortedIds.length == 0) {
return 0;
}
int slow = 0;
for (int fast = 1;
fast < sortedIds.length;
fast++) {
if (sortedIds[fast]
!= sortedIds[slow]) {
slow++;
sortedIds[slow] =
sortedIds[fast];
}
}
return slow + 1;
}
The logic is identical to the LeetCode solution.
Real-World Example — Sorted Merge Deduplication
After a K-way sorted merge, the output may contain duplicates from overlapping ranges.
Merge Result: [1, 1, 2, 3, 3, 3, 4]
After deduplication: [1, 2, 3, 4]
k = 4
The Two Pointer approach deduplicates the merged output in O(n) time with O(1) extra space.
Senior-Level Discussion — Why Is This Not O(n²)?
A common misconception is that comparing every element to the previous one is quadratic.
In reality:
fast moves forward exactly once per element → n - 1 steps
slow advances at most once per unique value → at most n steps
Total pointer movements → O(n)
There is no nested iteration. Both pointers move left to right.
Senior-Level Discussion — Is This Stable?
Yes.
The relative order of unique elements is preserved:
Input: [0, 0, 1, 1, 2]
Output: [0, 1, 2]
The original order 0 → 1 → 2 is maintained.
This is guaranteed because fast scans left to right and slow writes left to right.
Senior-Level Discussion — Can This Handle Very Large Arrays?
Yes.
The algorithm uses:
Time → O(n) — single pass
Space → O(1) — two integer variables
For a 1-billion-element sorted array, the algorithm processes it in linear time using only two index variables.
The bottleneck is I/O (reading and writing the array), not the algorithm.
Interview Explanation
A strong interview walkthrough:
1. Explain the slow-fast pointer pattern
2. Explain why the sorted property eliminates the need for a HashSet
3. Explain the invariant: nums[0..slow] is always the unique prefix
4. Walk through the dry run on the example
5. State O(n) time and O(1) space
6. Mention edge cases: single element, all duplicates, no duplicates
Interview Tips
Interviewers often ask:
- Why use Two Pointer instead of a HashSet?
- What does the slow pointer represent at any point in time?
- Why do you start fast at index 1?
- Why return slow + 1 and not slow?
- What if all elements are the same?
- What if the array has no duplicates?
- What happens to elements after index k?
- Can this be extended to allow at most k duplicates?
- What is the loop invariant?
- What is the space complexity?
Be prepared to explain the invariant before writing code.
Approach Comparison
| Approach | Time | Space | Notes |
|---|---|---|---|
| Brute Force (List) | O(n) | O(n) | Uses extra list — violates constraint |
| Two Pointer | O(n) | O(1) | Optimal — in-place, single pass |
Similar Problems
- LeetCode #27 — Remove Element — Remove all occurrences of a given value in-place.
- LeetCode #80 — Remove Duplicates II — Allow each element at most twice.
- LeetCode #283 — Move Zeroes — Move all zeroes to the end in-place.
- LeetCode #905 — Sort Array By Parity — Partition even and odd elements in-place.
All four problems use the same slow-fast write pointer pattern. Mastering Remove Duplicates from Sorted Array provides the foundation for all of them.
Unit Tests
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class RemoveDuplicatesTest {
final RemoveDuplicates solution =
new RemoveDuplicates();
@Test
void example1() {
int[] nums = {1, 1, 2};
int k = solution.removeDuplicates(nums);
assertEquals(2, k);
assertArrayEquals(
new int[]{1, 2},
Arrays.copyOfRange(nums, 0, k));
}
@Test
void example2() {
int[] nums = {0, 0, 1, 1, 1, 2, 2, 3, 3, 4};
int k = solution.removeDuplicates(nums);
assertEquals(5, k);
assertArrayEquals(
new int[]{0, 1, 2, 3, 4},
Arrays.copyOfRange(nums, 0, k));
}
@Test
void singleElement() {
int[] nums = {5};
assertEquals(1,
solution.removeDuplicates(nums));
}
@Test
void allSame() {
int[] nums = {3, 3, 3, 3};
int k = solution.removeDuplicates(nums);
assertEquals(1, k);
assertEquals(3, nums[0]);
}
@Test
void noDuplicates() {
int[] nums = {1, 2, 3, 4, 5};
int k = solution.removeDuplicates(nums);
assertEquals(5, k);
assertArrayEquals(
new int[]{1, 2, 3, 4, 5},
Arrays.copyOfRange(nums, 0, k));
}
@Test
void negativeNumbers() {
int[] nums = {-4, -4, -2, 0, 0, 1};
int k = solution.removeDuplicates(nums);
assertEquals(4, k);
assertArrayEquals(
new int[]{-4, -2, 0, 1},
Arrays.copyOfRange(nums, 0, k));
}
@Test
void twoElementsDuplicate() {
int[] nums = {7, 7};
assertEquals(1,
solution.removeDuplicates(nums));
}
@Test
void twoElementsUnique() {
int[] nums = {3, 9};
int k = solution.removeDuplicates(nums);
assertEquals(2, k);
assertArrayEquals(
new int[]{3, 9},
Arrays.copyOfRange(nums, 0, k));
}
@Test
void duplicateAtEnd() {
int[] nums = {1, 2, 3, 4, 4};
int k = solution.removeDuplicates(nums);
assertEquals(4, k);
assertArrayEquals(
new int[]{1, 2, 3, 4},
Arrays.copyOfRange(nums, 0, k));
}
}
Summary
Remove Duplicates from Sorted Array is the canonical problem for the in-place slow-fast Two Pointer write pattern. The brute-force approach using an extra list violates the O(1) space constraint. The Two Pointer approach achieves O(n) time and O(1) space by maintaining a slow pointer as the write position and a fast pointer as the read scanner. The sorted property guarantees all duplicates are adjacent, making a single forward pass sufficient. The invariant nums[0..slow] always holds the complete unique prefix. Mastering this pattern directly enables solving Remove Element, Remove Duplicates II, Move Zeroes, and many other in-place array problems.
Key Takeaways
- Use a slow pointer as the write position and a fast pointer as the read scanner.
- The slow pointer always marks the last confirmed unique element.
- When
nums[fast] != nums[slow], advance slow then writenums[fast]at the new slow position. - Return
slow + 1, notslow. - The sorted property makes O(1) space possible — all duplicates are adjacent.
- Never overwrite
nums[slow]before advancing it. - Handle single-element arrays: the loop simply does not execute, and
1is correctly returned. - Achieve O(n) time and O(1) space — a single pass with two index variables.
- This pattern generalises to Remove Element, Move Zeroes, and Remove Duplicates II.