☰
leetcode面试经典150刷题实录:二分查找与二分答案详解
2026/10/10 7:29:30 网站建设 项目流程

今天是1月25日,我保持LeetCode刷题记录的第66天。如果用一句话介绍这篇文章:一份围绕“leetcode面试经典150”的刷题实录,里面有二分查找和二分答案的完整拆解、两道经典150真题的题解、一场周赛的收获,以及66天连续刷题不中断的实操经验。适合正在准备算法面试、或者已经刷了几个月却总觉得“刷一道忘一道”的人读。我不会给你灌鸡汤,只记录今天真正做过的事、踩过的坑,还有那些值得抄进笔记里的模板。

刷题打卡这件事,听起来很简单,但真正坚持到第66天,你会发现关键不是“多努力”,而是“多稳定”。今天正好是周末,原计划只做两道题收尾二分查找专题,结果一坐下来就顺着题单多写了两道,晚上还顺手参加了周赛430。于是就有了这篇比较完整的记录,标题也跟着我的固定打卡格式走:day66(1.25)——leetcode面试经典150。

1. 为什么选“面试经典150”作为刷题主线

1.1 面试经典150和热门100题怎么选

刚开始刷LeetCode的时候,我也收藏过一堆所谓“刷题路线图”,但最后真正走完一遍的,只有官方整理的面试经典150题。原因很简单:它按专题组织,每个专题的题目数量足够让你形成肌肉记忆。比如二分查找这个板块,不是只给一道题,而是把搜索旋转排序数组、寻找旋转排序数组中的最小值、爱吃香蕉的珂珂等放在一起。练完整个专题,你自然就能总结出一套通用模板,而不是“好像见过这道题,但换个说法就不会了”。

热门100题当然也是好东西,每道题都经过大量用户验证,属于高频中的高频。但它的排列逻辑更偏向“出现频率”,而不是“知识结构”。对于还没有建立完整算法框架的选手来说,直接刷热门100题容易陷入“这道题会了,下一道题又像新题”的困境。面试经典150恰好弥补了这个短板:数组/字符串、双指针、滑动窗口、哈希、区间、栈、链表、二叉树、图、回溯、二分查找、堆、动态规划,每个板块都有成体系的题目。跟着这个题单走,就像跟着教材学一遍算法,而不是被题海淹没。

1.2 第66天的实际进度:我在哪个专题

我的刷题习惯是每两周挑一个周末做专题收尾。今天第66天,刚好走在二分查找专题的末尾。上午先用了20分钟重写“搜索旋转排序数组”的模板,因为这类题的边界条件实在太容易翻车。下午继续做“爱吃香蕉的珂珂”,中间经历了超时和边界判断两个Bug。晚上参加周赛430,发现其中一道题的核心思路居然也是二分答案。整体节奏不轻松,但非常充实。

这里顺带解释一下标题里的“day66(1.25)”是什么意思。我每天打卡都固定用这个格式:第几天加日期加刷题主题。比如今天的记录就是“day66(1.25)——leetcode面试经典150”。这样做的好处是,以后回看时能清楚知道自己是在什么阶段、什么周期完成的哪些专题。很多人的刷题计划之所以半途而废,就是因为没有这种颗粒度足够细的记录。66天下来,翻着记录找题和找状态,比翻收藏夹里的题解管用得多。

2. 今日核心:二分查找与二分答案的细节拆解

2.1 先分清两个“二分”

很多人一听到二分,脑子里只有“在有序数组里找一个数”。但面试里更常考的其实是“对答案二分”。这两个概念虽然都叫二分,但解决的问题完全不同。

经典二分查找是数据结构层面的搜索:数组已经有序,通过比较中间值和目标值,每轮排除一半数据,复杂度O(log n)。它处理的是“在一个已知集合里找元素”的问题。二分答案则是优化层面的枚举:当问题要求“最小可行值”或者“最大可行值”,而这个值的取值区间很大、且可行性与这个值之间存在单调关系时,不需要从1开始一个一个试到上限,而是直接对值域进行二分,每次取中间值,调用一个check函数来判断这个中间值是否可行。

