LCR 173:在点名(二分查找) —— 题解

👋 欢迎阅读

🎯 欢迎来到「点名」题解之旅! 本文将带你从"点名时发现学号缺了一个"这一直观场景出发,深入理解二段性二分 的巧妙运用,并掌握如何比较元素与下标是否相等定位缺失的学号

在开始之前,建议你先:

  • 了解题目背景 :这是 LCR 173 题,给定递增 数组 records,其元素本应为 0 ~ n 的连续整数,但缺失了一个数字 ,找出它。本质上,缺失位置之前 满足 records[i] == i之后 满足 records[i] != i,问题转化为二分找到第一个"下标与值不相等"的位置

  • 明确学习目标 :掌握下标-值比较二分 ,理解缺失在中间与缺失在末尾 两种情形的区分,并熟练处理单元素数组等边界情况。

  • 准备好环境 :建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 records = [0,1,2,3,5] 输出 4records = [0,1,2,3,4] 输出 5)。

本文将从问题转化、下标比较、区间收缩、结果校验 到代码实现,层层递进。即使你对二段性二分 还不熟悉,我们也会从"谁和座号对不上,谁就是缺席者"这一直觉出发,让你轻松抓住核心思想 ------下标值相等为左段,不等即缺失。现在,让我们一起二分点名,找出那个缺勤的学号吧! 📋🎯

一.题目

LCR 173. 点名 - 力扣(LeetCode)

二.做题思路

一、问题分析(前置分析)

  • 题目要求:在递增数组 records 中找出缺失的那个整数 (元素本应为 0 ~ n 连续,缺失一个)。
  • 关键约束:数组严格递增 ;元素范围 [0, n+1]缺一个 ;缺失数字可能在中间 也可能在末尾
  • 核心思路:利用二段性 ------缺失位置前满足 records[i] == i,缺失位置后满足 records[i] == i + 1(整体右移一位),二分找第一个 records[i] != i 的位置

二、算法策略(下标-值比较二分)

核心步骤:

  1. 初始化区间n = records.size() - 1(数组最后一个下标)、left = 0right = n,在 [0, n] 上二分。
  2. 二分收敛while (left < right)mid 下取整left + (right - left) / 2)。
  3. 下标比较
    • mid == records[mid] → 前 mid+1 个元素全部正确,缺失在右半,left = mid + 1
    • mid != records[mid] → 缺失在左半(含 mid),right = mid
  4. 结果校验 :收敛后若 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 + 1right = mid(下取整保证 mid < right)均严格缩小,不会死循环

四、实现细节(边界防护)

  • 初始化:n = records.size() - 1数组最后一个下标 ,非元素个数)、left = 0right = 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;(末尾缺失校验)。

五、返回值(目标映射)

  • 返回 leftleft + 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] == ii <= 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],其余收敛逻辑不变,即可适配任意偏移。

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

相关推荐
重生之后端学习1 小时前
438. 找到字符串中所有字母异位词[中等]✅
开发语言·数据结构·算法·leetcode·职场和发展
渡之1 小时前
ArduPilot LowPassFilter 深度解析
算法·无人机
Lazionr2 小时前
模板进阶:深入理解模板机制
c++·windows
ting94520009 小时前
Humalike X Hermes 深度技术剖析:单指令注入群聊社交智能的底层架构、算法与跨 IM 平台实现
人工智能·算法·架构
豆沙沙包?9 小时前
C++~~~stack容器、queue容器、list容器(p45-P56)
c++·windows·list
吴声子夜歌9 小时前
Java面试——算法
java·算法·面试
evans在进步9 小时前
LeetCode 64:最小路径和——Java 原地动态规划详解
java·leetcode·动态规划
Frank_refuel9 小时前
【C++八股】面向对象
开发语言·c++
h_a_o777oah9 小时前
【图论】Tarjan 缩点:解决有向图中环的问题
c++·算法·图论·acm·强连通分量·缩点·tarjan