寻找矩阵中的宝藏:二维二分查找的奇妙冒险
从前,有一个叫小码的程序员,他收到了一个藏宝图(一个二维矩阵)。藏宝图有个神奇的特性:每一行的宝藏从左到右越来越值钱,而且下一行的第一个宝藏一定比上一行的最后一个更值钱。小码要找到价值正好为
9的宝藏。他能成功吗?让我们跟随他的冒险,揭开二分查找的神秘面纱!
📜 藏宝图的秘密:有序矩阵
小码拿到的藏宝图是这样的:
宝物分布图:
[1, 3, 5] ← 第0行
[7, 9, 11] ← 第1行
[13, 15, 17] ← 第2行
藏宝图的规则很简单:
- 每一行,从左到右,宝物越来越值钱(升序排列)
- 下一行的第一个宝物,比上一行的最后一个宝物更值钱
这就意味着,如果把所有宝物排成一条直线,它们也是完全有序的!这就是我们算法的突破口。
🧭 小码的寻宝策略:两次缩小搜索范围
小码很聪明,他想到了一个绝妙的策略:
"我不需要一个个宝物找,我可以先确定宝藏在哪一行,再确定在哪一列!"
这个策略的精髓就是两次二分查找:
- 第一步(纵向锁定行):只看每行的第一个宝物,用二分法找到目标行
- 第二步(横向锁定列):在找到的那一行里,再用二分法找到目标宝物
让我们看看小码是如何操作的:
🎯 第一阶段:锁定目标行(看每行的"门牌号")
小码发现,每行的第一个宝物就像是这一行的"门牌号"。他要找价值为 9 的宝藏,所以要先看看这个宝藏可能在哪一行。
初始状态:
- 搜索范围:所有行(第0行到第2行)
- 关注的"门牌号":
[1, 7, 13]
二分查找过程:
第1轮:
看看中间那行(第1行)的门牌号:7
小码思考:7 <= 9 吗?是的!
说明宝藏可能在当前行或下面几行
小码记下:候选行 = 第1行
然后继续往下面找:说不定下面有更接近的
第2轮:
看看下面几行的中间(第2行)的门牌号:13
小码思考:13 <= 9 吗?不是!太大了!
说明宝藏肯定在第2行以上
现在搜索范围重叠了,停止搜索
最终确定:宝藏可能在刚才记下的第1行
为什么这样有效?
- 如果一行的门牌号比目标小,宝藏可能在这一行或下面
- 如果门牌号比目标大,宝藏肯定在上面某行
- 我们记录下最后一个门牌号小于等于目标的行,就是最可能的目标行
🎯 第二阶段:在目标行中精确查找
现在小码知道宝藏很可能在第1行,这一行的宝物是:[7, 9, 11]
二分查找过程:
第1轮:
看看中间位置(第1列)的宝物:9
小码兴奋地比较:9 == 9 吗?是的!完全相等!
宝藏找到了!在第1行第1列!
任务完成! 小码成功找到了价值为 9 的宝藏。
💻 小码的寻宝代码(JavaScript版)
小码把他的寻宝过程写成了代码:
/**
* 在有序矩阵中寻找宝藏
* @param {number[][]} treasureMap - 藏宝图(矩阵)
* @param {number} targetValue - 要寻找的宝藏价值
* @return {boolean} - 是否找到宝藏
*/
function findTreasure(treasureMap, targetValue) {
// 获取藏宝图的大小
const totalRows = treasureMap.length; // 有多少行
const totalCols = treasureMap[0].length; // 每行有多少个宝物
// --- 第一步:确定宝藏可能在哪一行 ---
let topRow = 0;
let bottomRow = totalRows - 1;
let targetRow = -1; // 记录目标行,初始为-1表示没找到
while (topRow <= bottomRow) {
const middleRow = Math.floor((topRow + bottomRow) / 2);
const doorplate = treasureMap[middleRow][0]; // 这一行的"门牌号"
if (doorplate <= targetValue) {
// 这个门牌号 <= 目标价值,宝藏可能在这一行或下面
targetRow = middleRow; // 记下这个候选行
topRow = middleRow + 1; // 继续往下找,看有没有更合适的
} else {
// 门牌号太大了,宝藏肯定在上面
bottomRow = middleRow - 1;
}
}
// 如果没有找到任何候选行,说明宝藏不存在
if (targetRow === -1) {
return false;
}
// --- 第二步:在目标行中精确寻找宝藏 ---
let leftCol = 0;
let rightCol = totalCols - 1;
while (leftCol <= rightCol) {
const middleCol = Math.floor((leftCol + rightCol) / 2);
const treasure = treasureMap[targetRow][middleCol];
if (treasure === targetValue) {
return true; // 找到宝藏了!
} else if (treasure < targetValue) {
leftCol = middleCol + 1; // 宝藏可能在右边
} else {
rightCol = middleCol - 1; // 宝藏可能在左边
}
}
return false; // 这一行找遍了,没有找到
}
// 小码使用他的寻宝函数
const treasureMap = [
[1, 3, 5],
[7, 9, 11],
[13, 15, 17]
];
console.log("寻找价值为9的宝藏:", findTreasure(treasureMap, 9)); // 输出:true
console.log("寻找价值为8的宝藏:", findTreasure(treasureMap, 8)); // 输出:false
🎨 可视化寻宝过程
为了更清楚地理解,让我们看看小码的搜索路径:
初始藏宝图:
行号 宝物
0 [1, 3, 5]
1 [7, 9, 11] ← 最终在这里找到宝藏!
2 [13, 15, 17]
第一步:锁定行(看每行的第一个数)
搜索 [1, 7, 13] 寻找最后一个 <=9 的数
✓ 1 <= 9,记下行0,继续往下
✓ 7 <= 9,记下行1,继续往下
✗ 13 > 9,往上找
最终确定:行1
第二步:在行1中搜索
搜索 [7, 9, 11] 寻找9
第一次就命中中间位置:9 == 9 ✓
🔍 算法的精妙之处
1. 边界的巧妙处理
if (doorplate <= targetValue) {
targetRow = middleRow; // 记下候选行
topRow = middleRow + 1; // 但还继续往下找
}
这种写法确保我们找到的是最后一个门牌号小于等于目标的行,避免了这种情况:
- 目标值=10,门牌号有[7, 9, 12]
- 如果找到9就停止,会错过第2行(实际宝藏可能在第1行)
2. 为什么是两次二分查找?
- 时间复杂度:O(log m + log n) = O(log(m×n))
- 这和把整个矩阵拉直做一次二分查找的效率一样
- 但逻辑更清晰,容易理解和调试
3. 错误处理
if (targetRow === -1) {
return false;
}
这个检查很重要:如果所有行的第一个数都比目标值大,那宝藏肯定不存在。
📊 效率分析:小码的方法有多快?
| 方法 | 时间复杂度 | 空间复杂度 | 优点 |
|---|---|---|---|
| 暴力搜索 | O(m×n) | O(1) | 简单直接 |
| 两次二分法 | O(log m + log n) | O(1) | 效率极高,逻辑清晰 |
| 一维二分法 | O(log(m×n)) | O(1) | 代码简洁 |
举个例子:
- 对于 1000×1000 的矩阵(100万个宝物)
- 暴力搜索:最多查看100万次
- 二分查找:最多查看 log₂(1000) + log₂(1000) ≈ 10 + 10 = 20次!
- 效率相差5万倍!
🧠 思维拓展:如果规则变了怎么办?
变体1:每行有序,但行间不一定连续(LeetCode 240题)
新的藏宝图:
[1, 4, 7, 11]
[2, 5, 8, 12]
[3, 6, 9, 13]
这时不能用两次二分法了,因为不能通过第一列确定行。解决方法:从右上角开始搜索,每次排除一行或一列。
变体2:更大的矩阵怎么办?
- 如果矩阵特别大,无法完全装入内存,可以考虑分批加载
- 仍然可以使用二分思想,先确定在哪一批数据中
🎯 实战练习:考考你!
题目1: 在下面的矩阵中寻找 target = 14
[1, 4, 7, 11]
[8, 9, 12, 15]
[10, 13, 16, 19]
[12, 14, 17, 22]
题目2: 如果 target = 0,算法会怎样处理?
题目3: 如果矩阵是空的,代码需要怎么修改?
💎 总结:小码的智慧
小码的这次寻宝经历教会了我们:
- 分解问题:复杂问题分解为简单步骤(先找行,再找列)
- 利用有序性:有序数据是二分查找的完美舞台
- 边界思维:仔细考虑各种边界情况(空矩阵、目标值太小/太大)
- 贪心策略:找到一个可行解后,继续寻找更优解
算法的本质,就是用计算机能理解的方式,把人类的智慧表达出来。 就像小码的寻宝过程,我们不是盲目搜索,而是有策略、有方法地缩小范围,最终精准定位目标。
现在,你也掌握了这种"两次二分法"的寻宝技巧。下次遇到类似的问题,记得像小码一样,先思考、再分解、最后精准出击!