举个例子。假设你要判断一辆车最少需要跑多快,才能在h小时内跑完所有路程。速度上限可能很大,线性试速度大概率超时。但速度越快,总耗时越短,“能不能在h小时内跑完”这件事,会随着速度单调变化。所以可以二分速度,而不是逐个枚举。这就是二分答案最典型的应用场景。

2.2 爱吃香蕉的珂珂:一道二分答案的完美入门题

LeetCode 875题“爱吃香蕉的珂珂”,也就是有人开玩笑叫“爱吃香蕉的狒狒”的那道题,是二分答案入门的绝佳素材。题目描述很直白:有n堆香蕉,第i堆有piles[i]根,警卫会在h小时后回来。珂珂每小时可以吃某堆的若干根香蕉。如果她决定一小时吃k根,那么她一小时最多吃k根,而且不会在同一个小时同时吃好几堆。要求找到能在h小时内吃完所有香蕉的最小k。

这道题有三个关键点需要想清楚。

第一,每堆香蕉必须在一小时内完整处理,不能“这堆吃一半,下一个小时再回来吃剩下的一半”。因为一小时只能选一堆香蕉来吃。所以对于某一堆数量为p的香蕉,吃完它需要的最少小时数是ceil(p/k)。这里不要用循环一次一次减,直接用整除公式:(p + k - 1) // k,也就是向上取整。

第二,总耗时等于所有堆的耗时相加,也就是sum(ceil(piles[i]/k))。如果这个总耗时小于等于h,说明当前速度k可行,但可能还能更慢,所以要把搜索区间向左收缩;如果总耗时大于h,说明k太小了,必须更快,所以要把搜索区间向右移动。

第三,k的取值下界是1,上界是max(piles)。因为一旦k大于等于最大堆的数量,任何一堆都能在一小时内解决,再大的k没有任何意义。

check函数写出来是这样:

def can_finish(k): total = 0 for p in piles: total += (p + k - 1) // k if total > h: return False return True

然后对k做二分。我习惯用左闭右开区间模板:

def minEatingSpeed(piles, h): left = 1 right = max(piles) + 1 while left < right: mid = (left + right) // 2 if can_finish(mid): right = mid else: left = mid + 1 return left

为什么right要设置成max(piles) + 1?因为左闭右开区间[left, right)里,right本身是不被包含的。而答案最大可能恰好就是max(piles)。如果right直接取max(piles),当答案等于max(piles)时,left会一路逼近到max(piles),但因为区间是左闭右开,循环终止条件left < right可能出现问题。为了确保所有可能答案都落在[left, right)内,right必须比最大可能值再大一位。这是一个很典型的小坑,记不住的话容易在笔试时翻车。

2.3 怎么识别“这道题该用二分答案”

刷题多了以后,看到一道新题,第一反应不应该是“这题有没有见过”,而应该是“这题能不能二分”。这里分享三个识别特征。

特征一:题目里出现“最小速度”、“最大重量”、“至少几天”、“至多几条”这类描述,并且存在一个可以调整的数值变量X,比如速度、距离、容量、天数。这个X就是我们要二分的对象。

特征二:随着X增大,题目给出的某个指标呈单调变化。通常来说,速度越大,耗时越短;容量越大,需要的容器越少;时间越长,能完成的任务越多。这种单调性正是二分的前提。

特征三:X的取值范围很大,可能是1e9甚至更大,根本不可能线性枚举。但是check函数本身却可以O(n)甚至O(logn)地快速判断。一旦满足这三点,别纠结贪心,先问自己:“如果把这个X当作答案,能不能写一个check函数?”

今天的“爱吃香蕉的珂珂”就是标准的特征三:速度上限max(piles),数组长度可能达到10^4,如果从1到max(piles)逐个试,遇到大量数据直接超时。二分后,时间复杂度降为O(n log max(piles)),稳得很。

3. 实操记录:day66的三道题题解与踩坑

3.1 搜索旋转排序数组:等号决定生死

LeetCode 33题“搜索旋转排序数组”是面试经典150里的老熟人。题目给一个升序排列的数组,在某个未知位置旋转,比如[0,1,2,4,5,6,7]可能变成[4,5,6,7,0,1,2]。要求用O(logn)的时间找到目标值下标,找不到返回-1。

这道题的核心思想是:每次取mid后,mid的左右两侧中,至少有一侧是严格有序的。我们只需要判断target到底落在哪一侧,然后缩小区间就行。判断方法很简单:

