LeetCode Top 100 Liked Questions 200. Number of Islands (Java版; Medium)

welcome to my blog

LeetCode Top 100 Liked Questions 200. Number of Islands (Java版; Medium)

题目描述
Given a 2d grid map of '1's (land) and '0's (water), count the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.

Example 1:

Input:
11110
11010
11000
00000

Output: 1
Example 2:

Input:
11000
11000
00100
00011

Output: 3
DFS, BFS
class Solution {
    public int numIslands(char[][] grid) {
        if(grid.length==0 || grid[0].length==0){
            return 0;
        }
        int n = grid.length, m = grid[0].length;
        boolean[][] memo = new boolean[n][m];
        int res = 0;
        for(int i=0; i<n; i++){
            for(int j=0; j<m; j++){
                if(grid[i][j]=='1' && memo[i][j]==false){
                    res++;
                }
                // dfs(grid, memo, i, j);
                bfs(grid, memo, i, j);
            }
        }
        return res;
    }

    private int[][] moves = {{-1,0}, {1,0}, {0,-1}, {0,1}};

    private void dfs(char[][] grid, boolean[][] memo, int i, int j){
        if(i<0||i>=grid.length||j<0||j>=grid[0].length||memo[i][j]==true||grid[i][j]=='0'){
            return;
        }
        memo[i][j] = true;
        for(int[] move : moves){
            dfs(grid, memo, i+move[0], j+move[1]);
        }
    }
    
    private void bfs(char[][] grid, boolean[][] memo, int i, int j){
        if(memo[i][j]==true||grid[i][j]=='0'){
            return;
        }
        int n = grid.length, m = grid[0].length;
        LinkedList<Integer> queue = new LinkedList<>();
        queue.add(i*m+j);
        while(!queue.isEmpty()){
            int cur = queue.poll();
            for(int[] move : moves){
                int x = cur/m + move[0];
                int y = cur%m + move[1];
                if(x>=0&&x<n&&y>=0&&y<m&&memo[x][y]==false&&grid[x][y]=='1'){
                    queue.add(x*m+y);
                    memo[x][y] = true;
                }
            }
        }
    }
}
第一次做; 并查集:1)元素-boss哈希表, 2)boss-人数哈希表, 3)初始化操作,collection 4)union(), 5)find(), 注意路径压缩在哪里; union()操作中, 必须通过findBoss()找某个元素的boss!; 核心:1)只需向右和向下遍历; 2)将所有的水域通过虚拟节点连接在一起
/*
并查集Union Find
*/
import java.util.HashMap;
import java.util.Stack;
import java.util.ArrayList;
import java.util.Collection;

