Skip to content
← All posts

Duplicate Zeros: In-Place Array Shifting

6 min read
leetcodeproblemeasyarrays

LeetCode Duplicate Zeros (problem 1089) asks you to modify a fixed-length integer array in-place. For every occurrence of zero, you duplicate it by shifting the remaining elements to the right. Any elements that would be pushed past the end of the array are simply dropped. The function returns nothing.

Given arr = [1, 0, 2, 3, 0, 4, 5, 0], the result after modification is [1, 0, 0, 2, 3, 0, 0, 4]. Each zero gets an extra copy inserted right after it, and the values 5 and the trailing 0 fall off the end.

input:1001223304455607duplicate each zero, shift rightresult:10023004
Each zero is duplicated in-place. Elements shift right, and values that fall past the end of the array are dropped.

Why this problem matters

Duplicate Zeros teaches you how to perform in-place array transformations when elements expand rather than shrink. Most in-place problems (like Remove Element or Remove Duplicates) reduce the array. This one grows certain elements, which means a naive left-to-right approach would overwrite values before you have a chance to move them. Learning to handle this by working from right to left is a technique you will use in problems like Merge Sorted Array (LeetCode 88) and other scenarios where you need to write into the same array you are reading from.

The key insight

If you try to duplicate zeros from left to right, you will overwrite elements that have not been processed yet. The fix is a two-pass approach:

  1. First pass: count the total number of zeros. This tells you how far the "expanded" array would extend beyond the original length.
  2. Second pass: walk backward from the last element, using two pointers. Pointer i reads from the original positions. Pointer j writes to the shifted positions. When j points past the end of the array, the write is skipped. When i lands on a zero, you write it twice (once for the original, once for the duplicate).

By moving right to left, every element is placed into its final position before that position is read. Nothing gets overwritten prematurely.

The solution

def duplicate_zeros(arr: list[int]) -> None:
    zeros = arr.count(0)
    n = len(arr)
    i = n - 1
    j = n + zeros - 1

    while i >= 0:
        if j < n:
            arr[j] = arr[i]
        if arr[i] == 0:
            j -= 1
            if j < n:
                arr[j] = 0
        i -= 1
        j -= 1

The first line counts how many zeros are in the array. This count determines the starting value of j, the write pointer.

We set i to the last index of the array and j to n + zeros - 1. Think of j as the index where arr[i] would land if the array were long enough to hold all duplicated zeros.

The main loop walks i backward from the end to the beginning. For each element, we first check whether j is within bounds. If it is, we copy arr[i] to arr[j]. If j is out of bounds, we skip the write because that element would fall off the end anyway.

Next, if arr[i] is zero, we need to write a second zero. We decrement j by one and, if the new j is still within bounds, place a zero there. This handles the duplication.

Finally, both i and j are decremented. The loop continues until i has processed every element in the original array.

The j pointer starts beyond the array boundary on purpose. Any writes where j >= n are effectively "phantom" writes that represent values pushed off the end. This is what makes the approach work without extra memory.

Visual walkthrough

Trace through arr = [1, 0, 2, 3, 0, 4, 5, 0] step by step. Watch how i reads from the original positions and j writes to the shifted positions, both moving right to left.

Step 1: Count the zeros. There are 3 zeros in the array.

1001223304455607

zeros = 3. This tells us the total shift: j starts at n + zeros - 1 = 8 + 3 - 1 = 10.

Step 2: Set i = 7 (last index) and j = 10 (virtual write position). Walk backward.

1001223304455607i=7

j = 10 is past the array boundary, so arr[i] = 0 is dropped. We still decrement j twice (once for the copy, once for the duplicate).

Step 3: i = 6, j = 8. arr[6] = 5. j is out of bounds, so 5 is dropped.

1001223304455607i=6

j = 8 is still past the end. This element gets pushed off. Decrement both i and j.

Step 4: i = 5, j = 7. arr[5] = 4. Write arr[7] = 4.

1001223304455647i=5j=7

j = 7 is within bounds. Copy arr[5] into arr[7]. Then i = 4, j = 6.

Step 5: i = 4, j = 6. arr[4] = 0. Write arr[6] = 0 and arr[5] = 0.

1001223304050647i=4j=5

Zero found. Write it at j = 6, then decrement j and write again at j = 5. Both copies land in bounds.

Step 6: Continue until i reaches 0. Final array is complete.

1001022334050647

All elements placed. Zeros at indices 1 and 4 have been duplicated. Values 5 and the trailing 0 were pushed off the end.

Notice how the first few iterations have j out of bounds, so those writes are skipped. As j enters the valid range, elements start landing in their correct final positions. By the time i reaches 0, every element has been placed exactly once.

Complexity analysis

ApproachTimeSpace
Two-pass right-to-leftO(n)O(1)

The first pass (arr.count(0)) scans the array once. The second pass walks backward through the array once. Total work is O(n). No extra arrays or data structures are allocated, so space is O(1).

The building blocks

This problem uses two reusable patterns that show up frequently in array problems.

In-place shifting with a virtual write pointer

The core idea is that j starts at a position beyond the array, representing where elements would go in an expanded version. By skipping writes where j >= n, you effectively "clip" the expanded result to fit the original array length. This same technique appears in Merge Sorted Array (LeetCode 88), where you merge two sorted arrays into one by writing from right to left.

Right-to-left traversal to avoid overwrites

Whenever you need to expand elements in-place (duplicating, inserting, or merging), working from right to left prevents you from clobbering unprocessed data. The pattern is simple: read from position i, write to position j, and move both pointers leftward. If j > i, you are guaranteed that the write position has already been read (or is beyond the original data), so nothing is lost.

This is the opposite of the read-write pointer pattern used in shrinking problems like Remove Element, where left-to-right is safe because the write pointer never overtakes the read pointer.

Counting to predict shift offsets

The first pass counts zeros to calculate the total displacement. Without this count, you would not know where j should start. This "count first, then act" strategy is a common setup when the transformation depends on global properties of the input rather than purely local decisions.

Edge cases

No zeros. If the array contains no zeros, zeros is 0, so j starts equal to i. Every write copies arr[i] back to the same position, and the array remains unchanged.

All zeros. Every element is zero, so every one gets duplicated. The write pointer j starts far beyond the array, and most writes are skipped. The result is the same all-zero array, since duplicating a zero just produces another zero.

Zero at the very end. The last element is zero, but there is no room to duplicate it. The j pointer for that element lands out of bounds, so the duplicate is silently dropped. The zero itself stays in its original position.

Single element. The loop runs once. If the element is zero, j starts at 1 (out of bounds for a length-1 array), so no duplicate is written. If it is nonzero, it stays in place. No special handling is needed.

From understanding to recall

This problem is easy to follow once you see the two-pass approach. The challenge is reproducing the pointer setup from scratch without hesitation. The indices i = n - 1 and j = n + zeros - 1 are the key details you need to recall. Practice writing the solution on a blank screen until the pointer initialization feels automatic. Then space out your reviews over days and weeks. That is how the pattern moves from understanding into the kind of recall you can rely on during an interview.

Related posts

Build a library of these array patterns and you will start recognizing them instantly in new problems. CodeBricks helps you organize and drill these building blocks with spaced repetition so they stick for good.