def search(nums, target): left, right = 0, len(nums) - 1 while left <= right: mid = (left + right) // 2 if nums[mid] == target: return mid if nums[left] <= nums[mid]: # 左半边有序 if nums[left] <= target < nums[mid]: right = mid - 1 else: left = mid + 1 else: # 右半边有序 if nums[mid] < target <= nums[right]: left = mid + 1 else: right = mid - 1 return -1

这里我要特别强调一个等号问题。很多初次写这道题的人会把第一行分支写成nums[left] < nums[mid],而不是<=。在数组只有两个元素时,比如[3,1],left=0,mid=0,left和mid指向同一个位置,这种情况下单个元素本身是有序的,应该进入“左半边有序”的分支。如果漏掉等号,程序会错误地认为左半无序,从而进入右半有序分支,导致某些情况判断错误。这个等号不是洁癖,是安全问题。

我今天重写这道题时,故意先不看题解,凭记忆写了一遍。结果第一版就漏了等号,在测试用例[5,1,3]里找3的时候直接返回了-1。这个问题很隐蔽,因为大部分测试数据不会让你第一轮就遇到left == mid,但一旦遇到,基本都是非错不可。如果你也想彻底搞懂,建议把旋转数组的两个经典边界情况手动走一遍:长度2且完全反转,长度3且旋转点在中间。

3.2 爱吃香蕉的珂珂:从超时到AC的完整过程

这道题我今天做了不止一遍。第一次自己写的时候,居然先写了个线性枚举版本:从速度1开始,一个一个试,看看哪个速度能完成。数据小还好,一旦piles元素多、max(piles)大,立刻TLE。提交后看到超时,我才反应过来,这不就是典型的二分答案模板题吗?66天刷题带来的直觉就在于,看到“最小速度”加“给定时间限制”,马上就该想到二分,而不是穷举。

第二次写,我吸取教训,把check函数提取出来,不过又踩了第二个坑。计算总时间时,我习惯性地写了这样一段:

while pile > 0: pile -= k hours += 1

看起来没问题,但piles[i]最大可能有10^9,k最小是1,单堆就要循环10^9次,必然超时。必须改成数学计算:(pile + k - 1) // k,一次除法直接得到完整小时数。这里最大的教训是:不要用循环模拟除法,除非k的范围也很小。

第三次写,终于AC。完整代码放在上面2.2节里,这里就不再重复。我想多说一句关于题目保证条件的细节。本题保证h >= len(piles),因为每小时最多只能吃一堆,如果h小于堆数,无论如何都吃不完。所以代码里不需要额外特判。但如果你在笔试中遇到类似题,最好在开头加一行if h < len(piles): return -1,防止题目偷偷改条件。

3.3 周赛430:专项练习之外的另一面

晚上我抽空参加了周赛430。很多人觉得周赛是高手证明自己用的,但我觉得它更像一个“题型识别压力测试”。平时刷题可以慢慢想,周赛却逼着你快速判断:这道题考什么,有没有现成模板可以套,边界条件是什么。

周赛430中有一道题,核心思路很接近二分答案。题目背景我记不清了,但当时我看到那个“最大...使得...”的问题结构,立刻联想到今天下午写的珂珂吃香蕉。检查函数没花多久就写好了。比赛结束后,我把这道题也整理进了错题本,但没有计入经典150的打卡记录,因为它不在题单上。

这里也提醒一点:周赛的罚时机制很严格,边界条件宁可多写几个测试用例再提交,也不要抢那几十秒。我今天因为少考虑一个数据范围,白白吃了一发罚时,排名掉了一截。这个教训在面试里同样适用:面试官不看你AC多快,而看你思路是否严密。

4. 66天坚持刷题的经验与状态管理

4.1 我是怎么保持66天不断更的

很多朋友问过我怎么坚持这么久。说实话,坚持不靠热血,靠的是机制。

第一,我的最低限度是每天至少一道题,简单题也算数。状态好的时候多刷几道,状态差的时候就挑一道Easy题快速收工。这样心理压力非常小,也不可能给自己找“今天太累了不刷了”的借口。