class Solution {
    public int numIslands(char[][] grid) {
        if(grid==null || grid.length==0 || grid[0].length==0)
            return 0;
        int rows = grid.length, cols = grid[0].length;
        /*
        1.如果当前是 “陆地”,尝试与周围合并一下”,此时 “周围” 并不需要像 “深度优先遍历” 和 “广度优先遍历” 一样,方向是四周。事实上,只要 “向右”、“向下” 两个方向就可以了
        2.需要给水域设置一个虚拟节点, 否则会出问题, 看下面的例子, 第二行第三个0自成一派, 这样就不能保证门派中只有一个水域, 所以应该让所有的水域成一派
        11110
        11010
        11100
        00000
        */
        ArrayList<Coor> al = new ArrayList<>();
        Coor[][] coors = new Coor[rows][cols];
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){
                Coor co = new Coor(i, j);
                al.add(co);
                coors[i][j] = co;
            }
        }
        //虚拟水域节点
        Coor water = new Coor(-1,-1);
        //将虚拟节点加入记录
        al.add(water);
        UnionFind uf = new UnionFind();
        uf.initialize(al);
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){
                if(grid[i][j]=='1'){
                    //right
                    if(isValid(grid, i, j+1) && grid[i][j+1]=='1')
                        uf.union(coors[i][j], coors[i][j+1]);
                    //down
                    if(isValid(grid, i+1, j) && grid[i+1][j]=='1')
                        uf.union(coors[i][j], coors[i+1][j]);
                }
                else{
                    uf.union(coors[i][j], water);
                }
            }
        }
        return uf.count()-1;
        
    }
    public boolean isValid(char[][] grid, int i, int j){
        return i>=0 && i<grid.length && j>=0 && j<grid[0].length;
    }
    
    public class UnionFind{
        //记录元素的掌门人
        private HashMap<Coor, Coor> bossMap;
        //记录门派的人数: 用于Union操作中, 让人数少的门派加入到人数多的门派
        private HashMap<Coor, Integer> countMap;
        UnionFind(){
            bossMap = new HashMap<>();
            countMap = new HashMap<>();
        }
        //初始化操作, 每个元素自成一派
        public void initialize(Collection<Coor> coors){
            bossMap.clear();
            countMap.clear();
            for(Coor co : coors){
                bossMap.put(co, co);
                countMap.put(co, 1);
            }
        }
        //返回门派数量
        public int count(){
            return countMap.size();
        }
        //Union操作
        public void union(Coor a, Coor b){
            //input check
            if(a==null || b==null)
                return;
            //获取掌门
            /*
            核心: 不能通过bossMap找a的boss, 只能通过findBoss找a的boss, 因为bossMap中的key,value可能不正确, bossMap的value只能通过findBoss()更新, 没有用到findBoss()一定会出错
            (5,3)的boss变成了(5,2), 但是此时boosMap中(5,3)的value依然是(5,3), 等下一次findBoss()途径(5,3)的时候会把boosMap中(5,3)的value改成正确的;  
            最后一个节点怎么办? 最后一个节点没有下一次了... 没有关系, 因为最后一个节点不会进入到该函数, 已经没有可以跟最后一个节点进行合并的节点了
            */
            
            Coor aBoss = findBoss(a);
            Coor bBoss = findBoss(b);
            //两个掌门不同的话,才需要合并
            if(aBoss != bBoss){
                int aCount = countMap.get(aBoss);
                int bCount = countMap.get(bBoss);
                if(aCount<=bCount){
                    //因为门派人数少, 导致本来是大哥, 现在成了小弟
                    bossMap.put(aBoss, bBoss);
                    //更新门派人数
                    countMap.put(bBoss, aCount + bCount);
                    //aBoss不再是掌门, 删除其对应门派的人数记录
                    countMap.remove(aBoss);
                }
                else{
                    bossMap.put(bBoss, aBoss);
                    countMap.put(aBoss, aCount + bCount);
                    countMap.remove(bBoss);
                }
            }
        }
        //Find操作
        public boolean isSameGang(Coor a, Coor b){
            return bossMap.get(a) == bossMap.get(b);
        }
        //循环版
        private Coor findBoss(Coor co){
            //findBoss中之所以用栈, 是为了路径压缩! 可以加速Find操作
            Stack<Coor> s = new Stack<>();
            //如果当前元素不是掌门(掌门的表现形式是什么? 在bossMap中,key和value一样的是掌门)
            while(co != bossMap.get(co)){
                s.push(co);
                co = bossMap.get(co);
            }
            //路径压缩, 让树更扁
            while(!s.isEmpty()){
                bossMap.put(s.pop(), co);
            }
            return co;
        }
    }
    public class Coor{
        int x;
        int y;
        Coor(int x, int y){
            this.x = x;
            this.y = y;
        }
    }
}
第一次做; 深度优先遍历(递归版)
/*
深度优先遍历:循环+栈, 栈不为空时
还有个递归版
*/
import java.util.Stack;

