岛屿数量(题号200)
题目
给你一个由'1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [ ["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
示例 2:
输入:grid = [ ["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
提示:
- m == grid.length
- n == grid[i].length
- 1 <= m, n <= 300
- grid[i][j] 的值为 '0' 或 '1'
链接
解释
这波啊,这波是一头雾水。
说实话,没有接触过此类题目的人应该很难想到解法吧,反正笔者这里是做不到的。
后来看看了思路,才费劲吧啦的写出来两种解决方案
具体的解释放在代码里,👇:
自己的答案(泛洪算法--FloodFill)
刚看到这个名字的时候是懵逼的,看上去很难懂的样子啊,其实并不是🐶。
它的原理其实很简单,放到这一题里来说是这样的,由于地图里0
是水,1
是陆地。并且陆地是可以和上下左右的陆地相互连接,那么这里可以在获取到第一块陆地的时候把它改成0
,然后遍历它上下左右的块,如果是1
就把它改成0
,继续找它周围的块,看看有没有陆地,如此遍历或者递归下去即可。
对了,别忘了在每次递归的开始前进行累计统计。
在所有的点都遍历完成后,此时所有的点应该都是0
,因为所有的1
都被改成0
了,这也就是所谓的泛洪算法,就跟洪水一样把把所有的陆地的淹掉,放在这题里简直再合适不过了。
👇看看代码:
var numIslands = function(grid) {
var lenI = grid.length - 1
lenJ = grid[0].length - 1
loop = [-1, 1]
count = 0
function findNeighbor(i, j) {
if (i < 0 || i > lenI) return
if (j < 0 || j > lenJ) return
if (grid[i][j] === '0') return
grid[i][j] = '0'
for (let k = 0; k < loop.length; k++) {
findNeighbor(i + loop[k], j)
findNeighbor(i, j + loop[k])
}
}
for (let i = 0; i <= lenI; i++) {
for (let j = 0; j <= lenJ; j++) {
if (grid[i][j] === '1') {
count++
findNeighbor(i, j)
}
}
}
return count
};
整体逻辑很简单,有一个递归的函数findNeighbor
来把所有的相关陆地都干掉,在递归开始前也累计了count
,用来返回最后的值。
findNeighbor
内部的实现很简单,首先判断边界值,如果过界了直接返回,如果本身就是水也直接返回。
之后就是拿上下左右的数据,这里用了一个loop
数组来对进行相邻块的获取,避免了调用四次findNeighbor
的丑陋代码。当然了,这里如果把loop
改为一个二维数组仅仅需要调用一次findNeighbor
。
别的也就没了,这种写法也是比较简单的。
自己的答案(并查集)
关于并查集的概念其实也并不是很复杂,有个老哥说的不错,这里放个链接了,不再赘述,点击这里,这老哥的代码Java的,可以不看,主要看它的概念,说的真的很明白,而且很有趣。
看完这个概念之后就很容易理解了,这里在第一次循环时把每块陆地的parent
都设置为陆地自己,之后开始第二次遍历,如果当前是陆地,并且陆地周围也有陆地,那么就把两块陆地连接在一起,如果到最后就可以得到一个完美的并查集。
统计陆地数量的话,可以放在并查集的内部,每次addSet
是count
加1,之后连接两块陆地时减1即可。
话不多说,看代码👇:
class UnionFind {
constructor() {
this.parents = new Map()
this.count = 0
}
addSet(location) {
location = location.toString()
this.parents.set(location, location)
this.count++
}
findSet(location) {
location = location.toString()
while (this.parents.get(location) !== location) {
location = this.parents.get(location)
}
return location
}
unionSet(location1, location2) {
var location1Parent = this.findSet(location1)
var location2Parent = this.findSet(location2)
if (location1Parent === location2Parent) return
this.parents.set(location1Parent, location2Parent)
this.count--
}
getCount() {
return this.count
}
}
function numIslands(grid) {
var len1 = grid.length
len2 = grid[0].length
uf = new UnionFind()
loop = [1, -1]
for (let i = 0; i < len1; i++) {
for (let j = 0; j < len2; j++) {
if (grid[i][j] === '1') uf.addSet([i, j])
}
}
for (let i = 0; i < len1; i++) {
for (let j = 0; j < len2; j++) {
if (grid[i][j] === '1') {
for (let k = 0; k < loop.length; k++) {
if (grid[i + loop[k]] && grid[i + loop[k]][j] === '1') {
uf.unionSet([i, j], [i + loop[k], j])
}
if (grid[i][j + loop[k]] && grid[i][j + loop[k]] === '1') {
uf.unionSet([i, j], [i, j + loop[k]])
}
}
}
}
}
return uf.getCount()
}
这里代码比较长啊,首先是一个UnionFind
类的定义,这就是这里的并查集。
内部实现也比较简单,👇依次看看:
-
constructor
没啥可说的,定义了
parents
和count
两个变量 -
addSet
每次添加节点的时候,都将节点的
parent
设置为自己,这里也就是对象的key
对应着value
,之后count
自增一 -
findSet
这个方法就是用来找到节点顶部的
parent
,这里用一个while
来进行遍历查找,层层递进找到最顶端的parent
。 -
unionSet
这方法用来连接两个节点,先找到两个节点对应的顶端
parent
,如果它们是一个parent
,那就不管了,如果不是,随便把一个节点的顶端parent
设置为另外一个节点的顶端parent
即可。最后
count
自减1,因为两个节点连在一起了,等于去掉一个节点。 -
getCount
最后拿到
count
返回即可。
之后的逻辑也和前面说的一样,首先遍历一次数组,拿到所谓陆地元素,并且插入到并查集当中。此时并查集中拥有所有单块陆地的元素,并且它们的parent
都是自己。
之后进行第二次遍历,此时开始对并查集进行连接操作,每连接一块陆地,总数count
减去1,如此操作完所有的单个陆地元素,即可得到最后的结果,返回即可。
小结
这里虽然给出了并查集的解法,但并不是一种好的方法,性能和泛洪算法差得太多,泛洪算法的内存占用和时间可以都可以排到前95%以上,但是并查集的解法只能到5%左右,很差很差,当然了,也是因为笔者写的不好,看到一个只循环一次的并查集解法,看起来不错,这里贴个链接,可以参考。
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇
有兴趣的也可以看看我的个人主页👇