经常有面试问题是二维网格问题,比如迷宫问题、迷宫最短路径问题、N皇后问题等等。求解时主要有以下要素:
- 遍历:对每个格子(位置)进行遍历。
- 搜索相邻的格子。
总体的搜索可以有深度优先(DFS)和广度优先(BFS)之分。
根据我自己的经验,在有限的时间内解答这类问题时,要注意以下几点:
二维数组的行列顺序
二维数组是按照先行后列的顺序存储的:
void func(int[][] data) { if (data == null || data.length = 0 || data[0].length == 0) { // ... return; } int rows = data.length; int cols = data[0].length; for (int r = 0; i < rows; ++r) { // 或者更好的写法是:c < data[r].length // 但这类问题一般都是个矩形区域,因此把cols提前定好,代码更简洁 for (int c = 0; c < cols; ++c) { data[r][c] = 0; } } // ... }
循环变量名称
循环变量名称似乎根本不是个问题,一般会用i和j来表示。但在有限时间而且缺乏IDE帮助debug的情况下,对于二维格子问题,很容易引起思维混乱。我自己就有一次在线测试时,因为把行和列弄颠倒了,导致时间到了还无法把程序调通。后来自己“复盘”的时候,才发现原因所在。
因此,建议把变量名称弄得一看就懂:
- 行数和列数:rows,cols
- 表示行和列的循环变量名:r,c
例如:
for (int r = 0; r < rows; ++r) { for (int c = 0; c < cols; ++c) { data[r][c] = 0; } }
搜索相邻的格子
一般我会按东西南北四个方向编写:
// N if (r > 0) { backtrack(r - 1, c); } // S if (r < rows - 1) { backtrack(r + 1, c); } // W if (c > 0) { backtrack(r, c - 1); } // E if (c < cols - 1) { backtrack(r, c + 1); }
这样看似很有效率,但很啰嗦,而且在情急之下容易犯错。我经常会因为拷贝粘贴的代码中没改完全而导致程序不能通过,而之后要用肉眼去观察一遍来找出错误的话,很费时间。因此,在效率没有多少区别的情况下,程序越简短越好。
// N S W E private static final int[] rowOffsets = {-1, 1, 0, 0}; private static final int[] colOffsets = { 0, 0, -1, 1}; private void backtrack(int[][] data, int[][] visited, int r, int c) { // ... int rows = data.length; int cols = data[0].length; BiPredicate<Integer, Integer> isValid = (row, col) -> row >= 0 && row < rows && col >= 0 && col < cols; // 尝试搜索相邻的格子 for (int i = 0; i < rowOffsets.length; ++i) { int r1 = r + rowOffset[i]; int c1 = c + colOffset[i]; if (isValid.test(r1, c1) && !visited[r1][c1]) { backtrack(data, visited, r1, c1); } } // ... }
有人喜欢在枚举相邻格子时,按东南西北的顺序转一圈。在我看来似乎没什么必要。最重要的是简单且不易弄错,所以我都是选择按北、南、东、西的顺序轮转。当然,每个人习惯不同,可以选择自己最舒适的顺序。
总结:对于编程来说,最好是从一开始就不要把bug放进去,否则要找出来就需要花费成倍的时间。因此,要选择各种一看就懂的方式写代码,防止写到中间自己都迷糊了。另外,为了避免查找bug的时间过长,应该尽量写简短的代码(当然不是那种为了短而短的十分缺乏可读性的那种)。