class Solution {
    public int numIslands(char[][] grid) {
        if(grid==null || grid.length==0 || grid[0].length==0)
            return 0;
        int rows=grid.length, cols=grid[0].length;
        boolean[][] flag = new boolean[rows][cols];
        int res = 0;
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){
                res += dfs(flag, grid, i, j);
            }
        }
        return res;
    }
    /*
    深度优先遍历的核心目的是为了扩张土地
    */
    public int dfs(boolean[][] flag, char[][] grid, int i, int j){
        //坐标是否越界
        if(!isValid(grid, i, j))
            return 0;
        //是否访问过该坐标
        if(flag[i][j]==true)
            return 0;
        //改坐标是否表示水域
        if(grid[i][j]=='0')
            return 0;
        //dfs
        flag[i][j]=true;
        //up
        if(expand(flag, grid, i -1, j)){
            //新条件新递归
            dfs(flag, grid, i-1, j);
        }
        //down
        if(expand(flag, grid, i+1, j)){
            //新条件新递归
            dfs(flag, grid, i+1, j);
        }
        //left
        if(expand(flag, grid, i, j-1)){
            dfs(flag, grid, i, j-1);
        }
        //right
        if(expand(flag, grid, i, j+1)){
            dfs(flag, grid, i, j+1);
        }
        return 1;
    }
    //判断(i,j)能否成为带扩张的土地
    public boolean expand(boolean[][] flag, char[][] grid, int i, int j){
        return isValid(grid,i,j) && !flag[i][j] && grid[i][j]=='1';
    }
    public boolean isValid(char[][] grid, int i, int j){
        return i>=0 && i<grid.length && j>=0 && j<grid[0].length;
    }
}
第一次做; 深度优先遍历(循环版): 循环+栈, 栈不为空时…
/*
深度优先遍历:循环+栈, 栈不为空时
还有个递归版
*/
import java.util.Stack;

class Solution {
    public int numIslands(char[][] grid) {
        if(grid==null || grid.length==0 || grid[0].length==0)
            return 0;
        int rows=grid.length, cols=grid[0].length;
        boolean[][] flag = new boolean[rows][cols];
        int res = 0;
        for(int i=0; i<rows; i++){
            for(int j=0; j<cols; j++){
                res += dfs(flag, grid, i, j);
            }
        }
        return res;
    }
    public int dfs(boolean[][] flag, char[][] grid, int i, int j){
        //坐标是否越界
        if(!isValid(grid, i, j))
            return 0;
        //是否访问过该坐标
        if(flag[i][j]==true)
            return 0;
        //改坐标是否表示水域
        if(grid[i][j]=='0')
            return 0;
        //dfs
        Stack<Coordinate> s = new Stack<>();
        s.push(new Coordinate(i,j));
        while(!s.isEmpty()){
            Coordinate curr = s.pop();
            //up
            if(expand(flag, grid, curr.x -1, curr.y)){
                flag[curr.x-1][curr.y]=true;
                //dfs的核心1: 将curr重新压栈
                s.push(curr);
                //再将新的坐标压栈
                s.push(new Coordinate(curr.x-1, curr.y));
                //dfs的核心2: 找到向某一方向延伸的道路后直接continue, 不再考虑其他方向的延伸
                continue;
            }
            //down
            if(expand(flag, grid, curr.x+1, curr.y)){
                flag[curr.x+1][curr.y]=true;
                s.push(curr);
                s.push(new Coordinate(curr.x+1, curr.y));
                continue;
            }
            //left
            if(expand(flag, grid, curr.x, curr.y-1)){
                flag[curr.x][curr.y-1]=true;
                s.push(curr);
                s.push(new Coordinate(curr.x, curr.y-1));
                continue;
            }
            //right
            if(expand(flag, grid, curr.x, curr.y+1)){
                flag[curr.x][curr.y+1]=true;
                s.push(curr);
                s.push(new Coordinate(curr.x, curr.y+1));
                continue;
            }
        }
        return 1;
    }
    //判断(i,j)能否成为带扩张的土地
    public boolean expand(boolean[][] flag, char[][] grid, int i, int j){
        return isValid(grid,i,j) && !flag[i][j] && grid[i][j]=='1';
    }
    public boolean isValid(char[][] grid, int i, int j){
        return i>=0 && i<grid.length && j>=0 && j<grid[0].length;
    }
    public class Coordinate{
        int x;
        int y;
        Coordinate(int x, int y){
            this.x = x;
            this.y = y;
        }
    }
}
第一次做; 广度优先遍历: 循环+队列, 队列不为空时…
/*
三种方法:
1.广度优先遍历
2.深度优先遍历
3.并查集

先用广度优先遍历
踩坑最多的是...把char[][]中的元素当成int判断了! 得注意数据类型啊!
*/
import java.util.LinkedList;


