【算法】双指针与滑动窗口四二分——找点与找边界两类模板与判定设计三课摘要双指针系列第四篇收官二分专题。开篇先立两类模板找点lrmid±1 是终点、mid 出局与找边界lr 保 mid 收缩 是路标、mid 可能是答案——循环条件与收缩方式是一组拆开单换一个就是死循环或漏格子。然后三课LC704/35裸题判决书的新形态——指针的最终位置是所有判决的汇总以及模板是抵达同一不变量的不同路径LC34双二分连发“幸存者要验明身份”——漏了一个守卫target 不存在时输出垃圾旋转三部曲 LC153/33/81判定设计三课锚点选择、哪半有序 值域两端、等值退化。附一个三犯病历值域半边——排除式思维总漏端点药方是包含式两端卡。首做 vs 重做的对比数据放在结尾框架生效的实证。前置阅读双指针与滑动窗口一框架总纲——三类问题、一个原理与判决书。配套代码仓库按题号分目录https://github.com/a18792721831/studyleetCode【算法】双指针与滑动窗口四二分——找点与找边界两类模板与判定设计三课【算法】双指针与滑动窗口四二分——找点与找边界两类模板与判定设计三课摘要1. 两类模板先分清再动笔2. 第一课 LC704/LC35裸题、幸存者身份与殊途同归3. 第二课 LC34幸存者要验明身份4. 第三课旋转三部曲——判定设计的三次升级4.1 LC153 寻找旋转数组最小值锚点选择4.2 LC33 搜索旋转数组哪半有序 值域两端4.3 LC81 旋转数组含重复等值退化5. 三犯病历值域半边6. 首做 vs 重做框架生效的实证7. 二分速查表总结参考资料1. 两类模板先分清再动笔二分所有题的第一问不是怎么写是找点还是找边界——它决定整个模板找点LC704/33/81找边界LC35/34/153问题形态target 存在吗 / 在哪第一个/最后一个满足条件的位置mid 的角色判完就出局mid 本身可能是答案不能排除循环条件l rl r收缩方式mid±1mid 出局r mid保 mid/l mid1收尾循环内 return循环外return nums[l]lr 即答案循环条件与收缩方式是一组三条配对铁律lr配mid±1每格轮流当 mid全覆盖lr配rmidmid 向下取整保证mid rrmid严格缩小——不死循环l mid是毒药l1r时midllmid原地转圈——l 方向的收缩只能配mid1模板错配的代价不是报错是逼你写出自己跟不动的逻辑——我首做 LC153 时把找点模板lrmid±1硬套在找边界题上为了绕开mid 可能是答案不能排除的矛盾判定条件越缠越复杂三个变量两两比较最后缠出一个恒假条件else 分支的前提与条件自相矛盾永不执行。事后看那个分支死掉反而是幸运——它要是活的错误方向早把我带沟里了。2. 第一课 LC704/LC35裸题、幸存者身份与殊途同归LC704 二分查找是找点模板的裸题六行代码没什么可说的。值得说的是它的判决书——二分的排除论证长什么样nums[mid] target 时数组有序 ⟹ mid 右侧的所有值 nums[mid] target nums[mid] target 时对称 ⟹ 每次被排除的那一半每一个元素都被有序性证明了 ≠ targetLC35 搜索插入位置是找边界模板的裸题target 不存在时返回插入位置——“第一个 target 的位置”。这题我用了条混合的路子找点模板吃光区间退出后return l——凭什么判决书的新形态每次 l mid1都携带一份判决nums[mid] target 每次 r mid-1都携带一份判决nums[mid] target ⟹ 循环退出时l 左侧的所有格子已被证明 target ⟹ l 就是第一个 target 的位置 插入点指针的最终位置不是碰巧停在那的——它是所有判决书的汇总。return l能对靠的是这个不变量不是运气。这题还有个值得记的认识标准找边界模板lrrmid保 mid和我的混合解法殊途同归——两条路退出时 l 的语义完全相同。模板不是圣旨是抵达同一个不变量的不同路径——能自己换算模板才算真的懂了模板。3. 第二课 LC34幸存者要验明身份有序数组中 target 的第一个和最后一个位置。[5,7,7,8,8,10], 8→[3,4]。这题 LC35 × 2左边界是第一个 target原样右边界是它的镜像“最后一个 target”。首做三版线性扩张越界 panic → 死循环嵌套死代码 → 双二分终版重做两版两段病历都值钱首做第一版找到 target 后用 while 向两边线性扩张——[8,8,8,...,8]直接退化为 O(n)二分白做扩张时nums[start-1]没有墙答案贴下标 0 时nums[-1]panic。边界也要二分找——找到 target 不停是找点思维的最后残留找边界的是路标继续往边界压不是出口。重做版核心逻辑一步到位左边界保 mid、右边界出 mid——上次卡三版的地方一稿全对但丢了首做终版里的守卫ifnums[l]!target{return[]int{-1,-1}}没有它target 不存在时输出垃圾[1,0]。注释里那句此时 l r 一定是第一个 target是错误断言——lr的身份是第一个 target 的位置是不是 target 还差一格验证。这就是判决书规范的第二条排除要有判决幸存者也要验明身份——循环退出时指针停在哪、那格是什么语义、是不是答案三问缺一不可。顺带一笔首做版的len0/1特判在重做版里删了——l r的循环天然覆盖。特判是结构缺陷的补丁结构对了特判蒸发。4. 第三课旋转三部曲——判定设计的三次升级模板不变变的只是一次比较能买到多少信息。三道题是判定设计的完整课程表。4.1 LC153 寻找旋转数组最小值锚点选择旋转数组从 mid 切开至少一半是有序的唯一的断崖只能落在其中一半。判定哪半有序只需要一次比较——但和谁比锚点选nums[r]不选nums[l]nums[mid] nums[r] ⟹ 断崖横在 mid 和 r 之间 ⟹ 最小值严格在 mid 右边mid 出局 nums[mid] nums[r] ⟹ [mid, r] 无断崖 ⟹ 最小值在 mid 或其左边mid 保留锚点若是nums[l]未旋转数组里nums[l]本身就是最小值mid 比 l 大既可能表示 mid 在右段、也可能表示整个数组没旋转——一次比较买不到判定歧义。锚点选择的判据就一条比较的结果必须无歧义地指向一个收缩方向。终版六行forlr{mid:(lr)/2ifnums[mid]nums[r]{lmid1}else{rmid}}returnnums[l]4.2 LC33 搜索旋转数组哪半有序 值域两端有了 153 的哪半有序加上值域判断就是 33。首做七用例七错起步重做时三问流程在纸面上就抓出了老病详见第 5 节。终版结构forlr{mid:(lr)/2ifnums[mid]target{returnmid}ifnums[l]nums[mid]{// 左半有序lmid 时天然有序ifnums[l]targettargetnums[mid]{// 值域两端都卡rmid-1}else{lmid1}}else{// 右半必有序ifnums[mid]targettargetnums[r]{// 两端都卡lmid1}else{rmid-1}}}两个细节各值一行注释判定用不是——区间缩到两格时lmidnums[l]nums[mid]同一个格子严格会走错分支值域nums[l] target target nums[mid]——mid 端开着判过已出局、l/r 端闭着还没判过必须留在候选里。4.3 LC81 旋转数组含重复等值退化重复元素让nums[l] nums[mid]可能意味着断崖藏在等值段里[1,1,1,0,1]左半 [1,1,1] 判有序实际 [l, mid] 里的断崖被等值遮住了——比较买不到信息。判决书规范的第三条登场ifnums[l]nums[mid]{// 等值遮住断崖无法判定哪半有序l// nums[l] nums[mid] ≠ target刚判过扔掉不丢答案continue// 退化只排除一个已知非答案的格子}最坏情况全等数组退化为 O(n)——这是这题的下界退化不是妥协是数学极限。LC153 首做时我在和之间犹豫过等价性81 给出了答案元素互不相同时二者等价有重复时必须单独处理——等号在约束变化时会分家。这题还有一条工程教训我明知该拿 33 的底稿加退化分支还是从零重写了一套判定——结果 33 版用血换来的两样东西值域两端、循环条件配对无声蒸发五个普通用例反而翻车。改代码用加法不用重写旧版本的守卫是血换的要么带上要么说明为什么删。5. 三犯病历值域半边一个病值得单独开节因为它犯了我三次、每次换一件马甲第一犯LC33 首做判定写 nums[mid] target漏了 target nums[l] 的情况 → 主用例 [4,5,6,7,0,1,2] 找 0 当场翻车0 不在左半值域 [4,7) 里 第二犯LC81 首做重写换了一套判定体系值域又只剩 mid 一端 → [5,6,0,1,2] 找 6 翻车6 nums[r]2不在右半值域 第三犯LC33 重做的思考题规则表述nums[mid] target 取左边 → 纸面上被抓没进代码病根诊断我用排除式思维“什么条件下丢这半”时排除条件只写了 mid 一端——忘了target 越过 nums[l]/nums[r] 那一端同样排除这半。药方是改用包含式每半问一句target 在不在这半的值域里——nums[l] target nums[mid]两端天然都写手就不会漏。第三犯被纸面抓住是三问流程的胜利首做时这个病要七个用例实测才暴露重做时在动笔前的思考题阶段就被摁住了。两分钟的问答省一整轮提交-翻车-修复。6. 首做 vs 重做框架生效的实证这批题我做了两轮——首做框架地图建立之前和重做三大类框架 三问流程之后题首做重做LC704首做阶段跳过一版过LC35首做阶段跳过一版过混合解法殊途同归LC34三版线性扩张 panic → 死循环嵌套死代码 → 终版两版核心一步到位只丢守卫LC153两版模板错配出恒假条件一版过LC33两版七用例七错起步思考题抓病 一版全绿LC81两版未过死循环 重发明丢细节豁免判定设计已修完思维惯性强坑没有白踩但坑要挂到框架的钩子上才算资产——散着放是挫折挂起来是检查表。值域半边三犯、模板错配、lmid死循环、幸存者不验身份全部在总纲篇的检查表里有自己的行。这就是本系列第一篇总纲存在的意义也是我从题目做了不少、原理说不出里爬出来的路。7. 二分速查表问题判据/口诀出处找点还是找边界mid 判完 能不能出局能→找点模板不能→找边界模板全类循环条件与收缩成组配对lr↔mid±1lr↔rmidlmid是毒药153/34值域判断包含式两端卡nums[l] target nums[mid]——mid 开、l/r 闭33三犯病历判定锚点比较结果必须无歧义指向一个收缩方向153 锚 nums[r]33 锚 nums[l] vs nums[mid] 配 153/33等值买不到信息nums[l]nums[mid]→ 退化l判决书它 ≠ target扔掉安全最坏 O(n) 是下界81幸存者身份循环退出时三问停在哪 / 什么语义 / 是不是答案——差一格验证nums[l] ! target守卫35/34找到 target 停不停找点 是出口找边界 是路标继续压向边界34特判结构对了特判蒸发lr循环覆盖单元素/空数组34/33改代码加法不重写旧版守卫是血换的81手推工具判定表先写全四格再翻译排除式思维易漏端点改包含式33 重做总结二分篇收官整个双指针系列四篇走完。这题群的两个最后收获二分是判定设计的艺术不是模板背诵。两类模板一天就能背会但每道题真正的工作量在判定锚点选谁153、哪半有序怎么判33、等值怎么退化81——每次比较要买多少信息决定了代码的形状。判定设计的元判据只有一条比较的结果必须无歧义地指向一个收缩方向买不到就退化退化也救不了就换武器滑窗篇的负数死亡证明是同一句话的滑窗版。重做是检验框架的试金石。六道题首做平均两版多重做四道一版过、一道两版、一道豁免——提升的不是熟练度是病在纸面阶段就被抓出来的能力。三问流程哪个大类、哪个模板、判决书是什么把凭感觉写换成对着地图点名值域半边这种三犯老病第三次连代码都没碰到就被摁死在思考题里。系列四篇至此闭环总纲立框架滑窗讲吃进、判定、吐出相向讲比较、排除、收缩二分讲找点与找边界。三门算法的账也合上了——回溯在决策树上走路DP 把树折叠成表双指针把表折叠成两个指针。下一个系列见。参考资料LeetCode 704. 二分查找LeetCode 35. 搜索插入位置LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置LeetCode 153. 寻找旋转排序数组中的最小值LeetCode 33. 搜索旋转排序数组LeetCode 81. 搜索旋转排序数组 II双指针与滑动窗口一框架总纲——三类问题、一个原理与判决书双指针与滑动窗口二滑动窗口——吃进、判定、吐出双指针与滑动窗口三相向双指针——比较、排除、收缩版权声明本文为博主原创文章遵循 CC 4.0 BY-SA 版权协议转载请附上原文出处链接和本声明。