Java算法精讲:岛屿数量与DFS/BFS应用

在这里插入图片描述

📚 前言

亲爱的同学们,大家好!今天我们要一起探索一个非常经典且在面试中高频出现的算法问题——岛屿数量。这个问题不仅能够很好地检验我们对图论搜索算法的理解,还能锻炼我们的编程思维!✨

想象一下,你是一名探险家,手持一张由’0’(水)和’1’(陆地)组成的藏宝图。你的任务是数出这张图上有多少个岛屿(被水完全包围的陆地)。听起来简单,对吧?但当地图变得复杂,岛屿形状各异时,如何高效地完成这个任务呢?这就是我们今天要解决的问题!🧭

这个问题不仅是算法面试中的常客,更是理解深度优先搜索(DFS)和广度优先搜索(BFS)的绝佳案例。掌握了它,你将能够应对各种图论搜索问题,为你的算法之旅打下坚实基础。

让我们一起揭开"岛屿数量"这个经典问题的神秘面纱吧!👀

🧠 知识点说明

在深入问题之前,我们先来了解一些基础知识:

1. 问题描述

"岛屿数量"问题通常描述为:

给你一个由 '1'(陆地)和 '0'(水)组成的二维网格 grid,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

例如:

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1
输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

2. 深度优先搜索(DFS)的基本概念

深度优先搜索是一种用于遍历或搜索树或图的算法。它沿着树的深度遍历树的节点,尽可能深地搜索树的分支。当节点v的所有边都已被探寻过,搜索将回溯到发现节点v的那条边的起始节点。

DFS的基本思想是:

  • 从起始节点开始,沿着一条路径一直走到底
  • 当无法继续前进时,回溯到上一个节点,尝试另一条路径
  • 重复上述过程,直到所有节点都被访问

在实现上,DFS通常使用递归或栈来实现。

3. 广度优先搜索(BFS)的基本概念

广度优先搜索是一种用于遍历或搜索树或图的算法。与DFS不同,BFS从起始节点开始,先访问起始节点的所有邻接节点,然后再访问这些邻接节点的邻接节点,以此类推。

BFS的基本思想是:

  • 从起始节点开始,先访问所有距离为1的节点
  • 然后访问所有距离为2的节点
  • 以此类推,按照距离的递增顺序访问所有节点

在实现上,BFS通常使用队列来实现。

4. 网格问题的特点

在网格问题中,我们通常将每个单元格视为一个节点,相邻的单元格之间有边相连。在"岛屿数量"问题中:

  • 节点是网格中的每个单元格
  • 边连接水平或垂直相邻的单元格
  • 我们只关心值为’1’的单元格(陆地)

🔍 重难点说明

解决"岛屿数量"问题有两种主要方法:DFS和BFS。让我们一一解析:

方法一:深度优先搜索(DFS)

DFS的思路是:

  1. 遍历整个网格
  2. 当找到一个值为’1’的单元格(陆地)时,将岛屿计数器加1
  3. 使用DFS将与当前陆地相连的所有陆地标记为已访问(通常通过将其值改为’0’或使用额外的访问数组)
  4. 继续遍历网格,重复步骤2和3,直到遍历完整个网格

这种方法的关键在于:每次发现一个新的陆地,我们就将与之相连的整个岛屿都标记为已访问,这样就不会重复计数。

方法二:广度优先搜索(BFS)

BFS的思路与DFS类似,但使用队列而不是递归或栈:

  1. 遍历整个网格
  2. 当找到一个值为’1’的单元格(陆地)时,将岛屿计数器加1
  3. 使用BFS将与当前陆地相连的所有陆地标记为已访问
  4. 继续遍历网格,重复步骤2和3,直到遍历完整个网格

难点解析

本题的难点在于:

  1. 理解如何使用DFS或BFS来标记整个岛屿
  2. 正确处理网格的边界条件
  3. 理解为什么我们可以直接修改原网格(将访问过的陆地改为水),而不需要额外的访问数组

下面我们将重点讲解DFS和BFS两种实现方法。

💻 核心代码说明

DFS实现

/**
 * 岛屿数量 - DFS实现
 */
public class NumberOfIslandsDFS {
    
    /**
     * 计算岛屿数量
     * @param grid 二维字符网格,'1'表示陆地,'0'表示水
     * @return 岛屿的数量
     */
    public static int numIslands(char[][] grid) {
        // 边界条件检查
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return 0;
        }
        
        int rows = grid.length;
        int cols = grid[0].length;
        int count = 0;
        
