面试 15:顺时针从外往里打印数字(剑指 Offer 第 20 题)

面试 15:顺时针从外往里打印数字

题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印每一个数字。例如输入: {{1,2,3}, {4,5,6}, {7,8,9}} 则依次打印数字为 1、2、3、6、9、8、7、4、5

这是昨天最后给大家留下的题目,相信大家也有去思考如何处理这道题目了。

初看这个题目,比较容易理解,也无需牵扯到数据结构或者高级的算法,看起来问题比较简单,但实际上解决起来且并没有想象中的容易。

大家极有可能想到循环嵌套的方式,套用几个 for 循环就可以啦。

  1. 首先打印第 1 行,然后第一个 for 循环从第一列打印到最后一列。
  2. 到最后一列,开始向下打印,为了防止重复打印第一行最后一列的数字,所以应该从第二行开始打印;
  3. 上面步骤 2 到底的时候,再在最后一行从倒数第二列开始往前打印一直到第一列;
  4. 用步骤 3 到最后一行第一列的时候再往上打印,第一行第一列由于步骤 1 已经打印过,所以这次只需要从倒数第二行第一列开始打印到顺数第二行第一列即可;
  5. 然后里面其实是一样的,不难看出里面其实就是对一个更小的矩阵重复上面的步骤 1 到步骤 4;
  6. 由于之前说了一定注意边界值,所以我们再步骤 1 之前严格注意一下传入矩阵为 null 的情况。

思路想好了,所以开始下笔写起代码:

public class Test15 {

    private static void print(int[][] nums) {
if (nums == null)
return; int rows = nums.length;
int columns = nums[0].length;
// 因为一次循环后 里面的矩阵会少 2 行,所以我们步长应该设置为 2
// 因为一次循环后 里面的矩阵会少 2 行,所以我们步长应该设置为 2
for (int i = 0; i * 2 < rows || i * 2 < columns; i++) {
// 向右打印,i 代表第 i 行,用 j 代表列,从 0 到 列数-1-2*i
for (int j = i; j < columns - 2 * i; j++) {
System.out.print(nums[i][j] + ",");
}
// 向下打印,j 代表行,列固定为最后一列-i*2
for (int j = i + 1; j < rows - 2 * i; j++) {
System.out.print(nums[j][rows - 1 - 2 * i] + ",");
}
// 向左打印,j 代表列,行固定为最后一列-i*2
for (int j = rows - 2 - 2 * i; j >= 2 * i; j--) {
System.out.print(nums[rows - 1 - 2 * i][j] + ",");
}
// 向上打印,j 代表行,列固定为第一列 +i*2
for (int j = rows - 2 - 2 * i; j > 2 * i; j++) {
System.out.print(nums[j][2 * i] + ",");
}
}
} public static void main(String[] args) {
int[][] nums = {{1, 2, 3},
{4,5,6},
{7,8,9}};
print(nums);
}
复制代码

上面的代码可能大家会觉得看的很绕,实际上我也很晕,在这种很晕的情况下通常是极易出现问题的。不信?不妨我们分析来看看。

  1. 首先我们做了 null 的输入值判断,挺好的,这没问题;
  2. 然后我们做了一个循环,输出看成一个环一个环的输出,因为输出完成一个环后总会少 2 行和 2 列,最后一次输出例外,所以我们给出步长为 2 ,并且中间的判断采用 || 而不是 &&,这里也没啥问题;
  3. 我们直接代入题干中的例子试一试。
  4. rows = 3,columns = 3,最外层循环会进行 2 次,符合条件;
  5. 进入第一次循环,第一次打印向右,j 从 0 一直递增到 2 循环 3 次,打印出 1, 2, 3,没问题;
  6. 进入第二次循环,本次循环我们希望打印 6,9;我们从 i + 1 列开始,一直到最后一列,正确,没问题;
  7. 进入第三次循环,测试没问题,可以正常打印 8,7;
  8. 进入第四次循环,测试没问题,可以正常打印 4;
  9. 最外层循环进入第二次,此时 i = 1, i < 1,出现错误。额,这里循环结束条件应该 i <= columns - 2 * i
  10. ....

不知道小伙伴有没有被绕晕,反正我已经云里雾里了,我是谁?我在哪?

各种试,会发现坑还不少,其实上面贴的这个代码已经是经过上面这样走流程走了好几次修正的,但特别无奈,这个坑始终填不满。

有时候,不得不说,其实能有上面这般思考的小伙伴已经很优秀了,但在算法上还是欠了点火候。在面试中,我们当然希望竭尽全力完成健壮性很棒,又能实现功能的代码,但不得不说,人都有思维愚钝的时候,有时候就是怎么也弄不出来。

我们在解题前,其实不妨通过画图或者其他的方式先和面试官交流自己的思路,虽然他不会告诉你这样做对与否。但这其实就形成了一种非常好的沟通方式,当然也是展现你沟通能力的一种体现!

前面的思路其实没毛病,只是即使我们得到了正解,但这样的一连串代码,别说面试官,你自己可能都看的头大。

我们确实可以用这样先打印矩阵最外层环,打印完后把里面的再当做一个环,重复外面的情况打印。环的打印次数上面也提了,限制结束的条件就是环数 <= 行数的二分之一 && 环数 <= 列数的 二分之一。

所以我们极易得到这样的代码:

private static void print(int[][] nums) {
if (nums == null)
return;
int rows = nums.length;
int columns = nums[0].length;
for (int i = 0; i * 2 < rows && i * 2 < columns; i++) {
printRing(nums, i, rows, columns);
}
}
复制代码

我们着重是需要编写 printRing(nums,i) 的代码。

仔细分析,我们打印一圈实际上就分为四步:

