引言
你有没有遇到过这种情况:
面试官轻描淡写地扔过来一道题:"给你一个m×n的矩阵,每行递增,而且每一行的第一个数都比上一行最后一个大......问你能不能快速找到某个目标值?"
你心里一咯噔:
👉 这不是普通的二维数组啊,这简直是升序界的卷王之王!
然后你灵光一闪:
💡 "等等!这不就是个'假装是二维'的一维数组吗?"
没错,今天我们要聊的,就是一个能让二分查找从平面直角坐标系直接穿越到数轴上跳舞的经典问题。我们不讲晦涩公式,只用最接地气的语言,带你把这道题"吃干抹净"。
🧩 问题长什么样?先别慌,它是纸老虎!
题目大概是这样:

看到这个结构,聪明的你已经发现了关键点:
✅ 整个矩阵展开后,其实是一个严格升序的一维数组!
也就是说,上面那个矩阵等价于:
js
[1, 3, 5, 7, 10, 11, 16, 20, 23, 30, 34, 60] // 完全有序!
所以------
🎉 我们可以用二分查找来解决!
但问题是:怎么在"二维空间"里搞"一维操作"?
这就引出了两种神仙思路👇
🚀 方法一:两次二分 ------ 先找小区再敲门
想象一下你在一栋高档公寓楼里找朋友:
- 大楼有
m层(对应行) - 每层有
n户(对应列) - 所有房间号按顺序排列,且下一层第一个房间号 > 上一层最后一个
你要找住在"房间号=11"的老张。
你会怎么做?
🧠 当然是:
- 先看每层的第一户门牌号,确定他在哪一层;
- 然后再去那层挨家挨户敲门找他。
这就是两次二分法的核心思想!
✅ 第一步:二分找"可能住的那一行"
我们对第一列进行二分查找,找的是:
"最后一个首元素 ≤ target"的那一行
为什么是"最后一个"?因为后面的行开头就太大了,不可能有目标。
举个例子,target = 11:
| 行 | 首元素 |
|---|---|
| 0 | 1 |
| 1 | 10 |
| 2 | 23 |
显然,第2行开始首元素23 > 11,所以只能在第0或第1行。而我们要找的是"最后一个小于等于11的首元素",那就是第1行(10 ≤ 11)。
于是我们锁定:目标最多只能出现在第1行!
✅ 第二步:在这行内部再二分查找
第1行是 [10, 11, 16, 20],标准升序数组,直接套模板二分即可。
找到了11 → 返回 true!
🎯 成功定位,就像快递小哥精准投递!
💡 关键细节:避免死循环的小技巧
在找行的时候,为了避免 low 和 high 卡住不动,我们需要向上取整:
js
const mid = Math.floor((high - low + 1) / 2) + low;
否则当 low=0, high=1 时,mid 永远是0,就会陷入无限循环------相当于你一直在一楼徘徊,不敢上二楼 😵💫
🚀 方法二:一次二分 ------ 把二维压成一维,"降维打击"!
如果说方法一是"一步步推理",那方法二就是"开挂模式"!
它的哲学是:
"我不管你是几维,只要整体有序,我就当你是一条直线!"
🌀 思路精髓:虚拟一维数组 + 下标映射
假设矩阵是 m × n,我们可以把它看作一个长度为 m * n 的一维数组。
如何将一维下标 k 映射回二维坐标?
✨ 答案非常优雅:
js
row = Math.floor(k / n); // 第几行
col = k % n; // 第几列
是不是像极了小时候学的"排座位"?
比如一共4列:
- 第0个同学 → 第0行第0列
- 第5个同学 → 第1行第1列(5 ÷ 4 = 1余1)
于是我们可以直接在整个"虚拟数组"上做二分查找!
🔍 实战演示:target = 11
总长度 = 3×4 = 12,初始范围 [0, 11]
中间下标 mid = 5 → 对应位置是:
js
row = Math.floor(5 / 4) = 1
col = 5 % 4 = 1
→ matrix[1][1] = 11 ✅ 找到了!
boom!一击必中!
🆚 两种方法对比:谁更适合你?
| 维度 | 两次二分法 | 一次二分法 |
|---|---|---|
| 时间复杂度 | O(log m + log n) = O(log mn) | O(log mn) |
| 空间复杂度 | O(1) | O(1) |
| 代码长度 | 稍长(两个函数) | 超短(10行搞定) |
| 逻辑清晰度 | 分步思考,适合教学 | 数学美感强,适合装X |
| 边界处理难度 | 第一列二分需小心 | 相对简单 |
| 推荐指数 | ⭐⭐⭐⭐☆ | ⭐⭐⭐⭐⭐ |
🤔 如何选择?
- 如果你是初学者,建议先掌握两次二分法------它更符合直觉,像是走路:先迈左脚,再迈右脚。
- 如果你是老司机,直接上一次二分法,代码简洁得像诗一样,面试官看了都想鼓掌👏
🎯 我的独特理解:这不是"两种方法",而是"两种思维方式"
很多人说:"哦,一个是分治,一个是映射。"
我说:错!这是两种人生哲学!
-
两次二分法像是"稳扎稳打型选手":
"我不急,先缩小范围,再精确打击。"
类似于生活中那些做事有计划、步步为营的人。
-
一次二分法则是"全局视角型天才":
"你们还在纠结维度?我已经把它拍平了!"
就像马斯克说的:"不要被表象迷惑,抓住本质。"
而这道题的本质是什么?
🔑 只要数据整体有序,维度只是障眼法!
你可以把它当成一张表格,也可以当成一条线,甚至可以当成一部电影的时间轴------只要你能找到"顺序"的锚点。
🛠️ 实际代码实现(可直接复制粘贴)
✅ 方法一:两次二分(推荐学习用)
js
var searchMatrix = function(matrix, target) {
// 找到可能包含 target 的最后一行(基于首列)
let low = -1;
let high = matrix.length - 1;
while (low < high) {
const mid = Math.floor((high - low + 1) / 2) + low;
if (matrix[mid][0] <= target) {
low = mid;
} else {
high = mid - 1;
}
}
if (low === -1) return false; // 比最小的还小
// 在该行内进行二分查找
const row = matrix[low];
let left = 0, right = row.length - 1;
while (left <= right) {
const mid = Math.floor((right - left) / 2) + left;
if (row[mid] === target) {
return true;
} else if (row[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return false;
};
✅ 方法二:一次二分(推荐实战用)
js
var searchMatrix = function(matrix, target) {
const m = matrix.length;
const n = matrix[0].length;
let low = 0;
let high = m * n - 1;
while (low <= high) {
const mid = Math.floor((high - low) / 2) + low;
const val = matrix[Math.floor(mid / n)][mid % n];
if (val < target) {
low = mid + 1;
} else if (val > target) {
high = mid - 1;
} else {
return true;
}
}
return false;
};
⚡ 提示:背下这个下标映射公式,关键时刻能救你一命!
🌟 写在最后:学会"降维",才能跳出题海
很多刷题人陷入困境的原因是:
"我做了100道二分题,怎么换了个马甲就不认识了?"
因为你记得的是"形式",而不是"灵魂"。
真正的高手,看到这种矩阵不会想"二维怎么搞",而是问自己:
❓ "这个结构有没有全局有序性?"
❓ "能不能把它变成我能处理的形式?"
一旦你掌握了这种化归思维,你会发现:
- 旋转排序数组?→ 找断点 → 降维处理
- 山脉数组找峰值?→ 利用单调性 → 二分收缩
- 二维搜索?→ 扁平化 → 当作一维
🚀 所谓算法能力,不是记忆套路,而是不断把新问题翻译成旧知识的能力。
❤️ 结语:愿你也能在代码世界里"以无厚入有间"
庄子说:"彼节者有间,而刀刃者无厚,以无厚入有间,恢恢乎其于游刃必有余地矣。"
翻译成程序员语言就是:
只要你找对了突破口(有序性),哪怕问题看起来坚不可摧(二维+嵌套),你的算法也能像一把薄刃,轻松滑进去,咔嚓一声------搞定!