        // 遍历整个网格
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                // 如果当前单元格是陆地
                if (grid[i][j] == '1') {
                    // 岛屿数量加1
                    count++;
                    // 使用DFS将与当前陆地相连的所有陆地标记为已访问
                    dfs(grid, i, j, rows, cols);
                }
            }
        }
        
        return count;
    }
    
    /**
     * DFS遍历与当前陆地相连的所有陆地,并标记为已访问
     * @param grid 二维字符网格
     * @param i 当前行索引
     * @param j 当前列索引
     * @param rows 网格的行数
     * @param cols 网格的列数
     */
    private static void dfs(char[][] grid, int i, int j, int rows, int cols) {
        // 边界条件检查或当前单元格是水
        if (i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] == '0') {
            return;
        }
        
        // 将当前陆地标记为已访问(通过将其改为水)
        grid[i][j] = '0';
        
        // 递归访问上、下、左、右四个方向的相邻单元格
        dfs(grid, i - 1, j, rows, cols);  // 上
        dfs(grid, i + 1, j, rows, cols);  // 下
        dfs(grid, i, j - 1, rows, cols);  // 左
        dfs(grid, i, j + 1, rows, cols);  // 右
    }
    
    // 测试代码
    public static void main(String[] args) {
        // 测试用例1
        char[][] grid1 = {
            {'1', '1', '1', '1', '0'},
            {'1', '1', '0', '1', '0'},
            {'1', '1', '0', '0', '0'},
            {'0', '0', '0', '0', '0'}
        };
        System.out.println("测试用例1的岛屿数量: " + numIslands(grid1));  // 预期输出: 1
        
        // 测试用例2
        char[][] grid2 = {
            {'1', '1', '0', '0', '0'},
            {'1', '1', '0', '0', '0'},
            {'0', '0', '1', '0', '0'},
            {'0', '0', '0', '1', '1'}
        };
        System.out.println("测试用例2的岛屿数量: " + numIslands(grid2));  // 预期输出: 3
    }
}

BFS实现

import java.util.LinkedList;
import java.util.Queue;

/**
 * 岛屿数量 - BFS实现
 */
public class NumberOfIslandsBFS {
    
    /**
     * 计算岛屿数量
     * @param grid 二维字符网格,'1'表示陆地,'0'表示水
     * @return 岛屿的数量
     */
    public static int numIslands(char[][] grid) {
        // 边界条件检查
        if (grid == null || grid.length == 0 || grid[0].length == 0) {
            return 0;
        }
        
        int rows = grid.length;
        int cols = grid[0].length;
        int count = 0;
        
        // 遍历整个网格
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                // 如果当前单元格是陆地
                if (grid[i][j] == '1') {
                    // 岛屿数量加1
                    count++;
                    // 使用BFS将与当前陆地相连的所有陆地标记为已访问
                    bfs(grid, i, j, rows, cols);
                }
            }
        }
        
        return count;
    }
    
    /**
     * BFS遍历与当前陆地相连的所有陆地,并标记为已访问
     * @param grid 二维字符网格
     * @param i 当前行索引
     * @param j 当前列索引
     * @param rows 网格的行数
     * @param cols 网格的列数
     */
    private static void bfs(char[][] grid, int i, int j, int rows, int cols) {
        // 创建队列,用于BFS
        Queue<int[]> queue = new LinkedList<>();
        // 将当前陆地加入队列
        queue.offer(new int[]{i, j});
        // 将当前陆地标记为已访问
        grid[i][j] = '0';
        
        // 定义四个方向的偏移量:上、下、左、右
        int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
        
        // BFS遍历
        while (!queue.isEmpty()) {
            int[] current = queue.poll();
            int row = current[0];
            int col = current[1];
            
            // 检查四个方向的相邻单元格
            for (int[] dir : directions) {
                int newRow = row + dir[0];
                int newCol = col + dir[1];
                
                // 检查边界条件和是否是陆地
                if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols && grid[newRow][newCol] == '1') {
                    // 将相邻的陆地加入队列
                    queue.offer(new int[]{newRow, newCol});
                    // 将相邻的陆地标记为已访问
                    grid[newRow][newCol] = '0';
                }
            }
        }
    }
    
    // 测试代码
    public static void main(String[] args) {
        // 测试用例1
        char[][] grid1 = {
            {'1', '1', '1', '1', '0'},
            {'1', '1', '0', '1', '0'},
            {'1', '1', '0', '0', '0'},
            {'0', '0', '0', '0', '0'}
        };
        System.out.println("测试用例1的岛屿数量: " + numIslands(grid1));  // 预期输出: 1
        
        // 测试用例2
        char[][] grid2 = {
            {'1', '1', '0', '0', '0'},
            {'1', '1', '0', '0', '0'},
            {'0', '0', '1', '0', '0'},
            {'0', '0', '0', '1', '1'}
        };
        System.out.println("测试用例2的岛屿数量: " + numIslands(grid2));  // 预期输出: 3
    }
}

代码解析

