ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

二分查找边界怎么写?从704到34彻底搞懂LeetCode高频题

二分查找边界怎么写?从704到34彻底搞懂LeetCode高频题 1. 为什么背熟二分模板笔试时还是写不对边界聊到二分查找很多人的第一反应是“这不简单吗left、right、while循环、mid更新五年前就会了。”但真到LeetCode上做题尤其是一周刷个二三十题的时候你会发现一个很诡异的现象——代码看着完全没问题一提交就是死循环或者答案越界。这个现象的根源不在于你的逻辑思维能力而在于二分查找其实有两种完全不同的思维模型绝大多数教程把它们混在一起讲导致大家脑子里装的是一个“杂交模板”写的时候全凭手感自然就翻车。我用大白话解释一下这两种模型模型一查找某个确定存在的值。比如数组[1, 3, 5, 7, 9]让你找5的下标。这种场景下你只要找到一个位置它的值恰好等于目标值就可以立刻返回。这是最原始的二分查找也是大学课本里教的那种。模型二查找满足某个条件的边界。比如“第一个大于等于目标值的位置”“最后一个小于等于目标值的位置”。这种场景下整个数组里可能压根没有目标值或者目标值出现多次你需要返回的是一个“边界位置”而不是“某个值是否存在”。LeetCode上的二分题百分之八十以上考的是第二种模型。比如34. 在排序数组中查找元素的第一个和最后一个位置表面上是找值实际上是找左右边界。如果你一直用模型一的思维去写模型二的题就会出现一个经典问题找到目标值就return结果左边还有相同元素你返回的那个位置压根不是要求的答案。这也是为什么很多人在做704. 二分查找的时候觉得信心满满一转到34题就懵了。因为704是纯粹的模型一而34是模型二。所以这篇文章我想得很清楚不打算从头到尾把所有二分题过一遍那样没有意义。我更想做的事情是以LeetCode高频的二分查找题为例把“边界该怎么写、为什么这么写、什么时候该用左闭右开、什么时候用闭区间、check函数怎么设计、二分的到底是什么东西”这几个问题彻底聊透。你掌握的是判断方法而不是背题。2. 最经典的模板题704题背后真正的坑先看最入门的704. 二分查找题目长这样给定一个升序整数数组nums和一个目标值target找到返回下标找不到返回-1。很多人一上来就写这种int left 0; int right nums.length - 1; while (left right) { int mid left (right - left) / 2; if (nums[mid] target) { return mid; } else if (nums[mid] target) { left mid 1; } else { right mid - 1; } } return -1;这段代码本身没问题能过704。但问题在于你只会在这一题上用这种写法换个题就不知道怎么改了。我见过太多人刷了十几道二分题写出来的while条件一会儿是left right一会儿是left right左边界更新一会儿是mid 1一会儿是mid全看运气。2.1 先搞清楚while条件到底在表达什么left right和left right的区别不是“少写一个等号”那么简单它意味着你对“搜索区间还有没有意义”的定义是完全不同的。left right你用的是闭区间也就是[left, right]。只要左边界不超过右边界就说明搜索区间里还有一个元素没有被检查到。当left right的时候区间里还有一个元素需要继续判断。left right你用的是左闭右开区间如果right初始值是nums.length的话或者说明你约定“当left和right相遇的时候就退出循环”。这时候left和right相遇的那个位置通常是一个“候选答案”你退出循环后还需要额外确认一次。这两种写法没有绝对的对错都可以用。但问题是你不能混着用。你是在写代码不是在抽签。我见过有人right初始化成nums.length - 1然后while条件写left right更新时left mid 1、right mid - 1。这种写法在某些时刻会漏掉边界答案时对时错特别坑。2.2 704题的正确打开方式从“找到”到“逼近”虽然704是模型一但我想建议你一开始就用模型二的思维来做它。也就是不要等nums[mid] target才return而是把“寻找第一个大于等于target的位置”当作目标。伪代码是这样int left 0; int right nums.length; // 注意这里是length不是length-1 while (left right) { int mid left (right - left) / 2; if (nums[mid] target) { left mid 1; } else { right mid; } } if (left nums.length nums[left] target) { return left; } return -1;这段代码的思维是整个数组被拆成两部分左边是小于target的右边是大于等于target的。我要找的是这个分界点。循环结束后left指向的位置就是第一个大于等于target的位置。如果它恰好等于target那就找到了如果不是说明数组里没有target。你仔细品一下这个写法它和前面那段“直接找相等值”的写法本质区别在哪在于你不再对“等号”敏感了。你每轮判断的核心问题从“当前mid是不是答案”变成了“mid应该归到哪一边”。这种思维方式才是后面所有变体题的基础。我在实际刷题中98%的二分题都采用这种“找分界点”的思维只有极少数题目确实需要“值等于目标就返回”的短路逻辑。从一开始就先入为主地按分界点思维练后面遇到35. 搜索插入位置、34题这一类题你不需要额外切换思维直接就写出来了。2.3 为什么mid left (right - left) / 2这个写法是个老生常谈。写成(left right) / 2在Java和C里当数组特别大、left和right都接近int上限时相加会溢出变成一个负数然后你就得到了一个负的mid直接数组越界。但我想补充一个更深的点mid left (right - left) / 2这个写法取的其实是下中位数。当区间里有偶数个元素时它偏向左边。比如left2、right3mid(23)/22取的是左边的那个。这个细节很重要。当你写left mid或者right mid这种更新逻辑时如果恰好区间长度为2、mid取到了左边界可能会造成死循环。比如left2, right3mid2条件判断后left mid那么left还是2下一次循环还是2死循环。所以当你遇到死循环问题的时候第一反应应该是检查是不是该用上中位数mid left (right - left 1) / 2。这个技巧在后面找左边界、右边界的题里会频繁用到先记下来。3. 从704到34左右边界的写法差异一篇文章讲透34. 在排序数组中查找元素的第一个和最后一个位置这道题是二分查找从“入门”到“分水岭”的经典题。题目的场景很简单数组里有重复元素让你返回目标值第一次出现和最后一次出现的位置。找不到就返回[-1, -1]。比如nums [5,7,7,8,8,10], target 8答案应该是[3,4]。这题的难点就在于你得用两套几乎对称但又不一样的思路去找左右边界。不能靠“找到一个8之后往左往右线性扩展”那样最坏情况是O(n)虽然题目数据量小能过但你来做这道题的目的就没了。3.1 找左边界第一个大于等于target的位置左边界的定义是数组中第一个出现target的下标等价于“第一个大于等于target的元素位置”。套用前面704分界点的做法找左边界就是int left 0; int right nums.length; while (left right) { int mid left (right - left) / 2; if (nums[mid] target) { left mid 1; } else { right mid; } } // 循环结束后left就是第一个大于等于target的位置这个逻辑的核心是把数组中所有小于target的元素全部丢弃剩下的区间就是大于等于target的部分逐渐收缩最后left指向的就是分界点。判断一下如果left nums.length说明整个数组都小于target直接返回-1。如果nums[left] ! target也返回-1。只有nums[left] target时left才是左边界。这里有一个细节值得注意为什么right初始化为nums.length而不是nums.length - 1因为我们可能遇到“target比数组中所有元素都大”的情况。如果right初始化为nums.length - 1最终left会指向nums.length - 1此时无法区分“真的在最后一个位置找到了”还是“所有元素都小于target”。而把right指向数组末尾的“虚拟位置”可以优雅地处理这种越界情况让代码不需要额外判断最后统一检查left是否越界即可。3.2 找右边界最后一个小于等于target的位置或者第一个大于target的位置减一右边界的定义是数组中最后一个出现target的下标。有两种思路思路A找第一个大于target的位置然后减一。int left 0; int right nums.length; while (left right) { int mid left (right - left) / 2; if (nums[mid] target) { left mid 1; } else { right mid; } } // left指向第一个大于target的位置left - 1就是右边界 int rightBound left - 1;这个写法非常对称和左边界几乎一模一样只是把nums[mid] target改成了nums[mid] target。因为我们要找“大于target”的分界点所以等于target的元素要被分到左边被丢弃出去。思路B直接找最后一个小于等于target的位置但这种写法要注意中位数问题。int left 0; int right nums.length - 1; while (left right) { int mid left (right - left 1) / 2; // 注意这里要取上中位数 if (nums[mid] target) { left mid; } else { right mid - 1; } }这里为什么必须让mid取上中位数因为当left更新为mid的时候如果mid还是下中位数偏向left一侧left和right相邻时会发生死循环。举个例子left0right1下中位数为0如果nums[0]targetleft0死循环。但取上中位数mid1如果nums[1]targetleft1循环正常结束。我在第一次写这道题的时候就掉进过这个坑。对着一组测试数据debug了半天最后发现是取中位数方向的问题。从那以后我就养成一个习惯只要写left mid这种更新方式必须同步确认mid是上中位数。这两种思路没有高下之分但我个人更推荐思路A因为它的左右边界写法对称不容易记混。思路B虽然代码短但对中位数的要求极高稍不留神就死循环。3.3 为什么left和right的语义统一才是关键很多题解会用“左右闭区间”“左闭右开”等术语来解释但对初学者来说这些术语反而制造了理解障碍。我建议你先不纠结术语只牢牢记住一个概念你的代码每一轮都在做一道判断题——当前mid属于答案左边的阵营还是属于答案右边的阵营。通过丢弃一半不断缩小搜索范围。找到左边界的过程本质上是回答“这个位置有没有可能成为左边界的候选”如果不能理解这个“候选区”思想那你只能靠背模板。一旦题目稍加变形比如35. 搜索插入位置、658. 找到K个最接近的元素你就不知道怎么套了。35. 搜索插入位置其实就是704的翻版要求你找“第一个大于等于target的位置”找不到就返回应该插入的位置。你看如果你用的是“找分界点”的写法这题和你平时写的左边界代码几乎没有区别。你不需要额外想“如果返回值是-1怎么办”因为左边界的位置天然就是插入位置。这就是我一直强调“用模型二思维训练704”的原因。你解决了一道704其实把后面十几道题都顺带解决了大半。4. check函数二分查找真正的灵魂从875爱吃香蕉的狒狒说起很多人刷到一定量的时候会遇到一个瓶颈数组不是排序的甚至没有数组你怎么二分比如875. 爱吃香蕉的狒狒也就是LeetCode 073题目说的是有一堆香蕉每堆有piles[i]根狒狒每小时最多吃k根如果某一堆剩下的少于k根它这一小时就只吃这一堆不再吃下一堆。问在h小时内吃完最小的k是多少。这题一眼看上去和“二分查找”有什么关系数组不是有序的你要找的也不是数组中的某个元素而是一个“速度值”。这里的关键就在于我们二分的对象变了不再是数组下标而是答案的值域。4.1 二分答案对值域进行二分k的取值范围是[1, max(piles)]因为就算吃得再快最快也就是一小时吃掉最大那一堆的所有香蕉再快也没有意义了。在这个区间里面k越小需要的时间越长k越大需要的时间越短。也就是说存在一个单调性随着k的增大吃完所需总时间h(k)单调递减。那么“满足h(k) h”的最小k就是我们要的答案。这个“单调性”就是二分的核心。它不要求“数组有序”只要求“存在某个值小于它的都不满足条件大于等于它的都满足条件”。有了这个性质就可以对答案值域做二分每一轮用O(n)的代价计算当前候选值是否可行把总复杂度控制在O(n log max(piles))。我见过很多人拿到这题第一反应是从小到大遍历k算时间找到第一个满足条件的k。这个方法确实简单但最坏时间复杂度是O(max(piles) * n)当piles中最大值到了10^9级别直接超时。二分的意义就在于把“线性枚举答案”变成“对数枚举答案”这是数量级的差距。4.2 check函数的设计判断当前速度是否能在h小时内吃完这题的check函数其实很直白private boolean canFinish(int[] piles, int k, int h) { int hours 0; for (int pile : piles) { hours (pile k - 1) / k; if (hours h) return false; // 提前剪枝避免无谓计算 } return hours h; }(pile k - 1) / k是向上取整的写法表示这一堆香蕉需要吃多少小时。比如一堆有10根k3那么需要吃(103-1)/3 4小时。这个向上取整很关键不能写pile / k否则会漏掉“一堆没吃完”的情况导致总时间偏小得出错误答案。canFinish的复杂度是O(n)。在二分主流程里每一轮都要调用一次canFinish所以总复杂度是O(n log max(piles))对于LeetCode上大多数数据规模是完全够用的。4.3 主流程套用“第一个满足条件的位置”的模板主流程代码长这样int left 1; int right 0; for (int pile : piles) right Math.max(right, pile); while (left right) { int mid left (right - left) / 2; if (canFinish(piles, mid, h)) { right mid; } else { left mid 1; } } return left;你可能注意到了这个主流程和前面34题找左边界的写法几乎一模一样。区别只是判断条件从nums[mid] target换成了canFinish(piles, mid, h)。这就是我想强调的核心二分查找的骨架没有变变的只是每一轮里的“判断逻辑”。这个判断逻辑在面试和刷题圈里有个专门的名字——check函数。当你遇到一道题觉得“好像可以二分”但不知道怎么二分的时候你真正要想清楚的不是while循环怎么写而是我二分的对象是什么是数组下标还是答案值域我的check函数是什么给定一个候选值怎么判断它是否满足条件单调性在哪里是不是“满足条件的答案一定在某个区间内连续分布”这三个问题想清楚了代码只是一个形式问题。4.4 类似的“二分答案”题小猿的运载能力、分割数组的最大值再举几个相关的题帮你巩固这个思维。1011. 在D天内送达包裹的能力船的运载能力capacity越小越好但每天运的总重量不能超过capacity问在D天内运完所有包裹的最小capacity。答案的右边界是所有包裹重量之和左边界是单个包裹最大重量。判断条件给定capacity能不能在D天内运完。这个check函数的思路和875类似就是顺序装配载超过capacity就开新的一天。410. 分割数组的最大值把数组分成m个连续子数组使得这些子数组各自和的最大值最小。这个题的最佳解之一就是二分答案。你对“最大值”做二分check函数就是判断给定一个上限能不能用不超过m个子数组完成分割。这种“最值的最值”类问题二分是一个非常自然的解法思路。它的核心是你不能说“我不知道答案是什么”但你可以说“给定一个答案我能告诉你它行不行”。这种能力一旦具备二分就变成了一个非常强大的武器。5. 搜索热词里的两个特殊题最长回文子串和目标和其实也能用二分你会看到热搜词里有leetcode 5: 最长回文子串和leetcode 目标和。这两题本身并不是经典的二分题但我想借它们解释一个更高级的思路二分不一定非要应用在最直观的数组问题里它也可以作为复杂问题的一种降维工具。5.1 最长回文子串里二分的“半吊子用法”5. 最长回文子串本质上是让你在一个字符串里找一个最长的回文子串。常规解法是中心扩展或者Manacher算法。但有一种不那么高效但能work的二分做法是对回文子串的长度做二分。思路是这样的如果长度为L的回文子串存在那么更短的回文子串一定存在因为它的子串就是回文。换句话说长度为L的存在性是关于L的单调递减函数需要反过来理解更长的回文存在不代表更短的一定存在不对实际上如果存在长度为L的回文那么把首尾各去掉一个字符得到长度为L-2的字符串也一定是回文如果L2。所以“是否存在长度为L的回文”这个性质在L减小的方向上是单调的。基于这个单调性你可以二分回文长度然后对每个长度去检查是否存在对应长度的回文子串。检查的复杂度是O(n)用中心扩展判断每个中心能否延伸出该长度的回文总体复杂度O(n log n)。这确实比Manacher慢但比暴力O(n^3)强得多而且思维上很能锻炼“二分答案”的感觉。不过这题在实际面试中还是建议用Manacher或者中心扩展因为最优解法不是二分。我提这个只是想说明二分不一定非要在数组里用它更是一种判断问题的框架。5.2 目标和Target Sum从DFS到二分的思维转变leetcode 目标和这题给一个数组你可以在每个数前面加正号或负号问有多少种方案让总和等于target。经典的解法是DFS记忆化搜索或者动态规划。但有一种二分思路把数组从中间分成两半分别枚举两半可以组成的所有和然后对其中一半排序遍历另一半时在排序后的数组里二分查找互补的值。这个方法叫折半搜索Meet in the Middle可以把时间复杂度从O(2^n)降到O(2^(n/2) * log(2^(n/2)))。流程大致是这样数组长度可能是20或30。如果n30直接DFS会超时或爆内存。把数组分成两半每半枚举所有正负组合的和得到两个列表listA和listB。对listB排序。遍历listA中的每个值a在listB中二分查找有几个b等于target - a。累加计数。这里的二分查找用到的就是标准“查找target的位置”的模板也可以用lower_bound和upper_bound来找左右边界一次找到所有相等的值。这个思路在真正的算法竞赛里很常见LeetCode上偶尔也会出现类似的题。它让我看到二分的另一面——二分不仅能处理“存在性”问题还能处理“计数”问题。你不需要枚举所有组合只需要在排序后的数据里快速定位多个值的位置这就是二分的强大之处。5.3 搜索热词里的“1896 - 二分查找满足条件的数”是什么你可能注意到热搜词里有一条1896 - 二分查找满足条件的数。这看起来像是某个在线评测系统比如PTA或者高校OJ的题目编号核心就是“二分查找满足条件的数”。这种类型的题在OJ里非常典型给你一个有序数组让你二分查找第一个大于等于x的数、第一个大于x的数、最后一个小于等于x的数等等。只要你把34题写熟练了这类题都是同一套思路换个判断条件而已。我建议你在练习这类题的时候不要满足于“通过了”这个结果而是额外做一件事把二分主流程抽象成一个工具函数尝试用不同的单调性条件去套它。比如写一个firstGreaterOrEqual(int[] nums, int target)再用它去实现lastLessOrEqual。当你发现在不同场景下都能复用同一套主流程时你对二分的理解就扎实了。6. 二分查找的边界死循环一张表帮你定位问题二分查找最容易出问题的不是思路而是代码细节。我把常见的死循环、越界、答案错误归结为下面几类并给出定位方法。你在刷题调不通的时候先对照这张表查一遍。症状可能原因解决办法运行超时疑似死循环mid取的是下中位数但代码里存在left mid的更新将mid改为上中位数mid left (right - left 1) / 2运行超时疑似死循环while条件是left right但循环体内没有将left或right向中间收缩检查所有分支是否都更新了left或right确保每次迭代区间长度严格减小返回结果越界right初始化为nums.length - 1但二分语义需要包含“虚拟位置”将right初始化为nums.length用左闭右开区间处理结果差1查找“第一个大于等于”时判断条件写成了nums[mid] target改为nums[mid] target把等于target的元素划入“右侧阵营”结果差1查找“最后一个小于等于”时判断条件写成了nums[mid] target改为nums[mid] target把等于target的元素划入“左侧阵营”答案错误数组是乱序的但二分前提是序列满足单调性确认题干是否有排序隐含条件或者题目是否允许先排序。乱序数组不能直接二分整数溢出写成了mid (left right) / 2改为mid left (right - left) / 2这张表实际上覆盖了我刷二分题过程中80%的报错场景。当你遇到死循环时不要急着打日志逐行看先问自己三个问题mid取的是上中位数还是下中位数和left/right的更新方式是否匹配每次迭代left或right是否一定向中间收缩了至少一格循环的退出条件是否保证left和right不会错过目标位置这三个问题里第一个是最容易忽略的。因为很多人写left mid或者right mid的时候根本没有意识到mid的方向会影响循环终止。left mid必须配合上中位数right mid可以配下中位数。这是我在多次踩坑之后总结出的规律你可以直接拿去用。7. 二分实战代码模板我建议你直接背到肌肉记忆里虽然我说过“不要背模板”但有一个最低限度的模板是值得你内化成肌肉记忆的。因为骨架如果真的熟练你就可以把注意力全部放在check函数和边界条件上而不是每次都在while循环里纠结。7.1 模板一寻找第一个满足条件的位置左边界型int left 0; int right n; // n是搜索范围右边界通常取数组长度或值域上限1 while (left right) { int mid left (right - left) / 2; if (condition(mid)) { right mid; // mid可能成为答案收缩右边界 } else { left mid 1; // mid一定不是答案跳过 } } return left;使用场景第一个大于等于target、插入位置、满足条件的最小值如875题的速度。7.2 模板二寻找最后一个满足条件的位置右边界型int left 0; int right n - 1; while (left right) { int mid left (right - left 1) / 2; // 上中位数防止死循环 if (condition(mid)) { left mid; // mid可能成为答案收缩左边界 } else { right mid - 1; // mid一定不是答案跳过 } } return left;使用场景最后一个小于等于target、满足条件的最大值。这两个模板你不需要理解它们的所有推导细节但一定要能闭着眼睛写出来并且知道每个位置的mid到底是上中位数还是下中位数。因为真正的笔试和面试里你不会有多少时间去推演边界靠的全是这种肌肉记忆。7.3 一个巧妙的记忆口诀我给学生讲的时候喜欢用一个口诀“找左不找右mid偏左走找右不找左mid偏右走。”意思是如果你要找的是“第一个满足条件的位置”那么你倾向于把mid往左缩小使用right mid此时mid取偏左的下中位数如果你要找的是“最后一个满足条件的位置”那么你倾向于把mid往右推进使用left mid此时mid取偏右的上中位数。这个口诀虽然粗糙但足以帮助你在两分钟内写出正确的主流程。真正的check函数才是你需要动脑子的地方。8. 从热门100题看二分查找的使用场景以及什么时候别用二分LeetCode热门100题里二分查找相关的题目其实不止那几个我盘点了一下至少有这些值得反复做二分查找搜索插入位置在排序数组中查找元素的第一个和最后一个位置搜索旋转排序数组寻找旋转排序数组中的最小值搜索二维矩阵爱吃香蕉的狒狒在D天内送达包裹的能力分割数组的最大值找到K个最接近的元素8.1 旋转排序数组类的二分题33. 搜索旋转排序数组是二分查找里的进阶题型。整个数组是升序的但被旋转过一次比如[4,5,6,7,0,1,2]让你在O(log n)时间里找target。这题的精髓在于数组被旋转后不再是全局有序但它是分段有序的。你取mid之后左半边[left, mid]和右半边[mid1, right]中至少有一边是严格升序的。判断出哪一边有序后就可以判断target是否落在这段有序区间内从而决定收缩方向。这种题的思维模型仍然是“分界点”思想。你要找的分界点是“有序区间和旋转点的交界”。掌握了这种分段二分后面153题找旋转数组最小值就一通百通了。8.2 矩阵里的二分74题不是“二维二分”74. 搜索二维矩阵题干是把一个有序二维矩阵展开成一行时仍然是升序的。这题有两种写法第一种是先用二分确定行再用二分确定列第二种是把二维矩阵直接映射为一维数组做一次二分。第二种更简洁int m matrix.length, n matrix[0].length; int left 0, right m * n - 1; while (left right) { int mid left (right - left) / 2; int midValue matrix[mid / n][mid % n]; if (midValue target) return true; else if (midValue target) left mid 1; else right mid - 1; } return false;这个技巧的本质是把二维坐标映射成一维坐标。写的时候注意mid / n和mid % n别搞反我用n列数去算行号而不是m行数这个细节容易错。8.3 什么时候不要用二分最后也是一个很重要的提醒不是所有“看起来有序”的题都适合二分。比如35和34这类明确有序数组的题肯定没问题但如果题目涉及链表你就得慎重。链表不支持随机访问你计算mid需要从头遍历单次二分复杂度变成O(n log n)和线性扫描没有本质区别。再比如278. 第一个错误的版本这题n的范围很大二分思路很好。但如果题目允许你调用接口的代价很大你也要考虑每轮调用的成本。二分虽然把“调用次数”从n降到了log n但如果每次调用非常昂贵你仍然要注意常数优化。还有一个反直觉的例子69. x的平方根。很多人一看这题就想到二分没错可以二分。但如果你追求极致性能牛顿迭代法会更快。二分的上限是O(log n)牛顿法在数值上接近O(log n)但常数更小而且对连续函数的收敛速度是二次的。不过对面试来说二分已经足够不用执着于最优。9. 刷题路线与复盘方法这是我踩过坑后总结的经验如果你现在正准备刷LeetCode的二分专题我给你一条比较舒服的路线从易到难入门期704、35、69。这三题帮你建立基本的二分框架。不要急着提交通过就下一题把“找分界点”的写法在纸上画几遍。边界期34、278、153。重点练习左右边界的差异、上中位数和下中位数的选择。这阶段遇到死循环是正常的每次死循环都是成长。二分答案期875、1011、410。这些题帮你建立“二分答案值域”的思维。遇到题先想想我能check吗check的代价是多少单调性怎么证明综合期33、74、658。这些题把二分的场景延伸到旋转数组、矩阵、多条件筛选锻炼你的模型转换能力。我自己的练习方法是每一题在提交通过后强制自己用两种不同的写法再写一遍。比如704我用“闭区间等号返回”写一遍再用“左闭右开分界点”写一遍。你会发现两种写法虽然都能过但思维模式完全不同。这种双写练习能帮你在面试中灵活应对各种变形。还有一个复盘技巧每次做错或者卡住超过20分钟把错误类型记下来。比如这次是“mid方向选错导致死循环”下次是“check函数的边界算错了向上取整”。一段时间后你会发现自己的错误高度集中在那几个点上针对性纠正比盲目刷题有效得多。从我个人刷了这么多题的经验来看二分查找其实是最能稳定拿到分数的题型之一。因为它不像动态规划那样需要枚举状态转移方程也不像图论那样需要建图。它只需要你拥有三个能力写干净的主循环、设计合理的check函数、判断单调性。这三个能力是可以通过有针对性的练习在两周内训练出来的。如果你现在正在准备面试我建议你把34题、875题、33题这三道题当成二分专题的代表作反复练习到能在十分钟内无错写出完整解法。当你做到这一步绝大多数面试官问到的二分题你都不会慌。
返回列表