问题描述
给定一个矩阵,假设为n * n的,我们这个矩阵里有若干个数字为1的元素。其他地方的元素值则不为1. 现在需要我们提供一个方法将里面为1的元素所在的行和列都设置成1. 同时要求算法的空间复杂度为O(1)。
分析
这个问题看起来有点容易让人混淆,因为如果我们从前往后这么一行一行的去遍历时,如果碰到一个为1的元素就直接将它所在行和列都设置成1的话。相当于将后面要遍历的一些元素也变成了1,这样我们就没法判断这些元素是本来为1的还是后来被设置成1的。而且,因为有限的空间复杂度我们不能记录下来每个为1的元素的位置信息。
我们以下面的图为例,假定有一个矩阵,它里面包含的1元素如下:
因为在图中只有第2行,第3行和第5行有元素分别为1,那么按照前面设置的思路,最后被设置成的结果应该是这样的:
而如果我们当时碰到一个1元素就直接将它本行或者本列后面的部分置1的话,我们后面遍历到的时候就会产生问题,如下图:
在我们标红的这个地方,如果实现被设置成了1,后面因为没法判断,就会将整个矩阵都置成1了。
所以说,这种原来的思路就有问题。
标记和设置
在讨论这个问题的时候,当时因为受到前面这个思路的影响,可以说被带到沟里去了。其实我们用标记和设置的这个思路来考虑的话,就很好办了。
我们这样来看,既然我们每碰到一个元素,就要将它所在行和列都设置为1, 而为了不影响它后面的遍历,我们肯定不能去修改它后面的元素的值。另外,我们在一个矩阵里,如何去确定一个元素的位置呢?其实很简单,就和二维坐标一样,只要能够确定它所在的行号和列号就可以了。那么如果我们知道一个元素为1的时候,我们就将它所在的元素的行的第一个元素设置为1并将它所在列的第一个元素设置为1。这样不就相当于我们画一些方格的时候给打的点吗?而且有了这些点我们不就可以确定它们后面所画的线了吗?这样也不会对后面的修改造成任何混淆。
依然以前面的图为例,我们在碰到第一个为1的元素时,所要做的事就是设置它所在的行和列的第一个元素,如下图:
这个标红的两个点表示所在的行和列以后要设置为1. 按照这样的思路,我们第一回遍历完整个矩阵之后,得到如下的一个标记结果:
ok,有了这个图我们后面该怎么办呢?其实很简单了,我们就是遍历第一行和第一列,将里面为1的元素对应的行或者列设置为1。当然,在遍历的时候,比如说第一行的时候,我们还有一个特殊的情况要注意,就是假如我们矩阵里m[0][0]的元素为1,我们是不是要将它所在的列置为1呢?因为对应的是第一列,如果一开始我们就将它给设置了的话就破坏了原来设置的结果。所以针对这个特殊的情况,我们需要先跳过,在将后面的所有元素都设置好了之后再回过头来处理它,这样就没问题了。
现在我们来写一个代码的实现,这里主要分为两个部分,第一个是遍历数组设置第一行和第一列的标点元素,第二个是遍历第一行和第一列,然后将对应的行和列标注出来,然后再来判断一下最左上角的元素。
第一部分的实现如下:
void mark(int[][] m, int n) {
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++) {
if(m[i][j] == 1) {
m[0][j] = 1;
m[i][0] = 1;
}
}
}
方法很简单,做个标记。
第二个部分的代码:
public void set(int[][] m, int n) {
for(int i = 1; i < n; i++) { //注意这里i取从1开始,故意跳过m[0][0]元素。
if(m[0][i] == 1) {
setColumn(m, n, i);
}
if(m[i][0] == 0) {
setRow(m, n, i);
}
}
if(m[0][0] == 1) {
setRow(m, n, 0);
setColumn(m, n, 0);
}
}
而setRow, setColumn这两个方法的实现只要注意一点,不要从它们这一行第一个元素开始设置就行了。它们的实现如下:
void setColumn(int[][] m, int n, int col) {
for(int i = 1; i < n; i++)
m[i][col] = 1;
}
void setRow(int[][] m, int n, int row) {
for(int i = 1; i < n; i++)
m[row][i] = 1;
}
总结
这种问题并不难,有的时候就是要看自己分析的时候思路是不是清晰。当然,这种问题和刷题党的应对手段差不多。鸟儿大了,什么样的林子没见过?有点考核人思考能力的意义,但是并不大。