DFS实现
  1. 主要思路

    • 遍历整个网格,当找到一个陆地时,使用DFS将与之相连的所有陆地标记为已访问
    • 每次发现一个新的陆地(未被标记为已访问),就将岛屿计数器加1
  2. 关键函数

    • numIslands:主函数,遍历网格并计数岛屿
    • dfs:递归函数,标记与当前陆地相连的所有陆地为已访问
  3. 时间复杂度:O(M×N),其中M是网格的行数,N是网格的列数

    • 最坏情况下,我们需要访问网格中的每个单元格一次
  4. 空间复杂度:O(M×N)

    • 最坏情况下,整个网格都是陆地,递归调用栈的深度为M×N
BFS实现
  1. 主要思路

    • 与DFS类似,但使用队列而不是递归来遍历与当前陆地相连的所有陆地
    • 每次发现一个新的陆地,就将岛屿计数器加1,并使用BFS标记整个岛屿
  2. 关键步骤

    • 创建队列,将当前陆地加入队列并标记为已访问
    • 不断从队列中取出陆地,检查其四个方向的相邻单元格
    • 如果相邻单元格是陆地,则将其加入队列并标记为已访问
  3. 时间复杂度:O(M×N),与DFS相同

  4. 空间复杂度:O(min(M,N))

    • 最坏情况下,队列中最多存储min(M,N)个节点(当网格是一条对角线时)

DFS与BFS的比较

  1. 实现复杂度

    • DFS实现更简洁,使用递归
    • BFS实现稍复杂,需要使用队列
  2. 空间复杂度

    • 在最坏情况下,DFS的空间复杂度可能更高(递归调用栈)
    • BFS的空间复杂度通常更可控
  3. 适用场景

    • 如果网格较大且岛屿较小,DFS可能更高效
    • 如果需要按照距离顺序访问单元格,BFS更合适

🌟 对Java初期学习的重要意义

学习"岛屿数量"这个问题对Java初学者有着多方面的重要意义:

1. 图论算法的入门

这个问题是图论搜索算法的经典应用,通过它可以理解DFS和BFS这两种基本的图遍历算法。这些算法在计算机科学中有广泛的应用,是算法学习的基础。🧭

2. 递归思想的实践

DFS实现中的递归调用是理解递归思想的绝佳例子。递归是解决许多复杂问题的强大工具,掌握它对于提升编程能力至关重要。🔄

3. 数据结构的应用

BFS实现中使用了队列这一基本数据结构,这有助于理解如何在实际问题中应用数据结构。选择合适的数据结构是解决算法问题的关键。📊

4. 二维数组的操作

这个问题涉及到二维数组的遍历和操作,这是Java编程中的基本技能。熟练掌握二维数组的操作对于解决矩阵、图像处理等问题非常重要。🔢

5. 面试高频题目

"岛屿数量"是技术面试中的常见题目,掌握它不仅能提高你的编程能力,还能增加你在面试中的竞争力。特别是当你能够同时解释DFS和BFS两种解法时,会给面试官留下深刻印象。💼

6. 实际应用场景

这个算法在实际开发中有很多应用场景,如:

  • 图像处理中的连通区域标记
  • 网络拓扑分析
  • 地图中的区域识别
  • 游戏开发中的地图生成和路径规划

学习这个算法有助于你在实际工作中解决类似问题。🌐

📝 总结

今天我们一起学习了"岛屿数量"这个经典算法问题。我们不仅了解了它的基本概念和实现方法,还探讨了DFS和BFS两种解法的异同。

通过这个问题,我们学到了以下几点:

  1. 问题抽象的能力:我们将现实问题(计算岛屿数量)抽象为图论问题(连通分量计数)。

  2. DFS和BFS的应用:我们学习了如何使用这两种基本的图遍历算法来解决实际问题。

  3. 标记已访问节点的技巧:我们通过将访问过的陆地改为水,巧妙地避免了使用额外的访问数组。

  4. 边界条件的处理:我们学习了如何正确处理网格边界和无效输入。

这个问题虽然看起来简单,但它包含了图论算法的精髓。掌握了这个问题,你就掌握了DFS和BFS的基本应用,为学习更复杂的图算法打下了坚实基础。

希望通过这篇文章,你不仅学会了解决"岛屿数量"问题的方法,更重要的是理解了背后的算法思想。在编程的道路上,这些思想和技巧将会一直伴随着你,帮助你解决各种各样的挑战。💪

记住,算法学习是一个循序渐进的过程,今天的每一步都是为了明天的飞跃做准备。希望你能将今天学到的知识应用到实际问题中,不断提升自己的编程能力!🌈

如果你对这个问题还有任何疑问,或者想了解更多相关的算法和数据结构,欢迎在评论区留言交流!我们下次再见!👋


学习提示:尝试解决"岛屿的最大面积"问题,该问题要求计算网格中最大的岛屿面积。这将帮助你更深入地理解DFS和BFS在网格问题中的应用!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红目香薰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值