👋 欢迎阅读

🎯 欢迎来到「点名」题解之旅! 本文将带你从"点名时发现学号缺了一个"这一直观场景出发,深入理解二段性二分 的巧妙运用,并掌握如何比较元素与下标是否相等 来定位缺失的学号。
在开始之前,建议你先:
-
了解题目背景 :这是 LCR 173 题,给定递增 数组
records,其元素本应为0 ~ n的连续整数,但缺失了一个数字 ,找出它。本质上,缺失位置之前 满足records[i] == i,之后 满足records[i] != i,问题转化为二分找到第一个"下标与值不相等"的位置。 -
明确学习目标 :掌握下标-值比较二分 ,理解缺失在中间与缺失在末尾 两种情形的区分,并熟练处理单元素数组等边界情况。
-
准备好环境 :建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如
records = [0,1,2,3,5]输出4,records = [0,1,2,3,4]输出5)。
本文将从问题转化、下标比较、区间收缩、结果校验 到代码实现,层层递进。即使你对二段性二分 还不熟悉,我们也会从"谁和座号对不上,谁就是缺席者"这一直觉出发,让你轻松抓住核心思想 ------下标值相等为左段,不等即缺失。现在,让我们一起二分点名,找出那个缺勤的学号吧! 📋🎯
一.题目

二.做题思路
一、问题分析(前置分析)
- 题目要求:在递增数组
records中找出缺失的那个整数 (元素本应为0 ~ n连续,缺失一个)。- 关键约束:数组严格递增 ;元素范围
[0, n+1]内缺一个 ;缺失数字可能在中间 也可能在末尾。- 核心思路:利用二段性 ------缺失位置前满足
records[i] == i,缺失位置后满足records[i] == i + 1(整体右移一位),二分找第一个records[i] != i的位置。
二、算法策略(下标-值比较二分)
核心步骤:
- 初始化区间 :
n = records.size() - 1(数组最后一个下标)、left = 0、right = n,在[0, n]上二分。- 二分收敛 :
while (left < right),mid下取整 (left + (right - left) / 2)。- 下标比较:
mid == records[mid]→ 前mid+1个元素全部正确,缺失在右半,left = mid + 1;mid != records[mid]→ 缺失在左半(含 mid),right = mid。- 结果校验 :收敛后若
records[left] == left,说明缺失的是末尾数字n + 1,返回left + 1;否则返回left。示例执行过程 (
records = [0,1,2,3,5],缺失 4):
| 阶段 | left | right | mid | recordsmid vs mid | 操作 | 结果 |
|---|---|---|---|---|---|---|
| ① | 0 | 4 | 2 | 2 == 2 | 左段正确,收缩左侧 | left=3 |
| ② | 3 | 4 | 3 | 3 == 3 | 左段正确,收缩左侧 | left=4 |
| 收敛 | 4 | 4 | --- | records4=5 != 4 | 返回 4 | 4 |
三、正确性说明(简单版本)
- 二段性成立 :数组只缺一个数字,缺失位置前所有元素满足
records[i] == i,之后所有元素满足records[i] > i(整体右移),两种状态只切换一次 ,判据mid == records[mid]恰好识别分界,不会误判。- 收缩方向正确 :相等说明左段完好,缺失在右半可丢弃左段;不等说明缺失在左半(含 mid),保留 mid 向左收敛。区间单调收敛到第一个不等位置 ,不漏解。
- 末尾缺失校验完备 :若全部相等(缺失的是最后一位
n+1),二分会收敛到right = n,此时records[left] == left恒成立,返回left + 1恰好是缺失值,覆盖末尾情形。- 终止性 :
left = mid + 1与right = mid(下取整保证mid < right)均严格缩小,不会死循环。
四、实现细节(边界防护)
- 初始化:
n = records.size() - 1(数组最后一个下标 ,非元素个数)、left = 0、right = n。- 边界防护:
size == 1时(如[0])right = 0,循环不进入,records[0] == 0→ 返回 1;records[left]访问安全(left ≤ n ≤ size-1);缺失在末尾时靠校验分支兜底。- 复杂度:时间 O(log n) (每次排除一半),空间 O(1)(仅常数个变量)。
- 关键判断 :
if (mid == records[mid]) left = mid + 1; else right = mid;(二段性收敛)、if (records[left] == left) return left + 1;(末尾缺失校验)。
五、返回值(目标映射)
- 返回
left或left + 1:缺失的数字,对应题目"返回缺席的学号"。
三.代码
cpp
class Solution
{
public:
int takeAttendance(vector<int>& records)
{
int n = records.size() - 1; // 数组最后一个下标(搜索区间右端点)
int left = 0;
int right = n;
// 1. 二段性二分:比较元素与下标,找"第一个 records[i] != i"的位置
while (left < right)
{
int mid = left + (right - left) / 2; // mid 下取整,配合 right = mid
if (mid == records[mid])
{
left = mid + 1; // 前 mid+1 个元素全部正确,缺失在右半
}
else
{
right = mid; // 已出现错位:缺失在左半(含 mid)
}
}
// 2. 结果校验:区分"缺失在中间"与"缺失在末尾"
if (records[left] == left)
{
return left + 1; // 全部元素都与下标相等:缺失的是末尾数字
}
return left; // 第一个错位下标即缺失数字
}
};
四、易错点分析
难点1:n = records.size() - 1 的含义极易混淆
cpp
int n = records.size() - 1; // 注意:这是"最后一个下标",不是元素个数!
int right = n;
这里的 n 是数组最后一个下标 (搜索右端点),而缺失数字的取值范围是 [0, size]。若误把 n 当成元素个数(records.size()),right 会越界一位 ,收敛后 records[left] 访问越界 (UB)。理解"搜索区间是下标域 0, size-1,缺失值域是 0, size"是正确写对边界的前提。
难点2:判据 mid == records[mid] 的语义是"左段完好"
cpp
if (mid == records[mid])
left = mid + 1; // 跳过 mid
records[mid] == mid 说明前 mid+1 个元素全部与下标相等 (数组递增且只缺一个,错位只会在之后发生),因此缺失必在 [mid+1, right],left = mid + 1 跳过 mid 是安全的 。若误写成 left = mid,当所有元素都正确时,left 卡在 size-1 无法前进,死循环 (left < right 恒成立但 left 不再增大?实际 left=mid 且 mid==left 时区间不缩小)。
难点3:else 分支 right = mid 保留 mid 的原因
cpp
else
right = mid; // records[mid] != mid:缺失在 [left, mid] 内
records[mid] != mid 时,mid 可能是第一个错位位置 (即缺失值),也可能在其右侧,但缺失一定不在 mid 之后 (递增性:mid 之后的值都 ≥ recordsmid+1 > mid+1,错位更明显,但第一个错位在 mid 或更左)。保留 mid 向左收敛,不会漏掉第一个错位点。
难点4:末尾缺失必须靠 records[left] == left 兜底
cpp
if (records[left] == left)
return left + 1; // 全部相等 → 缺失的是 n+1
若缺失的是最后一个数字 (如 [0,1,2,3,4] 缺 5),数组内所有元素都与下标相等,二分一路 left = mid + 1 收敛到 left = size-1。此时若直接 return left 会误返回 size-1 ,必须校验 records[left] == left 后返回 left + 1------这个校验分支是"末尾缺失"情形的唯一出口,漏掉即错。
五、流程图

