基于排序数组中“第K个缺失的正整数”的二分查找解法
字数 3022 2025-12-23 23:17:55
基于排序数组中“第K个缺失的正整数”的二分查找解法
题目描述
给定一个严格递增的整数数组 arr 和整数 k,你需要找到并返回缺失的正整数序列中第 k 个缺失的正整数。
- 缺失的正整数定义为:不在数组
arr中,并且是正整数(从 1 开始)的那些数。 - 例如,
arr = [2,3,4,7,11],k = 5,缺失的正整数序列是 [1,5,6,8,9,10,12,...],第 5 个是 9。
解题过程循序渐进讲解
我们先从一个直观思路开始,逐步优化到高效算法。
1. 基础思路:模拟查找
最简单的想法是从 1 开始逐个正整数检查是否在数组中,若不在则计数,直到找到第 k 个缺失的数。
- 用指针
i遍历数组,用currentNum从 1 开始递增。 - 如果
currentNum等于arr[i],说明这个数存在,i前进;否则说明currentNum缺失,缺失计数加一。 - 当缺失计数等于 k 时,当前
currentNum就是答案。
这个方法的时间复杂度是 O(n + m),其中 m 是答案的值,在最坏情况下(数组元素很大,k 很大)可能接近 O(n + k),效率较低。
2. 核心观察:利用“到当前位置为止缺失的数量”
对于数组的某个位置 i(索引从 0 开始),arr[i] 这个值本身表示正整数序列中第 arr[i] 个数。
- 在
arr[i]之前(包括自身),本应有的正整数总数是arr[i]个。 - 但实际上数组到
i位置为止只有i+1个数存在。 - 所以,到
arr[i]为止,缺失的正整数的数量是:
missingCount = arr[i] - (i+1)
解释:arr[i]是当前应到的正整数,i+1是实际存在的数量,差值即为缺失数数量。
3. 例子验证公式
数组 arr = [2,3,4,7,11]:
- i=0, arr[0]=2, missingCount = 2 - (0+1) = 1(缺失了 1)。
- i=1, arr[1]=3, missingCount = 3 - 2 = 1(缺失了 1)。
- i=2, arr[2]=4, missingCount = 4 - 3 = 1(缺失了 1)。
- i=3, arr[3]=7, missingCount = 7 - 4 = 3(缺失了 1,5,6)。
- i=4, arr[4]=11, missingCount = 11 - 5 = 6(缺失了 1,5,6,8,9,10)。
4. 二分查找的引入
由于数组严格递增,missingCount 随 i 单调不减。
我们要找的是第 k 个缺失的正整数,也就是找到最小的索引 i,使得到 arr[i] 为止缺失的数量 ≥ k。
因为缺失数是连续的,如果我们找到第一个缺失数 ≥ k 的位置 i,那么:
- 如果 i=0,意味着在第一个数组元素之前就有足够缺失,答案 = k。
- 否则,答案 = arr[i-1] + (k - missingCountAt(i-1)),其中 missingCountAt(i-1) 是到 arr[i-1] 为止的缺失数。
更简单的方法:直接在 0 到 n-1 的索引范围内进行二分查找,找第一个满足arr[mid] - (mid+1) >= k的 mid。
5. 二分查找步骤
- 初始化 left=0, right=n-1。
- 当 left ≤ right 时:
- mid = (left+right)/2。
- 计算 missing = arr[mid] - mid - 1。
- 如果 missing < k,说明到 mid 为止缺失数还不够 k,答案在右侧:left = mid+1。
- 否则 missing ≥ k,说明到 mid 为止缺失数已经达到或超过 k,记录可能的 mid,并继续在左侧找更小的:right = mid-1。
- 循环结束后,left 是第一个满足 missing ≥ k 的索引(如果 left 超出数组范围,说明所有数之前缺失还不够 k)。
- 如果 left == 0,说明在第一个元素之前就缺失了 k 个,那么答案 = k。
- 否则,计算到 left-1 为止缺失了多少:missingBefore = arr[left-1] - (left-1) - 1。
- 答案 = arr[left-1] + (k - missingBefore)。
6. 实例推演
arr = [2,3,4,7,11], k=5。
- left=0, right=4, mid=2, missing=4-2-1=1<5 → left=3。
- left=3, right=4, mid=3, missing=7-3-1=3<5 → left=4。
- left=4, right=4, mid=4, missing=11-4-1=6≥5 → right=3。
循环结束,left=4。
因为 left>0,计算 missingBefore = arr[3] - 3 - 1 = 7-4=3。
答案 = arr[3] + (5-3) = 7+2 = 9。
符合预期。
7. 边界情况处理
- 如果 k 很大,超过数组最大元素能提供的缺失数,比如 arr=[1,2,3], k=5,则 left 最终会变成 n(数组长度),此时可以直接返回 arr[n-1] + (k - missingAt(n-1)),即最后一个元素之后继续数缺失数。
- 代码实现时,可以让 right 初始为 n(而不是 n-1),这样二分查找范围包含“最后一个元素之后”的位置,最后统一公式:
- 如果 left == 0,答案为 k。
- 否则,答案为 arr[left-1] + (k - (arr[left-1] - (left-1) - 1)),简化后为:
arr[left-1] + k - (arr[left-1] - left) = left + k。
惊喜的简化结果:答案 = left + k。
验证:上例 left=4, k=5 → 4+5=9。
另一个例子:arr=[1,2,3], k=5, 二分后 left=3, 3+5=8 正好是第 5 个缺失数(缺失序列 4,5,6,7,8,...)。
8. 最终算法步骤
- 初始化 left=0, right=n。
- 当 left < right:
- mid = (left+right)/2。
- missing = arr[mid] - mid - 1。
- 如果 missing < k: left = mid+1。
- 否则: right = mid。
- 二分结束后,left 就是满足条件的第一个位置。
- 返回 left + k。
9. 复杂度分析
时间复杂度 O(log n),因为只需要一次二分查找。
空间复杂度 O(1)。
10. 思考延伸
这个技巧本质上是在缺失数量的单调性上做二分查找,将“找第 k 个缺失数”转化为“找第一个缺失数 ≥ k 的位置”,然后利用索引和值的直接计算得到结果,避免了逐个枚举。这种思路在类似“有序数组中缺失元素”问题中很常见。