第二,打卡格式固定,每次都记录“dayN(日期)——主题”。day66(1.25)这个标题看起来简单,但它是非常强的正向反馈。每天在笔记上写下这个编号,你会看到自己在一点点推进。这种“看得见的进度”,比收藏夹里的一百篇题解更能让人坚持。

第三,碎片时间做轻量工作。通勤时我看题解和评论区,思考某道题为什么这样解,但不写代码。真正写代码固定安排在晚上,15到30分钟,足够完成一道题。把“输入”和“输出”分开,效率会高很多。

4.2 卡题和遗忘的应对方法

卡题是常事。我给自己定了个规则:一道题卡超过45分钟,直接看题解。这不是偷懒,而是避免无效的时间黑洞。看题解之后,我会合上题解,自己重新把代码写一遍。能写出来,这道题才算过;写不出来,那就标记“待复习”,第二天再写。

遗忘同样正常。算法不是靠背题,而是靠理解题型和模板。但理解会褪色,所以我的应对办法是每周末做一次主题复习:把这个星期做过的题重新看一遍,重点重写那些当时“看题解才写出来”的题目。重写时如果能流畅AC,说明真的吸收了;如果还是卡住,就打上标记,下周再复习一次。

心态上最忌讳的就是和别人比数量。我认识有人一周刷50题,但问他某类题的通用解法,只能答“多刷就好了”。而我的笔记里,每一题都有核心思路、复杂度和踩坑记录。66天下来,通过经典150题沉淀出来的是题型模板,而不是零散题解。这个差别在面试时才真正体现出来。

5. 常见问题速查:新手刷经典150最容易踩的坑

5.1 只刷题不总结,等于白刷

刷题最容易掉进的坑,是看到AC就激动地翻下一题,从不停下来总结。过一周再看,题目倒是眼熟,但就是不会写。这里的解药很简单:每道题固定写三行笔记,分别是核心思路、复杂度、踩坑点。

今天“爱吃香蕉的珂珂”的三行笔记我直接抄在下面,你可以参考这个格式:

  • 核心思路:对速度k做二分答案,check函数计算总耗时,总耗时<=h则右边界缩小,否则左边界右移。
  • 复杂度:O(n log max(piles))。
  • 踩坑点:单堆香蕉不能跨小时吃;用(p + k - 1)//k算时间;右边界要取max(piles)+1。

这种笔记不用长,三行就够。关键是写出来,写的过程会强迫你提炼,而提炼就是理解。

5.2 死磕Hard题和追求完美

另一个极端是上来就死磕Hard题。AC不了就怀疑人生,最后连刷题的勇气都没了。我的建议很直接:如果你还在刷经典150的第一遍,不要把Hard题当主食。Medium题才是面试主力,Hard题更多是锦上添花。与其在Hard上耗两小时,不如把二分查找的几种变体贴身过一遍。

还有一点心态问题:刷题不是考试,不需要“裸奔”。你完全可以先看题解,再合上自己写。面试时也可以和面试官讨论思路。刷题阶段的核心目标是内化套路,不是证明自己不看答案也能做。把这个想通了,很多焦虑都会消失。

5.3 经典错误排查表

我把刷二分题最常见的错误整理成一张速查表,今天就用得上的那种。

错误现象可能原因解决办法
代码TLE用线性枚举代替二分答案寻找单调变量,改造成check+二分
答案总是差1check函数分支写反,或者返回left/right混淆用一个最小用例手动走一遍,比如[3,1]找1
循环死循环二分更新区间时左右边界没有变化固定使用左闭右开模板,并确保每次更新都缩小范围
数组越界初始边界设置错误检查left和right的初始值,以及访问元素的索引是否越界

这张表不止适用于今天的三道题,对大部分二分题目都通用。我建议你把它存下来,遇到类似报错时先对号入座。

最后再分享一个小技巧。现在无论我用哪种语言写二分,都会把左闭右开模板先放在IDE的代码片段里,包括mid计算防溢出、check函数留空。每次遇到新题,先套模板改check,而不是从零推导边界。这个习惯帮我节省了大量时间,也让我把精力真正放在题目本身的逻辑上。如果你也在刷LeetCode,不妨试试今天这个节奏:一道二分查找、一道二分答案、一场周赛,然后睡前写三行笔记。坚持到第66天,你会看到自己的变化。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询