前言
作为一个前端,面试遇到算法内容时,不禁要问前端也需要学习算法吗?
我也曾有过这种疑问,但这不也正反映了前端越来越重要了吗?
作为 Leetcode 的初学者,我认为算法对于编码思想还是有一定帮助的。
本文以 Leetcode 上的题目:“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.lengthn == grid[i].length1 <= m, n <= 300grid[i][j]的值为'0'或'1'
题目分析
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 这句话是重点,由水平方向和/或竖直方向上相邻的的陆地连接形成。
DFS-深度搜索算法
深度搜索算法 和 递归回溯 有点像。而递归回溯,我看了一些题解基本都只有递归,没有回溯。
/**
* @param {character[][]} grid
* @return {number}
*/
var numIslands = function(grid) {
if (!Array.isArray(grid) || grid.length <= 0 || grid[0].length <= 0) return 0;
const m = grid[0]?.length
const n = grid.length
let count = 0
for(let x = 0; x < n; x++) {
for(let y = 0; y < m; y++) {
if(grid[x][y] === '1') {
dfs(x, y)
count++
}
}
}
function dfs(x, y) {
if(x < n && y < m && x >=0 && y >=0 && grid[x][y] === '1') {
grid[x][y] = '0'
dfs(x + 1, y)
dfs(x - 1, y)
dfs(x, y + 1)
dfs(x, y - 1)
}
}
return count
};
DFS 题解完全是自己写出来的,通过之后内心还是有点小激动的。
思路:
-
- 遍历二维数组中的每一项,如果是1表示是一个陆地,计数加1。并找到相邻的陆地,因为它们属于同一块陆地,并将它们全部标记为0,表示已经统计过。后续的遍历过程,它们已经被标记为0了。
这又有点像并查集的思想,只不过并查集是将相邻的1打包到一个集合里,算一个。
BFS-广度搜索算法
广度搜索算法就是,一级一级往下搜索。
/**
* @param {character[][]} grid
* @return {number}
*/
var numIslands = function(grid) {
if (!Array.isArray(grid) || grid.length <= 0 || grid[0].length <= 0) return 0;
const m = grid[0]?.length
const n = grid.length
let count = 0
let queue = []
for(let x = 0; x < n; x++) {
for(let y = 0; y < m; y++) {
if(grid[x][y] === '1') {
queue.push([x, y])
bfs(queue)
count++
}
}
}
function bfs(queue) {
const direct = [[-1, 0], [1, 0], [0, -1], [0, 1]]
while(queue.length > 0) {
let [curX, curY] = queue.shift()
for(let i = 0; i < direct.length; i++) {
const [dx, dy] = direct[i]
const x = curX + dx, y = curY + dy
if(isValidIndex([x, y]) && grid[x][y] === '1') {
queue.push([x, y])
grid[x][y] = '0'
}
}
}
}
function isValidIndex(arr) {
let [x, y] = arr
return x >=0 && y >=0 && x < n && y < m
}
return count
};
本题解是看了思路之后,自己写出来的,和官方题解一致。
这个题解就很有看点了:
- 利用了队列 queue, 这是一种思想,将待处理的数据放在队列里,然后在while循环中去做处理。这样可以将代码解构,程序嵌套就不会很深了。
- 四个方向查找时,定义了一个数组。通过循环数组去依次查找。我之前很笨,四个方向写了四个if语句,代码块的语句相似度很高,x+1, x-1, y+1, y-1 写的头都大了。
也可以这样:
const direct = [0, 1, 0, -1, 0]
Tips
这里 BFS 和 DFS 算法主要思路是一致的,就是遍历二维数组,遇到1就计数加1。然后,将相邻的陆地也标记为0,避免重复统计。
不同的是,DFS是通过递归的方式去找相邻的陆地。我们来看下上述DFS算法中第一次调用
dfs(x, y)发生了什么?dfs(x + 1, y)表示先向右查找,如果是陆地,则标记为0,并以此为基点,继续向四个方向去找,直到某个基点四个方向都为 0 为止。然后再向左查找dfs(x - 1, y),以此类推。而BFS第一次执行时,除了使用了队列外,其实并无太大的区别。这里用队列和栈结构都可以。
并查集
这个理解稍稍有点复杂,我也花了一下午才看明白:
/**
* @param {character[][]} grid
* @return {number}
*/
var numIslands = function(grid) {
if (!Array.isArray(grid) || grid.length <= 0 || grid[0].length <= 0) return 0;
const m = grid[0]?.length
const n = grid.length
let tmp = -1
let direct = [[1, 0], [0, 1]]
let uf = new UnionFind(m * n)
for (let x = 0; x < n; x++) {
for (let y = 0; y < m; y++) {
if (grid[x][y] === '0') {
uf.union(m * x + y, tmp)
} else if (grid[x][y] === '1'){
for (let i = 0; i < direct.length; i++) {
const [dx, dy] = direct[i]
const temX = x + dx, temY = y + dy
if (isValidIndex([temX, temY]) && grid[temX][temY] === '1') {
uf.union(m * x + y, m * temX + temY)
}
}
}
}
}
function isValidIndex(arr) {
let [x, y] = arr
return x >=0 && y >=0 && x < n && y < m
}
return uf.getCount()
};
class UnionFind {
constructor(n) {
this.count = n
this.parent = new Array(n)
this.size = new Array(n)
for(let i = 0; i < n; i++) {
this.parent[i] = i
this.size[i] = 1
}
}
union(p, q) {
const rootP = this.find(p)
const rootQ = this.find(q)
if(rootP === rootQ) return
// 合并parent,size。把q接到p上
if (this.size[rootP] > this.size[rootQ]) {
this.parent[rootQ] = rootP;
this.size[rootP] += this.size[rootQ];
} else {
this.parent[rootP] = rootQ;
this.size[rootQ] += this.size[rootP];
}
// 每合并一次 count--
this.count--
}
isConnected(p, q) { //判断p,q是否连通
return this.find(p) === this.find(q)
}
find(x) { // 找到x节点的root,只有root节点的parent[x] == x
while(this.parent[x] != x) {
this.parent[x] = this.parent[this.parent[x]]
x = this.parent[x]
}
return x
}
getCount() {
return this.count
}
}
并查集算法创建了一个类,并实现了合并、查找两个主要方法,通过 count 计算并查集的数量。
并查集思路:
- 首先要理解二维数组的每一个元素均可通过唯一标识
m * x + y来表示,m = grid[0].length。有一点要特别注意:for(let x = 0; x < 4; x++) { for(let y = 0; y < 3; y++) { console.log(3 * x + y) // 0 ~ 11 } }m、n的值别弄反了,我经常搞不清。 - 初始时,二维数组每一个元素都代表一个集合,并设置它的父节点为自己,size为 1。
- 遍历时,遇到1就向右,向下去找相邻的 1,然后合并到自己的父节点;遇到 0 也合并到 tmp。
- 合并两个节点,就是合并唯一标识
m * x + y,这比使用数组下标x、y方便多了。 - 合并节点就是找到它们的跟节点,然后将size小的加到size大的上去,然后将size小的跟节点也赋值为size大的根节点。
- 合并两个节点,就是合并唯一标识
- 然后获取最后的集合数量。
Tips
这里并查集写的有点复杂,其实与DFS、BFS类似。DFS/BFS是将相邻的陆地标记为0,并查集是将相邻的陆地合并。
总结
接触算法不久,如有不对的地方请大神指正。