上篇文章:二分查找进阶:巧用"二段性"寻找极值
想象一下,班里有n个同学按学号排队,唯独少了一个人,你怎么用最快的速度把他揪出来?很多同学看到这道题(LCR173.点名),第一反应是直接从头数到尾(遍历),或者算个总和减一下(高斯求和)。这些方法没错,但如果数据量是一千万呢?优秀的工程师永远在追求极致的效率。今天我们不讲废话,直接拆解这道题的5种解法。重点教你如何抓住"数组升序"这个隐藏的王炸条件,用二分查找实现从O(N)到O(logN)的性能飞跃!
目录
[LCR 173.点名(原:剑指offer:0~n-1 中缺失的数字)](#LCR 173.点名(原:剑指offer:0~n-1 中缺失的数字))
LCR 173.点名(原:剑指offer:0~n-1 中缺失的数字)
https://leetcode.cn/problems/que-shi-de-shu-zi-lcof/


理解题意
数组原本应该包含 0 到 n 的所有整数 (共 个数字,即原本班级有 人,但数组长度为-1)。
数组是严格升序排列的。
缺失了一个数字后,数组的元素值与其对应的下标索引会发生错位。在缺失数字之前的元素,满足records[i] == i。在缺失数字之后的元素,满足records[i] == i + 1 (即records[i] != i)。
我们要找的缺失数字,实际上就是第一个出现records[i]!= i 的下标索引 i
哈希表法
算法原理
最直观的方法是利用哈希表(或额外的一个布尔数组)来记录哪些数字出现过。
1.遍历给定的records 数组,将出现的数字存入哈希表或标记数组中。
- 从 0 遍历到 n(也就是数组长度),检查哪个数字不在哈希表/标记数组中。
3.不存在的那个数字即为缺失的学号。
时间复杂度:O(N) 空间复杂度:O(N)
直接遍历找结果法
算法原理
既然缺失数字之前的元素都满足records[i] == i,我们只需从头到尾遍历一次数组,对比元素值和下标。
1.遍历数组,对于当前的下标i,检查 records[i] == i 是否成立。
2.如果发现 records[i]!= i,那么当前的下标i 就是缺失的那个数字。
3.如果遍历完整个数组都没有发现不相等的元素,说明缺失的是最后一个数字(即数组的长度n)。
时间复杂度: O(N) 空间复杂度:O(1)
位运算法
算法原理
利用异或运算(XOR)的特性:a ^ a =0,且a ^ 0= a。我们知道完整的学号应该是 0 ~ n。
1.我们将0~ n 的所有数字进行异或操作,得到一个结果 res1。
2.再将records 数组中的所有元素进行异或操作,得到另一个结果res2。
3.将res1 ^ res2,所有在数组中出现的数字都会因为成对出现而相互抵消(变为 0),最后剩下的结果就是那个唯一没有成对的数字,也就是缺失的学号。
时间复杂度:O(N) 空间复杂度:O(1)
数学(高斯求和公式)
算法原理
利用等差数列求和公式(高斯求和公式)。
1.完整的学号集合是 0 ~ (共n + 1 个数字),其和可以通过公式求得:Sum = n *(n + 1)/ 2
(注意这里的n是指数组的长度)。
2.遍历给定的records 数组,计算当前数组所有元素的实际总和 actualSum
3.缺失的数字就是理论总和与实际总和的差值: Sum- actualSum
时间复杂度:O(N) 空间复杂度:O(1)
最优:二分查找算法


算法原理
利用数组严格升序排列的特性,我们可以将 O(N)的时间复杂度降维打击到 O(log N)。二分查找是解决有序数组搜索问题的最佳选择。
我们可以根据records[i]==i这个条件,将数组划分为两部分(具有二段性):
左半部分:满足records[i] == i。这说明缺失的数字在当前位置的右侧。
右半部分:满足records[i] !=i。这说明缺失的数字就是当前位置,或者在当前位置的左侧。我们要找的,实际上就是右半部分的左边界。
执行步骤:
1.定义左右指针:left = 0,right = recordssize()- 1 。
2.当left < right 时,进行循环:
计算中间索引l:mid = left +(right - left)/ 2。
如果records[mid] == mid,说明 mid 及之前的元素都是正常的,缺失元素在右边,因此更新left = mid + 1
如果records[mid] != mid,说明缺失元素在mid 处或 mid 的左边,因此更新right =mid 。
3.细节问题(边界处理):当循环结束时left ==right,跳出循环。此时指向的元素可能是缺失值对应的位置。我们需要进行最后一次判断:
如果records[left」 ==left,说明整个数组完全没有缺失前面的数字,缺失的是最后一个,应该返回 left + 1
否则,缺失的就是当前下标left。
class Solution
{
public:
int takeAttendance(vector<int>& records)
{
int left = 0, right = records.size() - 1;
while(left < right)
{
int mid = left + (right - left) / 2;
if(records[mid] == mid) left = mid + 1;
else right = mid;
}
// 细节问题
return records[left] == left ? left + 1 : left;
}
};
本章完。