LeetCode 73,为什么第一反应想到的解法很有可能是个坑?
本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是LeetCode第42篇文章,我们来看看LeetCode第73题矩阵置零,set matrix zeroes。
这题的难度是Medium,通过率在43%左右,从通过率上可以看出这题的难度并不大。但是这题的解法不少,从易到难,有很多种方法。而且解法和它的推导过程都挺有意思,我们一起来看下。
题意
首先我们来看题意,这题的题意很简单,给定一个二维数组。要求我们对这个数组当中的元素做如下修改,如果数组的i行j列为0,那么将同行和同列的元素全部置为0。要求我们直接在原数组上进行修改,而不是返回一个新的数组。
言下之意,要求我们实现一个in-place的方法,而避免额外开辟新的内存。
样例
Input:
[
[0,1,2,0],
[3,4,5,2],
[1,3,1,5]
]
Output:
[
[0,0,0,0],
[0,4,5,0],
[0,3,1,0]
]
近在眼前的解法原来是坑
这题的题意非常简单,解法也非常明显,以至于很多人拿到它都会当做模拟题来解决。即遍历一下数组,如果找到0,那么将它所在的行和列赋值为0,然后继续遍历。
这段逻辑并不难写,我们很容易写出来:
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
n = len(matrix)
if n == 0:
return
m = len(matrix[0])
for i in range(n):
for j in range(m):
# 当我们找到为0的位置之后,将所在的行和列置为0
if matrix[i][j] == 0:
for k in range(m):
matrix[i][k] = 0
for k in range(n):
matrix[k][j] = 0
但是很遗憾的是,这样的做法是错误的,实际上连样例都无法通过。通不过样例的原因也很简单, 因为0具有传递性。
举个简单的例子,假设第0行当中有一个0,那么最后的结果一定是第0行全部被置为0。但问题是我们是在遍历到0的时候来进行的set操作,这样会将第0行的其他元素也置为0。这样当我们遍历到后面的位置之后,会继续传递,从而将一些不该置为0的地方也置为0了。
举个简单的例子,比如:第0行是1 0 0 1。显然由于第0行存在0,所以操作之后的结果一定是全为0。但问题是matrix[0][3]这个位置原本并不为0,但是如果我们在发现matrix[0][1]为0的时候,将它置为0的话,那么当我们后面遍历到matrix[0][3]得到0的时候,会无法判断究竟是这个位置原本就是0,还是前面出现了0导致这一行全部变成了0。这两者的操作是不同的。
眼看着目标就在眼前,好像一伸手就碰得到,但是偏偏好像这一步就是咫尺天涯,怎么也碰不到。这种感觉想想都很难受,我想,当你试着用这种方法去解这道题然后发现不行的时候,一定会有这样的感觉。并且你会发现好像也没有什么很好的办法来优化。
这种情况在正式的算法比赛当中经常遇到,所以专业的竞赛选手有了经验(吃过亏)之后,想出思路的第一时间就会立即转向思考,这样做是不是会有什么坑,或者是考虑不到的情况。严谨一点的同学还会构思几组不同的测试数据进行测试,或者是脑海中模拟算法的运算。
刚不过去只能绕
以前我年轻的时候总是不信邪,有时候明知道这个方法并不好,或者是存在反例,但是仍会坚持想要通过自己的努力想出一个方案来解决它,而不是更换方法。
我不知道有多少人有同样的想法,但是一般来说头铁的毛病最后总是会被治好的。这题算是一个不错的例子,如果你坚持使用模拟的方法来做这道题,只有一种方案就是再创建一个同样大小的数组来作为缓存。当我们遇到0的时候,我们不直接修改原数组中的结果,而是修改缓存,将同行和同列缓存数组中的元素置为0,最后再将缓存数组与原数组合并。
但是显然这不是一种好的方法,因为题目要求in-place的目的就是为了节约空间,我们另外创建了一个同样大小的数组显然违背了题目的本意。
所以头铁到最后还是得认清现状,这个方法不适合这道题,需要更换解法。如果是在比赛当中得出的这个结论,那么很有可能奖牌已经和你没什么关系了。坚持和固执本身也许没有太大的区别, 可能只是出现的场景不一样。
进阶解法
回到这道题本身,我们已经证明了模拟的思路是行不通的,除了一边遍历一边操作可能带来的混乱之外,还有一个点是这样的复杂度很高。因为如果原数据当中如果本身0就很多的话,那么我们会需要不停地操作,极端情况下,如果所有元素都是0,那么我们每一个位置都需要操作一下行列,整体的复杂度会达到。
既然如此,还有什么好的办法吗?
当然是有的,其实也挺明显的,因为对于一个出现的0来说它会影响的范围是固定的,就是所在的行和列,那我们是不是记录下会全部置为0的行和列,最后再遍历一遍数据,看下当前元素是不是出在置为0的范围当中就可以了。这种方法需要我们再创建两个数组,用来存储行和列是否被置为0。
这个解法也很直观,想到了代码应该不难写:
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
n = len(matrix)
if n == 0:
return
m = len(matrix[0])
rows = [0 for _ in range(n)]
cols = [0 for _ in range(m)]
# 记录置为0的行和列
for i in range(n):
for j in range(m):
if matrix[i][j] == 0:
rows[i], cols[j] = 1, 1
# 如果所在行或者列置为0,那么当前位置为0
for i in range(n):
for j in range(m):
if rows[i] or cols[j]:
matrix[i][j] = 0
终极解法
上面的做法虽然通过之后的战绩不太光彩,没能战胜90%以上的提交,但是能够通过,而且算法没有数量级的差距,也算是可以的。如果让我来做,我可能就想到这种方法为止了。但是题目当中明确说了,还有空间复杂度为O(1)的算法,逼得我进一步思考了一下。
一般来说我们都是优化时间复杂度,很少会优化空间复杂度。相比于优化时间,优化空间有时候更加困难。因为有些时候我们可以空间换时间,可以预处理,可以离线计算……方法相对比较多。但优化空间的方法则很少,尤其是很多时候还不能牺牲时间,所以一般来说只能从算法本身来优化,很少有什么套路可以套用。
在这个问题当中,要优化空间复杂度到常数级,那么说明我们连数组都不能用。也就是说不能记录行和列的信息,但是我们也不能用模拟的方法来进行,那么应该怎么办呢?
干想是很难想出来的, 但是我们换个思路,问题就完全不一样了。上面的算法时间复杂度是最优的,空间复杂度不太行,那么有没有办法既使用同样的算法,又能节省空间呢?看起来似乎不可能,但是其实可以,方法说穿了也并不值钱,就是将数据想办法存在已有的地方,而不是另外开辟空间。在这个问题当中,已有的地方当然就只有一个就是原数组。也就是说我们要把每一行和列是否为0的信息记录在原数组当中,比如我们可以把第0行和第0列用来做这个事情。
但这样又会带来另外一个问题,如果第0行和第0列本身当中也有0出现该怎么办?没办法,只能特判了。我们单独用变量来记录第0行和第0列是否被置为0,这样我们就最大化地利用了空间,将空间复杂度降低到了常数级。
代码逻辑和上面一脉相承,只是多了一点骚操作。
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
n = len(matrix)
if n == 0:
return
m = len(matrix[0])
row, col = False, False
# 特判0,0的位置
if matrix[0][0] == 0:
row, col = True, True
# 特判第0列是否含0
for i in range(n):
if matrix[i][0] == 0:
col = True
break
# 特判第0行是否含0
for i in range(m):
if matrix[0][i] == 0:
row = True
break
# 将i行,j列是否为0的信息存入matrix当中
for i in range(0, n):
for j in range(0, m):
if matrix[i][j] == 0:
matrix[i][0] = 0
matrix[0][j] = 0
for i in range(1, n):
for j in range(1, m):
# 根据第0行与第0列数据还原
if matrix[i][0] == 0 or matrix[0][j] == 0:
matrix[i][j] = 0
# 最后处理第0行与第0列
if row:
for i in range(m):
matrix[0][i] = 0
if col:
for i in range(n):
matrix[i][0] = 0
总结
到这里,这道题就算是分享完了,它的题意简单,但是解法挺多的,我个人感觉也许还存在更好的解法也不一定。
我个人做完这题最大的感受不是这题的思路如何,也不是它涉及的算法如何,而是想到了很多和算法题无关的事情。比如我们生活当中有没有这样看似简单,但是做起来发现一点也不简单的事情?有没有眼看着目标就在眼前,却发现选择的路一开始就是错的呢?
带着这样的思路来做题,会发现题目也变得有意思多了。
今天的内容就是这些,如果喜欢本文,可以的话,请点个关注,给我一点鼓励,也方便获取更多文章。
LeetCode 73,为什么第一反应想到的解法很有可能是个坑?的更多相关文章
- LeetCode缺失的第一个正数
LeetCode 缺失的第一个正数 题目描述 给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数. 进阶:你可以实现时间复杂度为 O(n)并且只使用常数级别额外空间的解决方案吗? ...
- LeetCode 62,从动态规划想到更好的解法
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是LeetCode专题第36篇文章,我们一起来看下LeetCode的62题,Unique Paths. 题意 其实这是一道老掉牙的题目了 ...
- [LeetCode] Largest Rectangle in Histogram O(n) 解法详析, Maximal Rectangle
Largest Rectangle in Histogram Given n non-negative integers representing the histogram's bar height ...
- leetcode 第188题,我的解法,Best Time to Buy and Sell Stock IV
<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255) ...
- [LeetCode] 73. Set Matrix Zeroes 矩阵赋零
Given a m x n matrix, if an element is 0, set its entire row and column to 0. Do it in-place. Exampl ...
- LeetCode 73. Set Matrix Zeros(矩阵赋零)
Given a m x n matrix, if an element is 0, set its entire row and column to 0. Do it in place. click ...
- LeetCode Array Easy 53. Maximum Subarray 个人解法 和分治思想的学习
Description Given an integer array nums, find the contiguous subarray (containing at least one numbe ...
- Java实现 LeetCode 73 矩阵置零
73. 矩阵置零 给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0.请使用原地算法. 示例 1: 输入: [ [1,1,1], [1,0,1], [1,1,1] ...
- LeetCode 861翻转矩阵后得分详细解法
1. 题目内容 有一个二维矩阵 A 其中每个元素的值为 0 或 1 . 移动是指选择任一行或列,并转换该行或列中的每一个值:将所有 0 都更改为 1,将所有 1 都更改为 0. 在做出任意次数的移动后 ...
随机推荐
- QML文字灰飞烟灭效果
QML文字灰飞烟灭效果 1,目的 实现文字化作一缕青烟随风而逝的效果. 2,设计分析 在前面的章节中讲述了如何化作光斑碎片逐渐消失的效果,我们可以借鉴它将光斑换成烟雾,再加入端流产生微风浮动,加上字幕 ...
- redis文章汇总
方便集群管理时的查看操作 http://www.cnblogs.com/mushroom/p/4738170.html http://www.cnblogs.com/hjwublog/p/568170 ...
- LeetCode二分专题
二分 二分模板 两个模板:1.最大值最小模板一,2.最小值最大用模板二 单调性.两段性的性质 版本1:二分绿色端点是答案,最大值最小 int bsearch_1(int l, int r){ whil ...
- Linux --如何新增一块硬盘并自动挂载
1. 虚拟机添加硬盘 2. 分区 fdisk /dev/sdb 3. 格式化 mkfs -t ext4 /dev/sdb1 将刚刚创建的盘格式化成 ext4格式 4. 挂载 先创建一个目录,/hom ...
- UEFI Shell --常用命令解释
UEFI Shell解释 UEFI Shell 是一个提供用户和UEFI系统之间的接口,进入UEFI Shell可以对计算机系统进行配置 命令解释: 单独的help就可以输出所有指令,不做特殊说明,内 ...
- @vue/cli 4.0.5 学习记录
1. Vue CLI (@vue/cli) 是一个全局安装的 npm 包,提供了终端里的 vue 命令.Vue CLI 插件的名字以 @vue/cli-plugin- (内建插件) 或 vue-cli ...
- APIView中的dispatch
(1)dispatch方法详解----封装原有的request对象 (原request中的方法和属性均可直接在封装后的request中调用,或者使用request._request也可,如:reque ...
- Django之钩子Hook方法
局部钩子: 在Fom类中定义 clean_字段名() 方法,就能够实现对特定字段进行校验.(校验函数正常必须返回当前字段值) def clean_name(self): pass n ...
- 关于MYSQL 和INNODB的逻辑关系图。最好的理解是一点点动手做,观察,记录,思考。
每隔0.1秒就刷一次MYSQL文件的变化,并闪动标示出来,以观察SQL执行时,MYSQL的处理顺序. watch -n 0.1 -d stat /var/lib/mysql/ib_logfile0 / ...
- 关于mysql的metadata lock
昨天晚上上线,却发现一个ddl语句长时间没有生效 查processlist, 发现包括ddl语句在内的众多查询提示 “Waiting for table metadata lock” 唯一没有该提示的 ...