🎯 闭幕

🎉 恭喜你完成了「点名」问题的学习!
为了巩固知识并进一步拓展,建议你:
🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。
💡 深入思考
-
本题通过比较
records[mid]与 下标mid来判断缺失位置。为什么这种比较能揭示缺失信息? 其背后的"元素与下标对应关系"在有序无重复场景下具有什么特性? -
循环结束后,代码额外校验
if (records[left] == left) return left + 1;。这个分支处理的是什么情况? 如果不加这个校验,直接返回left,在什么场景下会出错? -
时间复杂度 O(log n),如果改用 异或运算 或 求和公式,时间复杂度也是 O(n),相比二分,哪种方案更优?为什么本题要求用二分?
📚 延伸挑战
-
如果数组 长度不固定 且缺失的数字可能出现在 任意位置(包括开头和末尾),当前的二分框架是否依然适用?
-
如果数组中的数字 不是从 0 开始(例如从 1 到 n,缺失一个),你如何修改比较条件来适配这种偏移?
如果你觉得本文对你有所帮助,欢迎:
👍 点赞 / 收藏
👤 关注作者 ,获取更多题解
💬 留言交流你的疑问或优化思路
📌 深入思考答案
-
比较
records[mid]与mid的有效性 :因为数组是 0~n 范围内的不重复升序排列,若左侧元素全部正确,则records[i] == i对i <= k恒成立;一旦出现错位,缺失数字就在该位置,此后所有元素都比下标大 1,形成"左段正确、右段错位"的二段性。 -
当
mid != records[mid]时 ,说明mid已经位于错位段,缺失一定在mid或其左侧,因此right = mid向左收缩,不会丢失缺失数字。 -
异或或求和 O(n) 比 O(log n) 慢,但更简单;本题要求二分是为了训练 二段性思维,且 O(log n) 更高效。
🔍 延伸挑战答案
-
挑战1 :若缺失位置任意(开头或末尾),只要数组仍是 0~n 的不重复升序排列,当前二分完全适用,因为二段性依然存在(开头缺失则第一个元素就不等于0,
mid比较立即触发错位分支)。 -
挑战2 :若数组从 1 开始,即应有
records[i] == i + 1,只需将比较条件改为mid + 1 == records[mid],其余收敛逻辑不变,即可适配任意偏移。
祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