题目描述
思路解析动画文字版
数组有序,离 x 最近的 k 个数一定挨在一起。左端点 lo 的范围只有 [0, n−k](再大窗口就出界),在这上面二分,O(log n) 就能锁定。
arr[mid] 是窗口左端外那个数、arr[mid+k] 是右端外那个数。左端外更远 → 丢左端,lo=mid+1;否则 hi=mid(相等也走这支,保留更小的数)。下面盯住 lo/hi/mid 三根指针怎么动。
开局 · 二分范围:换个大点的例子看二分怎么收缩:arr=[1..11](11 个数),k=3,x=8。lo 指下标 0、hi 指下标 8,答案左端点就夹在这两根指针之间。
第1轮 · mid 落点:mid 落在 lo 和 hi 正中间,下标 4。接下来把以 mid 为左端的窗口 [4,6] 画出来比一比。
第1轮 · 设想窗口 [4,6]:紫底就是设想的窗口 [4,6],里头是 5、6、7。现在看窗口两侧外面那两个数离 x=8 谁更远。
第1轮 · 比两端外侧:左端外 arr[4]=5 离 8 差 3(标灰=不划算),右端外 arr[7]=8 正好差 0。左端更远,保留它不划算——该丢左端、把窗口右移。
第1轮 · 丢左端 lo=mid+1:lo 跳到 5,下标 0~4 整段变灰出局——左端点不可能再落在那里了。范围一下砍掉一半,缩成 [5,8]。
第2轮 · mid 落点:在新范围 [5,8] 里重新取中点,mid 落到下标 6。再画出以它为左端的窗口。
第2轮 · 设想窗口 [6,8]:新设想窗口 [6,8],里头是 7、8、9。同样看两侧外面哪个离 x=8 更远。
第2轮 · 比两端外侧:左端外 arr[6]=7 差 1,右端外 arr[9]=10 差 2(标灰=不划算)。右端更远,丢右端、窗口左移。
第2轮 · 丢右端 hi=mid:hi 收到 6,下标 7、8 变灰出局。注意丢右端用 hi=mid(不是 mid−1),因为 mid 本身可能就是答案左端。范围缩成 [5,6]。
第3轮 · mid 落点:范围只剩 [5,6],mid 取到下标 5。最后一轮,先把以它为左端的窗口画出来。
第3轮 · 设想窗口 [5,7]:紫底窗口 [5,7],里头是 6、7、8。最后比一次两侧外面谁离 x=8 更远。
第3轮 · 比两端外侧:左端外 arr[5]=6 差 2(标灰),右端外 arr[8]=9 只差 1。这回是左端更远,丢左端、lo=mid+1=6。
第3轮 · 丢左端 lo=mid+1:lo 跳到 6,下标 5 变灰出局。现在 lo 和 hi 都指向 6,下一步它们就要撞上了。
二分结束 · lo=hi=6:lo 和 hi 撞到一起,二分停下。最优窗口左端点就是 6,所有非答案下标都已变灰,11 个数只用 3 轮比较就定下来了。
停前校验 · 窗口最优:停之前再确认一眼:窗口 [6,8] 两侧外面 arr[5]=6 和 arr[9]=10 离 8 都差 2,都不如窗口里的近,再怎么挪都不会更优——这就是二分停在这里的底气。
逐格取答案 · 第1格:锁定左端点后,从下标 6 起一格一格往右取。先点亮第 1 格 arr[6]=7。
逐格取答案 · 第2格:第 2 格 arr[7]=8 点亮——它正好就是 x 本身,离自己距离 0,当然在答案里。
取出答案窗口:点亮第 3 格 arr[8]=9,凑满 k=3 个:7、8、9,正好是离 8 最近的三个数,本来就是升序,直接返回。
回到原例验证:用同一招跑题目原例 arr=[1,2,3,4,5]:hi=n−k=1,mid=0 时两侧差打平,按规则走 else 保留更小的数,lo=0,取 arr[0..3] = [1,2,3,4],和答案一致。
雷区实演 · 相等时往哪挪:原例里 mid=0 时两侧差都是 2(打平)。规则要求平局取更小的数,所以走 else (hi=mid),保留靠左的窗口 [1,2,3,4]。若误用 ≥ 让 lo 右移,就会丢掉更小的 1,答案就错了。
边界三连:三种极端:全选、x 在最左外、x 在最右外。范围 [0,n−k] 和判定式天然把它们都兜住了。
凡是「在有序数组里挑一段固定长度、让它最优」的题都可往这个模板靠。另有双指针从两头往里夹的写法(每次缩离 x 更远的端点),思路直观但要缩 n−k 次,是 O(n−k),n 大时不如二分。
面试追问:把「答案为何连续」和「二分对象是左端点」讲透,是这题的面试加分项。
参考代码
class Solution: def findClosestElements(self, arr, k, x): lo, hi = 0, len(arr) - k # 左端点范围 [0, n-k] while lo < hi: mid = (lo + hi) // 2 # 左端外更远 → 丢左端,窗口右移 if x - arr[mid] > arr[mid + k] - x: lo = mid + 1 else: # 否则窗口左移 hi = mid return arr[lo:lo + k]复杂度
- 时间复杂度:O(log(n−k) + k),二分定左端点 O(log(n-k)),再切出 k 个元素 O(k)
- 空间复杂度:O(1),只用 lo/hi/mid 几个指针;不计返回结果本身
易错点
面试追问把动画讲成自己的话
追问为什么答案一定是连续的?
追问二分的对象是什么?
追问复杂度?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
山脉数组的峰顶索引
LeetCode 852 · 中等 · 沿着 二分套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题