Coding Interview Patterns Module 4 – Two pointers
In-place compaction with reader–writer pointers
Keep at most k copies of each value in a sorted array, in place and in one pass: the problem, three examples and why deleting copies costs n² moves.
What you will learn
- Remove duplicates from a sorted array in place, in one pass
- Implement keep-at-most-k compaction with a reader and a writer pointer
- Prove that the writer never overwrites an item the reader still needs
- Explain why deleting inside the loop costs O(n²) moves
- Maintain the original order of the kept items, and handle k = 0
Before you start
What you’ll learn · Before you start
What you will learn
- Remove duplicates from a sorted array in place, in one pass
- Implement keep-at-most-k compaction with a reader and a writer pointer
- Prove that the writer never overwrites an item the reader still needs
- Explain why deleting inside the loop costs O(n²) moves
- Maintain the original order of the kept items, and handle k = 0
Before you start
On this page
Removing duplicates from a sorted array in place takes one pass with reader–writer pointers: Two indices that both move right through one list: read visits every item once, and write marks where the next kept item goes, so the kept items gather at the front in their order. More in “Two pointers: converging and reader–writer”, even on a log of 1,000,000 readings, where deleting each extra copy can shift about 5 × 10¹¹ items. A reader visits every item in turn, and a writer marks the end of the part you keep: an item that stays is copied to the writer’s position, and the writer moves on.
Nothing is deleted from the middle of the list: one pass, O(n) time, O(1) extra memory, and the kept items stay in their order. The problem below is the general in-place compaction: Rearranging an array so that the items to keep fill its first part, in their original order, and returning the length of that part. Whatever lies after it is left over and ignored.: at most k copies of each value, plain duplicate removal when k is 1.
The problem: keep at most k readings per minute, in place
The problem
A weather station logs a reading every few seconds, and stamps lists the minute of each reading, sorted from
earliest to latest. To thin the log, keep at most k readings per minute, in place: rearrange stamps so that its
first w positions hold the kept stamps in their original order, and return w. What is left after position w
does not matter. Limits: 1 ≤ n ≤ 1,000,000 stamps, 0 ≤ k ≤ 1,000,000, and every stamp is between 0 and 10⁹.
stamps = [3, 3, 3, 5, 8, 8, 8, 8, 9]withk = 2gives 6, and the first six positions become 3, 3, 5, 8, 8, 9.stamps = [4, 4, 4, 7]withk = 1gives 2, for 4 and 7: plain duplicate removal.stamps = [1, 2, 2]withk = 0gives 0, because nothing may be kept.
The first example, before and after the call:
What the call must leave in the log for the first example, with k = 2
Text description of the diagram
The diagram has two rows, one under the other.
- Before the call, with k = 2, the nine stamps are 3, 3, 3, 5, 8, 8, 8, 8 and 9.
- After the call, which returns w = 6, the first six positions, stamps[:6], hold 3, 3, 5, 8, 8 and 9: at most two readings of each minute, in their original order. The last three positions, stamps[6:], are marked with question marks, because the problem does not say what they hold and nothing compares them.
Checkpoint
Check: Keep at most k readings per minute, in place
Deleting each extra copy makes the work quadratic
A natural first answer walks the list and counts the copies of the current minute. When a reading is one too many,
it deletes it where it stands, with del stamps[i], and looks at position i again. It is short and
plainly correct, but the deletion hides a cost: removing an item from a Python list shifts every item after it one
place left, which is why the Python tutorial calls lists slow to pop from the front. Step through it on the first
example:
Program · trace/delete_steps.py
stamps = [3, 3, 3, 5, 8, 8, 8, 8, 9]
k = 2
i = 0
copies = 0
moves = 0
while i < len(stamps):
if i == 0 or stamps[i] != stamps[i - 1]:
copies = 0
if copies < k:
copies += 1
i += 1
else:
moves += len(stamps) - i - 1
del stamps[i]
print(stamps, moves, "moves") Variables
- changed in this step
Printed so far
Nothing printed yet[3, 3, 5, 8, 8, 9] 9 moves
Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.
The drawing shows the variables when the program ends, and the program's 57 steps are listed below.
Change the program and run it here. Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Every step as text
- Start. No line has run yet, so there are no variables. Python starts with line 1.
- Line 1.
stamps = [3, 3, 3, 5, 8, 8, 8, 8, 9]makesstampsa list of 9 items. - Line 2.
k = 2setskto2. - Line 3.
i = 0setsito0. - Line 4.
copies = 0setscopiesto0. - Line 5.
moves = 0setsmovesto0. - Line 6.
while i < len(stamps):tests its condition: it is true (iis0andstampsis[3, 3, 3, 5, 8, 8, 8, 8, …]), so the body runs. - Line 7.
i == 0 or stamps[i] != stamps[i - 1]is true (iis0andstampsis[3, 3, 3, 5, 8, 8, 8, 8, …]), so the block underif i == 0 or stamps[i] != stamps[i - 1]:runs. - Line 8.
copies = 0copieskeeps its value,0. - Line 9.
copies < kis true (copiesis0andkis2), so the block underif copies < k:runs. - Line 10.
copies += 1changescopiesfrom0to1. - Line 11.
i += 1changesifrom0to1. - Line 6.
while i < len(stamps):tests its condition: it is true (iis1andstampsis[3, 3, 3, 5, 8, 8, 8, 8, …]), so the body runs again (pass 2). - Line 7.
i == 0 or stamps[i] != stamps[i - 1]is false (iis1andstampsis[3, 3, 3, 5, 8, 8, 8, 8, …]), so Python skips the block underif i == 0 or stamps[i] != stamps[i - 1]:. - Line 9.
copies < kis true (copiesis1andkis2), so the block underif copies < k:runs. - Line 10.
copies += 1changescopiesfrom1to2. - Line 11.
i += 1changesifrom1to2. - Line 6.
while i < len(stamps):tests its condition: it is true (iis2andstampsis[3, 3, 3, 5, 8, 8, 8, 8, …]), so the body runs again (pass 3). - Line 7.
i == 0 or stamps[i] != stamps[i - 1]is false (iis2andstampsis[3, 3, 3, 5, 8, 8, 8, 8, …]), so Python skips the block underif i == 0 or stamps[i] != stamps[i - 1]:. - Line 9.
copies < kis false (copiesis2andkis2), so the else block runs. - Line 13.
moves += len(stamps) - i - 1changesmovesfrom0to6. - Line 14.
del stamps[i]takes3out ofstamps, from position 2: the 6 items after it move up one place. Key point The third 3 is one too many, so it is deleted where it stands. The six readings after it each move up one place, andistays at 2 to read the 5 that moved into its slot. - Line 6.
while i < len(stamps):tests its condition: it is true (iis2andstampsis[3, 3, 5, 8, 8, 8, 8, 9]), so the body runs again (pass 4). - Line 7.
i == 0 or stamps[i] != stamps[i - 1]is true (iis2andstampsis[3, 3, 5, 8, 8, 8, 8, 9]), so the block underif i == 0 or stamps[i] != stamps[i - 1]:runs. - Line 8.
copies = 0setscopiesto0(it was2). - Line 9.
copies < kis true (copiesis0andkis2), so the block underif copies < k:runs. - Line 10.
copies += 1changescopiesfrom0to1. - Line 11.
i += 1changesifrom2to3. - Line 6.
while i < len(stamps):tests its condition: it is true (iis3andstampsis[3, 3, 5, 8, 8, 8, 8, 9]), so the body runs again (pass 5). - Line 7.
i == 0 or stamps[i] != stamps[i - 1]is true (iis3andstampsis[3, 3, 5, 8, 8, 8, 8, 9]), so the block underif i == 0 or stamps[i] != stamps[i - 1]:runs. - Line 8.
copies = 0setscopiesto0(it was1). - Line 9.
copies < kis true (copiesis0andkis2), so the block underif copies < k:runs. - Line 10.
copies += 1changescopiesfrom0to1. - Line 11.
i += 1changesifrom3to4. - Line 6.
while i < len(stamps):tests its condition: it is true (iis4andstampsis[3, 3, 5, 8, 8, 8, 8, 9]), so the body runs again (pass 6). - Line 7.
i == 0 or stamps[i] != stamps[i - 1]is false (iis4andstampsis[3, 3, 5, 8, 8, 8, 8, 9]), so Python skips the block underif i == 0 or stamps[i] != stamps[i - 1]:. - Line 9.
copies < kis true (copiesis1andkis2), so the block underif copies < k:runs. - Line 10.
copies += 1changescopiesfrom1to2. - Line 11.
i += 1changesifrom4to5. - Line 6.
while i < len(stamps):tests its condition: it is true (iis5andstampsis[3, 3, 5, 8, 8, 8, 8, 9]), so the body runs again (pass 7). - Line 7.
i == 0 or stamps[i] != stamps[i - 1]is false (iis5andstampsis[3, 3, 5, 8, 8, 8, 8, 9]), so Python skips the block underif i == 0 or stamps[i] != stamps[i - 1]:. - Line 9.
copies < kis false (copiesis2andkis2), so the else block runs. - Line 13.
moves += len(stamps) - i - 1changesmovesfrom6to8. - Line 14.
del stamps[i]takes8out ofstamps, from position 6: the item after it moves up one place. The third 8 goes the same way, and the two readings after it move up one place. - Line 6.
while i < len(stamps):tests its condition: it is true (iis5andstampsis[3, 3, 5, 8, 8, 8, 9]), so the body runs again (pass 8). - Line 7.
i == 0 or stamps[i] != stamps[i - 1]is false (iis5andstampsis[3, 3, 5, 8, 8, 8, 9]), so Python skips the block underif i == 0 or stamps[i] != stamps[i - 1]:. - Line 9.
copies < kis false (copiesis2andkis2), so the else block runs. - Line 13.
moves += len(stamps) - i - 1changesmovesfrom8to9. - Line 14.
del stamps[i]takes8out ofstamps, from position 5: the item after it moves up one place. - Line 6.
while i < len(stamps):tests its condition: it is true (iis5andstampsis[3, 3, 5, 8, 8, 9]), so the body runs again (pass 9). - Line 7.
i == 0 or stamps[i] != stamps[i - 1]is true (iis5andstampsis[3, 3, 5, 8, 8, 9]), so the block underif i == 0 or stamps[i] != stamps[i - 1]:runs. - Line 8.
copies = 0setscopiesto0(it was2). - Line 9.
copies < kis true (copiesis0andkis2), so the block underif copies < k:runs. - Line 10.
copies += 1changescopiesfrom0to1. - Line 11.
i += 1changesifrom5to6. - Line 6.
while i < len(stamps):tests its condition: it is false (iis6andstampsis[3, 3, 5, 8, 8, 9]), so the loop ends. - Line 15.
print(stamps, moves, "moves")prints[3, 3, 5, 8, 8, 9] 9 moves. That was the last line, so the program ends. Three deletions moved 6 + 2 + 1 = 9 readings. When every reading shares one minute, each deletion moves almost the whole list, about n²/2 moves in all.
"""The first answer most people write: delete each reading that is one too many, where it stands.
Deleting from a Python list moves every later item one place left, so the function also counts those moves.
"""
def keep_at_most_slow(stamps, k):
"""Keeps at most k readings per minute; returns the new length and the number of items moved."""
moves = 0
i = 0
copies = 0 # copies kept so far of the minute stamps[i - 1]
while i < len(stamps):
if i == 0 or stamps[i] != stamps[i - 1]:
copies = 0
if copies < k:
copies += 1
i += 1
else:
moves += len(stamps) - i - 1 # del shifts every item after position i
del stamps[i]
return len(stamps), moves
for stamps, k in [([3, 3, 3, 5, 8, 8, 8, 8, 9], 2), ([4, 4, 4, 7], 1), ([1, 2, 2], 0)]:
log = list(stamps)
w, moves = keep_at_most_slow(log, k)
print(f"k = {k}: {stamps} -> {w}, kept {log}, {moves} moves")
print()
print("The worst case: every reading in the same minute, k = 1")
for n in [1_000, 2_000, 4_000, 8_000]:
w, moves = keep_at_most_slow([7] * n, 1)
print(f"n = {n:>5,}: {moves:>12,} moves")
n = 1_000_000
print(f"n = {n:,} would need {(n - 2) * (n - 1) // 2:,} moves") Output
k = 2: [3, 3, 3, 5, 8, 8, 8, 8, 9] -> 6, kept [3, 3, 5, 8, 8, 9], 9 moves k = 1: [4, 4, 4, 7] -> 2, kept [4, 7], 3 moves k = 0: [1, 2, 2] -> 0, kept [], 3 moves The worst case: every reading in the same minute, k = 1 n = 1,000: 498,501 moves n = 2,000: 1,997,001 moves n = 4,000: 7,994,001 moves n = 8,000: 31,988,001 moves n = 1,000,000 would need 499,998,500,001 moves
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 delete_each.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
In the worst case every reading shares one minute: each deletion of this brute force: The simplest method that is plainly correct, usually trying every possibility. Said first in a round, it confirms what the problem asks, and later it checks a faster version's answers. More in “A six-step method for any coding problem” shifts almost the whole list, so the moves add up to about n²/2 and quadruple when n doubles, as the counts show. A million stamps need about 5 × 10¹¹ moves, against a million reads for one pass.
Checkpoint
Check: Deleting each extra copy, and what it costs
What to notice: kept readings only move towards the front
Two facts point to a faster method. A kept reading only ever moves towards the front, and a dropped one is never needed again, so you can copy each kept reading forward instead of deleting the others. And because the log is sorted, the copies of one minute sit next to each other, so whether a reading is one too many can be decided by looking a fixed distance back into the part already kept. Together they let every item be touched once, as the figure shows:
In the first example, every kept reading moves towards the front or stays
Text description of the diagram
The diagram has two rows of nine slots, numbered 0 to 8, for the first example with k = 2.
- The upper row is the log before the call: 3, 3, 3, 5, 8, 8, 8, 8 and 9. Brackets above it mark the copies of minute 3 in slots 0 to 2 and of minute 8 in slots 4 to 7: because the log is sorted, the copies of one minute sit side by side. The third 3 and the third and fourth 8 are dashed: they are copies too many.
- The lower row is the log after the call: 3, 3, 5, 8, 8 and 9 in slots 0 to 5, and three dashed slots, 6 to 8, whose values no longer matter.
- An arrow runs from each kept reading to its slot in the lower row: slot 0 to 0, 1 to 1, 3 to 2, 4 to 3, 5 to 4 and 8 to 5. Every arrow points straight down or to the left, so a kept reading only ever moves towards the front. The dashed readings have no arrow: once dropped, they are never needed again.
Checkpoint
Check: Kept readings move forward, copies sit together
These questions start with the idea your answers show is weakest.
Practice that adapts to you
Practise this lesson
9 questions about 9 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Reader and writer in one pass, step by step Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- A dry run on the first example, one row per reading
- The invariant, and a proof that the kept part is always right
- The code in 14 languages
- Two bugs that look right, reproduced
- Deleting against copying, measured
- Variants: filters, zeros moved to the end, and minutes seen once
- Practise: keep at most k copies
- Keep at most k copies of each value, in place
- A practice set with sample answers
- Interview questions
- To keep at most k copies of each value of a sorted array in place, a candidate's loop keeps a[r] when r < k or a[r] != a[r − k], and writes kept values forward. Is that right? Find an input that decides it, and explain what the test really checks.
- A Python solution removes duplicates from a sorted list by calling del a[i] whenever a[i] equals a[i − 1]. It passes the sample tests and times out on a hidden test with a million items. Explain the cost, estimate the time, and fix it.
- The same task on unsorted data: keep at most k copies of each value of an array of a million integers, in place, keeping the original order of the kept items. What changes, what does it cost in time and memory, and what can you offer if extra memory must be O(1)?
- Key takeaways
3 diagrams · 7 runnable examples (Python) · 1 exercise with sample tests · 3 interview answers · about 47 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- in-place compaction
- Rearranging an array so that the items to keep fill its first part, in their original order, and returning the length of that part. Whatever lies after it is left over and ignored.
References
- Using lists as queues (The Python Tutorial: data structures) (Python Software Foundation)
- The del statement (The Python Tutorial: data structures) (Python Software Foundation)
- Mutable sequence types (The Python Standard Library: built-in types) (Python Software Foundation)
- Array: length (JavaScript reference) (MDN Web Docs)
- Arrays.copyOf(int[], int) (Java SE 25 API) (Oracle)
- std::unique (cppreference.com)
- Package slices: func Compact (The Go Authors)
- Vec::truncate (The Rust Standard Library) (The Rust Project)
- Member access operators: the range operator .. (C# reference) (Microsoft)
- Arrays (PHP manual): array assignment copies the value (The PHP Group)
- The Swift Programming Language: functions, in-out parameters (Swift.org)
- List.length (Dart API reference) (Dart (Google))
- IntArray.copyOf (Kotlin standard library) (JetBrains)
- Array#slice! (Ruby documentation) (Ruby)
- Naming/MethodParameterName (RuboCop documentation) (RuboCop)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress