80. Remove Duplicates from Sorted Array II
Problem Description
You are given an integer array nums
that is sorted in non-decreasing order. Your task is to remove duplicates from this array in-place such that each unique element appears at most twice. The relative order of elements must be preserved.
The key requirements are:
- Modify the array in-place (cannot create a new array)
- Use only O(1) extra memory
- Each unique number can appear at most 2 times
- Return the length
k
of the modified array - The first
k
elements ofnums
should contain the final result
For example:
- If
nums = [1,1,1,2,2,3]
, after removing duplicates where each element appears at most twice, the array becomes[1,1,2,2,3,_]
where_
represents any value (doesn't matter). The function should returnk = 5
.
The solution uses a two-pointer technique where:
- Variable
k
tracks the position where the next valid element should be placed - For each element
x
in the array:- If
k < 2
(first two elements are always valid) orx != nums[k-2]
(current element is different from the element two positions back), we placex
at positionk
and incrementk
- Otherwise, we skip the element as it would create more than 2 duplicates
- If
The algorithm efficiently processes the array in a single pass with O(n) time complexity and O(1) space complexity.
Intuition
Since the array is sorted, all duplicate elements are adjacent to each other. This is the key insight that makes the problem solvable in a single pass.
Think about what we're trying to achieve: we want to keep at most 2 copies of each element. As we traverse the array, we need to decide whether to keep or skip each element.
The critical observation is: if we've already kept some elements in our result (let's say the first k
elements are our valid result so far), then for a new element x
, we should only add it if:
- We have fewer than 2 elements in our result (the first two elements can always be added), OR
- The element
x
is different from the element that's 2 positions back in our result
Why check 2 positions back? Because if x
equals nums[k-2]
, it means we already have at least 2 copies of x
in our result (at positions k-2
and k-1
). Adding another would violate our constraint.
Let's trace through an example: nums = [1,1,1,2,2,3]
- Start with
k = 0
- Element 1 (first):
k < 2
, so add it.k = 1
- Element 1 (second):
k < 2
, so add it.k = 2
- Element 1 (third):
k = 2
, check if1 != nums[0]
(which is 1). They're equal, so skip. - Element 2 (first):
k = 2
, check if2 != nums[0]
(which is 1). They're different, so add it.k = 3
- Element 2 (second):
k = 3
, check if2 != nums[1]
(which is 1). They're different, so add it.k = 4
- Element 3:
k = 4
, check if3 != nums[2]
(which is 2). They're different, so add it.k = 5
This approach naturally maintains the relative order and ensures no element appears more than twice, all while using just a single pointer k
to track our position.
Learn more about Two Pointers patterns.
Solution Approach
The implementation uses a two-pointer technique with a single pass through the array:
Algorithm Steps:
-
Initialize a pointer
k
: Setk = 0
to track the position where the next valid element should be placed. This represents the length of our processed array. -
Iterate through each element: For each element
x
innums
:- Check if element should be kept:
- If
k < 2
: The first two elements are always valid (we can have up to 2 duplicates) - OR if
x != nums[k-2]
: The current element is different from the element two positions back in our valid array
- If
- If the condition is met:
- Place the element:
nums[k] = x
- Increment the pointer:
k += 1
- Place the element:
- Otherwise: Skip the element (it would create more than 2 duplicates)
- Check if element should be kept:
-
Return the result: After traversing the entire array, return
k
as the length of the modified array.
Key Pattern - Why nums[k-2]
?
The check x != nums[k-2]
is the core of this algorithm:
nums[k-2]
represents the element that's 2 positions back in our valid result- If
x
equalsnums[k-2]
, then we know:nums[k-2] = x
(by definition)nums[k-1] = x
(because the array is sorted, and we wouldn't have addednums[k-1]
if it wasn't equal to or greater thannums[k-2]
)
- Therefore, adding
x
would create a third duplicate, which violates our constraint
Time and Space Complexity:
- Time Complexity:
O(n)
wheren
is the length of the array. We traverse the array exactly once. - Space Complexity:
O(1)
as we only use a constant amount of extra space (the variablek
).
Extension to k duplicates:
This approach can be generalized to allow at most k
duplicates by:
- Keeping the first
k
elements automatically - For subsequent elements, checking against
nums[pos-k]
instead ofnums[pos-2]
This makes the solution flexible and applicable to similar problems with different duplicate limits.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's trace through a small example to illustrate the solution approach.
Input: nums = [0,0,1,1,1,2,3,3]
We'll use a pointer k
to track where the next valid element should be placed.
Initial State:
k = 0
- Array:
[0,0,1,1,1,2,3,3]
Step 1: Process x = 0
(index 0)
- Check:
k < 2
? Yes (k=0) - Action: Place 0 at position k=0, increment k
nums[0] = 0
,k = 1
- Array:
[0,0,1,1,1,2,3,3]
Step 2: Process x = 0
(index 1)
- Check:
k < 2
? Yes (k=1) - Action: Place 0 at position k=1, increment k
nums[1] = 0
,k = 2
- Array:
[0,0,1,1,1,2,3,3]
Step 3: Process x = 1
(index 2)
- Check:
k < 2
? No (k=2). Check:1 != nums[0]
? Yes (1 β 0) - Action: Place 1 at position k=2, increment k
nums[2] = 1
,k = 3
- Array:
[0,0,1,1,1,2,3,3]
Step 4: Process x = 1
(index 3)
- Check:
k < 2
? No (k=3). Check:1 != nums[1]
? Yes (1 β 0) - Action: Place 1 at position k=3, increment k
nums[3] = 1
,k = 4
- Array:
[0,0,1,1,1,2,3,3]
Step 5: Process x = 1
(index 4)
- Check:
k < 2
? No (k=4). Check:1 != nums[2]
? No (1 = 1) - Action: Skip this element (would create 3rd duplicate)
- Array:
[0,0,1,1,1,2,3,3]
(unchanged)
Step 6: Process x = 2
(index 5)
- Check:
k < 2
? No (k=4). Check:2 != nums[2]
? Yes (2 β 1) - Action: Place 2 at position k=4, increment k
nums[4] = 2
,k = 5
- Array:
[0,0,1,1,2,2,3,3]
Step 7: Process x = 3
(index 6)
- Check:
k < 2
? No (k=5). Check:3 != nums[3]
? Yes (3 β 1) - Action: Place 3 at position k=5, increment k
nums[5] = 3
,k = 6
- Array:
[0,0,1,1,2,3,3,3]
Step 8: Process x = 3
(index 7)
- Check:
k < 2
? No (k=6). Check:3 != nums[4]
? Yes (3 β 2) - Action: Place 3 at position k=6, increment k
nums[6] = 3
,k = 7
- Array:
[0,0,1,1,2,3,3,3]
Final Result:
- Return
k = 7
- First 7 elements:
[0,0,1,1,2,3,3]
- Each unique element appears at most twice β
The key insight: by checking nums[k-2]
, we ensure that if an element already appears twice in our result (at positions k-2 and k-1), we don't add a third copy.
Solution Implementation
1class Solution:
2 def removeDuplicates(self, nums: List[int]) -> int:
3 # Pointer to track the position for the next valid element
4 write_index = 0
5
6 # Iterate through each element in the array
7 for current_num in nums:
8 # Allow element if:
9 # 1. We have less than 2 elements (write_index < 2), OR
10 # 2. Current element is different from the element 2 positions back
11 if write_index < 2 or current_num != nums[write_index - 2]:
12 # Place the current element at the write position
13 nums[write_index] = current_num
14 # Move write pointer forward
15 write_index += 1
16
17 # Return the length of the modified array
18 return write_index
19
1class Solution {
2 public int removeDuplicates(int[] nums) {
3 // Index to track the position for next valid element
4 int writeIndex = 0;
5
6 // Iterate through each element in the array
7 for (int currentNum : nums) {
8 // Allow element if:
9 // 1. We have less than 2 elements (writeIndex < 2), OR
10 // 2. Current element is different from the element 2 positions back
11 if (writeIndex < 2 || currentNum != nums[writeIndex - 2]) {
12 // Place the current element at writeIndex position
13 nums[writeIndex] = currentNum;
14 // Move to next write position
15 writeIndex++;
16 }
17 }
18
19 // Return the length of the modified array
20 return writeIndex;
21 }
22}
23
1class Solution {
2public:
3 int removeDuplicates(vector<int>& nums) {
4 // writeIndex keeps track of where to write the next valid element
5 int writeIndex = 0;
6
7 // Iterate through each element in the array
8 for (int currentNum : nums) {
9 // Allow element if:
10 // 1. We have less than 2 elements written (writeIndex < 2), OR
11 // 2. Current element is different from the element 2 positions back
12 // (this ensures at most 2 duplicates)
13 if (writeIndex < 2 || currentNum != nums[writeIndex - 2]) {
14 // Write the current element at writeIndex position and increment
15 nums[writeIndex++] = currentNum;
16 }
17 }
18
19 // Return the length of the modified array
20 return writeIndex;
21 }
22};
23
1/**
2 * Removes duplicates from a sorted array, allowing each element to appear at most twice.
3 * Modifies the array in-place and returns the new length.
4 *
5 * @param nums - The sorted array of numbers to process
6 * @returns The new length of the array after removing excess duplicates
7 */
8function removeDuplicates(nums: number[]): number {
9 // Initialize pointer for the position to place the next valid element
10 let currentIndex: number = 0;
11
12 // Iterate through each element in the array
13 for (const currentNumber of nums) {
14 // Check if we can add this element:
15 // 1. If we have less than 2 elements (currentIndex < 2), always add
16 // 2. If the current number is different from the element 2 positions back
17 if (currentIndex < 2 || currentNumber !== nums[currentIndex - 2]) {
18 // Place the current number at the current index position
19 nums[currentIndex] = currentNumber;
20 // Move the pointer to the next position
21 currentIndex++;
22 }
23 }
24
25 // Return the new length of the modified array
26 return currentIndex;
27}
28
Time and Space Complexity
Time Complexity: O(n)
, where n
is the length of the input array nums
. The algorithm iterates through the array exactly once with a single for loop. Each operation inside the loop (comparison, assignment, and increment) takes constant time O(1)
. Therefore, the overall time complexity is linear.
Space Complexity: O(1)
. The algorithm modifies the input array in-place and only uses a constant amount of extra space for the variable k
and the loop variable x
. No additional data structures are created that scale with the input size, making the space complexity constant.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusing the Two Pointers' Roles
Pitfall: Many developers mistakenly think k
represents an index in the original array or try to use it to compare with indices of unprocessed elements. This leads to incorrect comparisons like checking nums[i-2]
instead of nums[k-2]
.
Why it happens: The variable naming and the fact that we're iterating through the same array we're modifying can create confusion about which part of the array represents the "processed" valid elements versus the "unprocessed" original elements.
Solution:
- Think of
nums[0:k]
as your "result array" being built k
is the size of this result array AND the position where the next valid element goes- Always compare with positions in the result portion (
nums[k-2]
), not the iteration position
2. Off-by-One Errors with the k-2 Check
Pitfall: Accessing nums[k-2]
when k < 2
causes an index out of bounds error or incorrect array access with negative indices (in Python, negative indices wrap around).
Example of incorrect code:
def removeDuplicates(self, nums: List[int]) -> int:
k = 0
for x in nums:
# WRONG: This will access nums[-2] and nums[-1] when k is 0 or 1
if x != nums[k-2]:
nums[k] = x
k += 1
return k
Solution: Always guard the nums[k-2]
check with k < 2
condition first:
if k < 2 or x != nums[k-2]: # Correct: short-circuit evaluation prevents invalid access
3. Attempting to Delete Elements Instead of Overwriting
Pitfall: Trying to actually remove elements from the array using operations like del
, pop()
, or list slicing, which violates the O(1) space requirement and in-place modification constraint.
Incorrect approach:
def removeDuplicates(self, nums: List[int]) -> int:
i = 2
while i < len(nums):
if nums[i] == nums[i-2]:
nums.pop(i) # WRONG: O(n) operation inside loop, modifies array size
else:
i += 1
return len(nums)
Solution: Stick to the overwriting approach - don't actually remove elements, just overwrite positions and return the new logical length.
4. Misunderstanding the Return Value
Pitfall: Returning the wrong value or misunderstanding what k
represents. Some might return k-1
thinking it's the last valid index, or return the number of elements removed instead of kept.
Solution: Remember that k
represents:
- The COUNT of valid elements (not the last index)
- The LENGTH of the modified portion of the array
- The next position where a valid element would be placed (which equals the count)
5. Forgetting the Array is Already Sorted
Pitfall: Adding unnecessary complexity by trying to handle unsorted arrays or checking multiple previous elements unnecessarily.
Why it matters: Because the array is sorted, if nums[k-2] == x
, we know that nums[k-1] == x
as well (since we only add elements that maintain the sorted order). This is why checking just nums[k-2]
is sufficient.
Solution: Trust the sorted property - you only need to check against the element 2 positions back, not both nums[k-1]
and nums[k-2]
.
Which of these properties could exist for a graph but not a tree?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Donβt Miss This!