题目描述

✅ 1089. 复写零

image-20260928225631022

题意分析

把原数组中的每个零连续写两份,非零元素写一份,只保留展开后最前面的 $n$ 个元素。数组长度不能改变,需要直接修改原数组。

展开会把元素向右移动,从左往右直接写入可能覆盖尚未读取的原值。先确定哪些原元素会留在结果里,再从后往前回填,就能让写入发生在已经不需要读取的右侧。

解法:双指针从后向前写入

核心思路

[!blue]

第一趟中,i 是已统计的原元素个数,j 是这些元素复写后的虚拟长度。非零使 j 增加一,零使它增加二;直到 j >= n 时停止。此前每个元素至少贡献一格,所以 i <= j,统计过程不会提前读出数组范围。

此时找到的原前缀已经足够填满结果,后面的原元素都会落在结果范围之外,可以不再处理。将 i、j 各减一,把计数转为下标:i 指向尚未回填的最后一个原元素,j 指向它展开后的最后一个位置。

回填时先将原值写到 j,若原值为零,再把左边一格也写成零,然后一起向左推进。剩余前缀展开后不会更短,所以始终有 j >= i;原值为零时还多占一格,有 j >= i+1。因此两次写入都不会覆盖 i 左侧的未读数据,读取后再判断是否为零也安全。

第一趟的长度最多超过 $n$ 一格,恰好对应末尾的零只有第一份能留下。回填每一份前检查 j < n,越界时只略过写入,仍移动虚拟下标;这样保留下来的各值与完整展开数组的前 $n$ 位完全一致。

解题步骤

  • 前向累计非零占一格、零占两格,达到数组长度后停止。
  • 读写下标各退一位。
  • 倒序复制,零写两份,每次写前检查上界。

代码实现

class Solution {
    public void duplicateZeros(int[] arr) {
        int n = arr.length;
        int i = 0;
        int j = 0;

        // 只统计展开长度,先找到足以填满结果的原前缀。
        while (j < n) {
            if (arr[i] == 0) {
                j += 2;
            } else {
                j += 1;
            }

            i++;
        }

        i--;
        j--;

        // 从后向前回填,右侧旧值已不再需要。
        while (i >= 0) {
            // 虚拟位置可能越过数组末尾,只跳过该次写入。
            if (j < n) {
                arr[j] = arr[i];
            }

            if (arr[i] == 0) {
                j--;

                // 虚拟位置可能越过数组末尾,只跳过该次写入。
                if (j < n) {
                    arr[j] = 0;
                }
            }

            i--;
            j--;
        }
    }
}
func duplicateZeros(arr []int) {
    n := len(arr)
    i, j := 0, 0

    // 只统计展开长度,先找到足以填满结果的原前缀。
    for j < n {
        if arr[i] == 0 {
            j += 2
        } else {
            j += 1
        }
        i++
    }

    i--
    j--

    // 从后向前回填,右侧旧值已不再需要。
    for i >= 0 {
        // 虚拟位置可能越过数组末尾,只跳过该次写入。
        if j < n {
            arr[j] = arr[i]
        }
        if arr[i] == 0 {
            j--
            // 虚拟位置可能越过数组末尾,只跳过该次写入。
            if j < n {
                arr[j] = 0
            }
        }
        i--
        j--
    }
}

复杂度分析

  • 时间复杂度:$O(n)$,计数与回填各一次。
  • 空间复杂度:$O(1)$,只使用读写下标。

关键点总结

[!green]

  • 第一趟的写位置是虚拟长度,可能比数组多一。
  • 越界跳过的是写入,不是这个元素对应的指针推进。

易错点总结

[!yellow]

  • 从左向右直接覆盖,会丢失后面还需要的原值。
  • 计数结束后没有把长度转成末下标,整体位置会偏一。
  • 虚拟写位置不检查上界,尾部零截断时会越界。
  • 无零时读写下标重合,数组保持不变;全零或只有一个零时,同一套截断规则也能填满结果,不必另写分支。

相似题目

题目 难度 关联与区别
27. 移除元素 简单 原题向前压缩要保留的元素,本题向后填充扩张后的虚拟位置,防止覆盖未读数据。
88. 合并两个有序数组 简单 同样在固定数组中从后向前写入,避免扩张或合并时破坏尚未读取的内容。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/41436715
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!