  1. 从左到右打印一行;
  2. 从上到下打印一列;
  3. 从右到左打印一行;
  4. 从下到上打印一列;

不过值得注意的是,最后一圈有可能退化为只有一行,只有一列,甚至只有 1 个数字,因此这样的打印并不需要 4 步。下图是几个退化的例子,他们打印一圈分别只需要 3 步、2 步 甚至 1 步。

面试 15:顺时针从外往里打印数字(剑指 Offer 第 20 题)

因此我们需要仔细分析打印时每一步的前提条件。

  • 第一步总是需要的,不管你是一个数字,还是只有一行。
  • 如果只有一行,那就不用第二步了,所以第二步能进去的条件是终止的行号大于起始的行号;
  • 如果刚刚两行并且大于两列,则可进行第三步打印;
  • 要想进行第四步的话,除了终止列号大于起始行号以外,还得至少有三行。

此外,依然得额外地注意:数组的下标是从 0 开始的,所以尾坐标总是得减 1 ,并且每进行一次循环,尾列和尾行的坐标总是得减去 1。

所以,完整的代码就奉上了:

public class Test15 {

    private static void print(int[][] nums) {
if (nums == null)
return;
int rows = nums.length;
int columns = nums[0].length;
for (int i = 0; i * 2 < rows && i * 2 < columns; i++) {
printRing(nums, i, rows, columns);
}
} private static void printRing(int[][] nums, int start, int rows, int columns) {
// 设置两个变量,endRow 代表当前环尾行坐标;endCol 代表当前环尾列坐标;
int endRow = rows - 1 - start;
int endCol = columns - 1 - start; // 第一步:打印第一行,行不变列变,列从起到尾
for (int i = start; i <= endCol; i++) {
System.out.print(nums[start][i] + ",");
}
// 假设有多行才需要打印第二步
if (endRow > start) {
// 第二步,打印尾列,行变列不变,需要注意的是尾列第一行已经打印过
for (int i = start + 1; i <= endRow; i++) {
System.out.print(nums[i][endCol] + ",");
}
}
// 至少两行并且 2 列才会有第三步逆序打印
if (endCol > start && endRow > start) {
// 第三步,打印尾行,行不变,列变。需要注意尾行最后一列第二步已经打印
for (int i = endCol - 1; i >= start; i--) {
System.out.print(nums[endRow][i] + ",");
}
}
// 至少大于 2 行 并且大于等于 2 列才会有第四步打印
if (endRow > start && endCol - 1 > start) {
// 第四步,打印首列,行变,列不变。需要注意尾行和首行的都打印过
for (int i = endRow - 1; i >= start + 1; i--) {
System.out.print(nums[i][start] + ",");
}
}
} public static void main(String[] args) {
int[][] nums = {{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}};
print(nums);
}
}
复制代码

用自己准备的测试用例输入测试,没有问题,通过。

上面的代码中用两个变量 endRowendCol 以及画图完美地解决了我们思路混乱并且代码难以看明白的问题。「其实不用吐槽判断方法有重复的情况,我们都是为了看起来思路更加清晰。

只看不练,很明显这样的题是容易被绕进去的,思路其实我们很好想到,但实现出来完全是另外一回事,所以大家不妨再去动手试试吧~

紧张之余,还是要留下明天的习题,记得提前思考和动手练习哟~

面试题:输入两个整数序列,第一个序列表示栈的压入顺序,请判断二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如:压入序列为{1,2,3,4,5},那{4,5,3,2,1} 就是该栈的弹出顺序,而{4,3,5,1,2} 明显就不符合要求;

上一篇:猫都能学会的Unity3D Shader入门指南


下一篇:【剑指Offer】20、包含min函数的栈