class Solution {
    public int numIslands(char[][] grid) {
        //input check
        if (grid == null || grid.length == 0 || grid[0].length == 0)
            return 0;
        int rows = grid.length;
        int cols = grid[0].length;
        boolean[][] flag = new boolean[rows][cols];
        int res = 0;
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                res += bfs(grid, i, j, flag);
            }
        }
        return res;
    }

    public int bfs(char[][] grid, int i, int j, boolean[][] flag) {
        //坐标越界检查
        if (!isCoordinateValid(grid, i, j))
            return 0;
        //是否访问过
        if (flag[i][j] == true)
            return 0;
        //是否是水
        if (grid[i][j] == '0')
            return 0;
        //bfs
        LinkedList<Coordinate> queue = new LinkedList<>();
        queue.add(new Coordinate(i, j));
        flag[i][j] = true;
        while (!queue.isEmpty()) {
            Coordinate curr = queue.poll();
            //up; 坐标不越界,并没没有访问过
            if (expand(grid, curr.x - 1, curr.y, flag)) {
                queue.add(new Coordinate(curr.x - 1, curr.y));
                flag[curr.x - 1][curr.y] = true;
            }
            //down
            if (expand(grid, curr.x + 1, curr.y, flag)) {
                queue.add(new Coordinate(curr.x + 1, curr.y));
                flag[curr.x + 1][curr.y] = true;
            }
            //left
            if (expand(grid, curr.x, curr.y - 1, flag)) {
                queue.add(new Coordinate(curr.x, curr.y - 1));
                flag[curr.x][curr.y - 1] = true;
            }
            //right
            if (expand(grid, curr.x, curr.y + 1, flag)) {
                queue.add(new Coordinate(curr.x, curr.y + 1));
                flag[curr.x][curr.y + 1] = true;
            }
        }
        return 1;
    }

    public boolean expand(char[][] grid, int i, int j, boolean[][] flag) {
        return isCoordinateValid(grid, i, j) && !flag[i][j] && grid[i][j] == '1';
    }

    public boolean isCoordinateValid(char[][] grid, int i, int j) {
        int rows = grid.length;
        int cols = grid[0].length;
        if (i < 0 || i >= rows || j < 0 || j >= cols)
            return false;
        return true;
    }

    public class Coordinate {
        int x;
        int y;

        Coordinate(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
}
题解; 并查集方法, 思路分析写的非常好! 尤其是:1)只需向右和向下扩张 2)设置虚拟节点

原文链接

使用并查集解决本问题的思想很简单:

1、如果当前是“陆地”,尝试与周围合并一下;

2、如果当前是“水域”,就把所有的“水域”合并在一起,为此,我设置了一个虚拟的结点,表示“所有的水域都和这个虚拟结点是连接的”。

注意:

1、针对上面的第 1 点:如果当前是 “陆地”,尝试与周围合并一下”,此时 “周围” 并不需要像 “深度优先遍历” 和“广度优先遍历” 一样,方向是四周。
事实上,只要 “向右”、“向下” 两个方向就可以了,原因很简单,你可以在脑子里想象一个 “4 个方向” 和 “2 个方向” 的算法执行流程(或者看我下面展示的动画),
就知道 “4 个方向” 没有必要;

2、针对上面的第 2 点:由于我设置了“虚拟结点”,最后返回“岛屿个数”的时候,应该是连通分量个数 - 1,不要忘记将 “虚拟结点” 代表的 “水域” 分量去掉,
剩下的连通分量个数就是 “岛屿个数”。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值