针对昨天的推文,有几句想说的
又一次周末推文,原本是想周一推的,但是这样的错误让我实在无法等待到周一了。
在昨天的推文 面试 15:顺时针从外往里打印数字 中,我出现了明显的「勘误」,额不,这不是「勘误」,这是明显的算法错误。我们来看我们的算法题目,题目来自《剑指 Offer》第 20 题。
题目:输入一个矩阵,按照从外向里以顺时针的顺序依次打印每一个数字。例如输入:
{{1,2,3},
{4,5,6},
{7,8,9}}
则依次打印数字为 1、2、3、6、9、8、7、4、5
我们最后给大家总结的代码是这样的。
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 <= rows / 2 && i <= columns / 2; 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);
}
}
实际上细心的小伙伴会发现这并不是 AC 的代码,也有一名小伙伴在推文后面留言要测试用例(暂不清楚他是否发现了问题),在文后留言中,我发现了一名叫「wants温拿」的小伙伴这样留言到。
你好,你试过数组是{{1, 2, 3, 4},{5, 6, 7, 8}}这种情况吗?会有重复打印的情况。
我立马停下手中的工作,放弃了午休,用这个测试用例代入查看代码。
确实存在重复打印的问题,当我们输入 {{1,2,3,4},{5,6,7,8}} 的时候,我们控制台得到的输出是:1,2,3,4,8,7,6,5,6,7,
从这里我们可以看出,完整的测试用例是多么重要,所以以后我们的推文都会加重对测试用例的投入时间。
出现了这样的重复,其实不少人一眼便能看出是 for 循环出了问题,然而南尘却非常愚钝,我竟然第一感觉是 printRing()
方法的最后一个 if 出现了问题。
确实发现了 if 的判断出了一点差错,我们最后一个 if 希望判断的是 至少大于 2 行,并且最少 2 列才可以打印。
而我们的代码中给出的 if 却是 至少大于 2 列,并且最少 2 行才可以打印。
这里明显会出现当我们的输入是 2 列 n 行( n >= 2 ) 的时候,不会执行第四个 for 循环的。
发现了这个问题,但无论是分析还是运行,这都不是造成我们 重复打印的原因。
我们看看为什么会出现这样的情况,倘若我们是用 IDE 来处理的话,直接 Debug 一下就会发现我们是 for (int i = 0; i <= rows / 2 && i <= columns / 2; i++)
这个 for 循环的问题。用我们「wants温拿」小伙伴的测试用例就可以看到,原本我们期望的是 for 循环只需要执行 1 次便退出,而目前的代码会进行两次循环,所以导致了我们的重复输出。
正常现场面试是手写的,我们可没那么幸运可以利用 IDE 进行 Debug,这里为了时间快就姑且快速处理。
知道了是 for 循环的问题,我们自然就知道怎么处理了。实际上在我们上篇文章的分析中,我们已经得到了一个信息:那就是第一次循环是从 nums[0][0] 开始打印,第二次循环就是从 nums[1][1] 打印……第 n 次循环就是从 nums[n][n] 开始打印。而我们现在就需要看看什么时候才应该有第二次循环。毫无疑问,至少得有 3 行 3 列。进行第三次循环至少得有 5 行 5 列,第四次循环至少得有 7 行 7 列 ….. 第 n 次循环,至少得有 2 * n -1 行 2 * n - 1 列。所以我们不难得到,正确的循环进行条件应该为:
2 * (i + 1) - 1 <= rows && 2 * (i + 1) - 1 <= columns
简化后得到:
2 * i + 1 <= rows && 2 * i + 1 <= columns
顺着这样的思路,我们肯定直接用 while 循环更加贴近我们的思路,所以自然得到代码为:
public class Test15 {
private static void print(int[][] nums) {
if (nums == null)
return;
int rows = nums.length;
int columns = nums[0].length;
int i = 0;
while (2 * i + 1 <= rows && 2 * i + 1 <= columns) {
printRing(nums, i, rows, columns);
++i;
}
}
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 (endCol > start && endRow - 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}};
print(nums);
}
}
代码写毕,我们还是得拿出我们的测试用例进行验证:
输入 null 或者 空数组,什么也不输出,满足条件;
输入 1 个数字的,比如 {{1}},输出 1 满足条件;
输入 1 行 x 列的,比如 {{1,2,3,4,5,6,7,8}},输出满足条件;
输入 x 行 1 列的,比如 {{1},{2},{3},{4}},输出满足条件;
输入 3 行 2 列的,输出满足条件;
输入 2 行 3 列的,输出满足条件;
这次应该是能 AC 了,总结一下:完整的测试用例很重要,完整的测试用例很重要,完整的测试用例很重要!
重要的话说了三遍了,当然也印证了一句话,南尘本来就是比较菜的,普普通通的一个人,所以各位叫我「大佬」那还真是承受不起啦~
不过从这样的错误中,你我一起成长,岂不妙哉?
最近也有不少小伙伴在问我怎么学习算法以及为什么花这么多时间学习算法,其实在这里也很明了了,学习算法每个人的目的不一样,我主要觉得一个人应该足够靠谱,从程序思维都能感悟出来了。
对于怎么学习算法,大家可以看到我这是啃 《剑指 Offer》,用 Java 实现,并且不看答案,提前思考并直接上手编写代码的。不过我还差了一步,那就是自己思考后再去看看作者的代码。很明显,这里我只要去看作者的答案,自然就不会在小伙伴「wants温拿」指出了问题才明白的。
最后,为了强调大家一起学习,对于本次找出问题的「wants温拿」同学也准备了个小红包以示心意,请「wants温拿」小伙伴,在后台回复「加群」获取我的微信添加方式,然后找到我领取你的小红包哟~
另外,希望其他小伙伴后面也认真跟进南尘的文章,发现这样的问题的,最好能自己找出为什么出错的原因就更好的。
好了南尘的朋友们,周六愉快,周末愉快!爱你们~
—————END—————
我是南尘,只做比心的公众号,欢迎关注我。
推荐阅读:
欢迎关注南尘的公众号:nanchen
做不完的开源,写不完的矫情,只做比心的公众号,如果你喜欢,你可以选择分享给大家。如果你有好的文章,欢迎投稿,让我们一起来分享。