题目描述

✅ 1060. 有序数组中的缺失元素

给定一个严格递增的正整数数组 nums 和一个正整数 k,从 nums[0] 开始,按从小到大的顺序找到第 k 个不在数组中的整数,并返回它。

示例 1:

输入:nums = [4,7,9,10], k = 1
输出:5
解释:从 4 开始,缺失的整数依次为 5、6、8、11……,第一个缺失的整数是 5。

示例 2:

输入:nums = [1,2,4], k = 3
输出:6
解释:缺失的整数依次为 3、5、6……,第三个缺失的整数是 6。

提示:

  • 1 <= nums.length <= 5 * 10^4
  • 1 <= nums[i] <= 10^7
  • 1 <= k <= 10^8
  • nums 严格递增。

题意分析

给定严格递增的整数数组,从首元素之后开始按整数顺序寻找不在数组中的数,返回第 k 个缺失值。k 从一开始计数,答案可以位于两个数组元素之间,也可以超过数组最后一个元素。

计数起点由 nums[0] 决定,不是固定从正整数一开始。数组中的数都已经存在,不能把某个数组元素本身当成缺失答案。

解法:缺失数量上的二分查找

核心思路

[!blue]

从 nums[0] 到 nums[i],如果整数完全连续,共应有 nums[i] - nums[0] + 1 个值;实际数组在这段内有 i + 1 个值。因此累计缺失数量为 missing(i) = nums[i] - nums[0] - i,无需真的列出缺失整数。

数组严格递增,向右走到下一元素时,缺失量增加的是两元素之间的空缺数,不会减少。所以 missing(i) 单调不减,可以二分定位第一个累计缺失数量达到 k 的位置。

先检查最后一个元素。如果 missing(n - 1) < k,数组内部的缺失数还不够,剩余答案一定在末尾之后。那里每前进一步都是缺失值,因此答案为 nums[n - 1] + k - missing(n - 1)。

否则二分第一个满足 missing(p) >= k 的位置 p。因为 missing(0) = 0 且 k 为正,这个位置不会是零,前一位置一定存在,并满足 missing(p - 1) < k。

第 k 个缺失值就在 nums[p - 1] 和 nums[p] 之间。前一位置已经累计了 missing(p - 1) 个缺失值,再从它之后向右走 k - missing(p - 1) 个整数,就得到答案。即使 missing(p) 恰好等于 k,答案也仍是 nums[p] 之前的缺失值,而不是已经存在的 nums[p]。

解题步骤

  1. 用公式计算末尾的累计缺失量。
  2. 若它小于 k,从最后一个元素向后补足剩余缺失数量并返回。
  3. 否则在下标区间中二分:累计缺失量不少于 k 时保留中点及左侧,不足时移到中点右侧。
  4. 两边相遇得到第一个达标下标,读取它前一位置的值与累计缺失量。
  5. 返回前一位置的值加上尚未计满的缺失数量。

代码实现

class Solution {
    public int missingElement(int[] nums, int k) {
        int n = nums.length;
        int missingLast = missing(nums, n - 1);

        // 内部缺失不足,剩余答案在最后元素之后。
        if (missingLast < k) {
            return nums[n - 1] + k - missingLast;
        }

        int left = 0;
        int right = n - 1;

        while (left < right) {
            int mid = left + (right - left) / 2;

            // 达标位置仍可能是第一个,保留中点。
            if (missing(nums, mid) >= k) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }

        // 从尚未达到第几项的前一元素向后补缺口。
        int before = missing(nums, left - 1);

        return nums[left - 1] + k - before;
    }

    private int missing(int[] nums, int index) {
        return nums[index] - nums[0] - index;
    }
}
func missingElement(nums []int, k int) int {
    n := len(nums)
    missing := func(index int) int {
        return nums[index] - nums[0] - index
    }

    missingLast := missing(n - 1)
    // 内部缺失不足,剩余答案在最后元素之后。
    if missingLast < k {
        return nums[n-1] + k - missingLast
    }

    left, right := 0, n-1
    for left < right {
        mid := left + (right-left)/2
        // 达标位置仍可能是第一个,保留中点。
        if missing(mid) >= k {
            right = mid
        } else {
            left = mid + 1
        }
    }

    // 从尚未达到第几项的前一元素向后补缺口。
    before := missing(left - 1)
    return nums[left-1] + k - before
}

复杂度分析

设数组长度为 $n$。

  • 时间复杂度:$O(\log(n+1))$,每次用常数时间计算一个下标的缺失量;答案在末尾之外时直接返回。
  • 辅助空间复杂度:$O(1)$,只使用二分边界和若干计数变量。

关键点总结

[!green]

  • 连续应有数量减去实际数量,得到可直接计算的累计缺失函数。
  • 缺失量单调不减,寻找第一个达到 k 的下标,而不是查找一个具体数组值。
  • 内部答案从达标位置的前一个元素补差,末尾外答案单独还原。

易错点总结

[!yellow]

  • 用下标或固定的一作为起点,会忽略题目从 nums[0] 开始计数的约定。
  • 缺失量可能连续相等,二分需要找第一个达标位置,不能命中相等就直接返回下标或数组值。
  • 省略末尾之外的分支,会在所有下标都未达到 k 时错误定位。
  • 内部答案要从前一位置补缺口,从达标元素本身继续增加会跳过正确空隙。
  • 二分成功下标至少为一依赖 k > 0;这里不能把序号当成零基。

相似题目

题目 难度 关联与区别
1539. 第 k 个缺失的正整数 简单 同样二分第k个缺失值,原题缺失范围从正整数1开始,本题以数组首元素为起始基准。
163. 缺失的区间 简单 原题输出所有缺失区间,本题利用各区间缺失数量直接定位第k个元素。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2022/64759297
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!