leetcode
leetcode 251 ~ 300
岛屿数量 II

岛屿数量 II

难度:

标签:

题目描述

代码结果

运行时间: 100 ms, 内存: 20.4 MB


/*
题目思路:
1. 使用Java Stream API来简化代码结构,但核心逻辑依然是使用并查集进行处理。
2. 使用数组记录每个位置的父节点,以此构建并查集。
3. 每次添加新的陆地时,利用Stream对周围的陆地进行合并操作。
4. 返回每次添加操作后的岛屿数量。
*/
 
import java.util.*;
import java.util.stream.IntStream;
 
public class NumberOfIslandsIIStream {
    private int[] parent;
    private int[] rank;
    private int count;
 
    public List<Integer> numIslands2(int m, int n, int[][] positions) {
        List<Integer> result = new ArrayList<>();
        parent = new int[m * n];
        rank = new int[m * n];
        Arrays.fill(parent, -1);
        count = 0;
 
        int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
 
        for (int[] position : positions) {
            int row = position[0];
            int col = position[1];
            int index = row * n + col;
            if (parent[index] != -1) { // already land
                result.add(count);
                continue;
            }
 
            parent[index] = index;
            count++;
 
            IntStream.range(0, directions.length)
                     .mapToObj(i -> new int[]{row + directions[i][0], col + directions[i][1], (row + directions[i][0]) * n + (col + directions[i][1])})
                     .filter(newPos -> newPos[0] >= 0 && newPos[0] < m && newPos[1] >= 0 && newPos[1] < n && parent[newPos[2]] != -1)
                     .forEach(newPos -> union(index, newPos[2]));
 
            result.add(count);
        }
 
        return result;
    }
 
    private int find(int i) {
        if (parent[i] != i) {
            parent[i] = find(parent[i]); // path compression
        }
        return parent[i];
    }
 
    private void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX != rootY) {
            if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else {
                parent[rootY] = rootX;
                rank[rootX]++;
            }
            count--;
        }
    }
}

解释

方法:

此题解采用了并查集的数据结构来追踪和管理岛屿的连接状态。每个单元格都初始化为其自身的代表(自环),表示各自为独立的岛屿。遍历每个位置,若当前位置是陆地,则检查其四周的单元格。如果相邻的单元格已经是陆地,并且属于不同的集合,则通过合并这两个集合来减少岛屿数量。每合并一次,岛屿数量减一。最后,将当前的岛屿数量加入结果列表。

时间复杂度:

O(m)

空间复杂度:

O(row * col + m)

代码细节讲解

🦆
在并查集中,每次合并两个岛屿时,为什么选择将一个岛屿的代表节点直接指向另一个岛屿的代表节点?这样的操作是否会影响结构的平衡性?
在并查集中,合并两个集合通常是通过将一个集合的代表节点指向另一个集合的代表节点来完成的。这种方法快速有效,但如果不加管理,确实可能会影响结构的平衡性,导致查找操作的效率下降。在最坏的情况下,这可能导致查找操作的时间复杂度接近线性。为了优化这一点,常见的改进方法包括按秩合并(合并时考虑树的大小或高度)和路径压缩(在查找时扁平化结构)。这样可以帮助保持树的高度较低,从而优化操作的效率。
🦆
在处理边界条件时,你是如何确保不会对数组越界进行操作的?具体是通过哪些条件来判断的?
在处理边界条件时,为了确保不会对数组进行越界操作,代码中添加了具体的判断条件来检查数组索引的有效性。具体来说,在检查一个单元格的相邻单元格是否为陆地时,需要确保这些相邻单元格的行号和列号都在有效的范围内。这通过条件 `row > nr > -1 < nc < col` 来实现,即确保行号 `nr` 在0和 `row-1` 之间,列号 `nc` 在0和 `col-1` 之间。这样可以防止访问无效的数组索引,避免运行时错误。
🦆
并查集数据结构在初始化时将每个点设为自己的代表。这种初始化方式是否适用于所有并查集的应用场景,还是仅限于特定问题?
并查集数据结构将每个点初始化为自己的代表是一种通用的初始化方式,适用于大多数使用并查集的场景。这种方式简单明了,可以确保在开始处理任何合并操作之前,每个元素都是自己独立集合的代表。这为后续的合并操作提供了一个清晰的起点。虽然这种初始化方式广泛适用,但在特定情况下,如果能提前知道某些元素将属于同一集合,可以通过初始时直接将这些元素放在同一集合中来优化并查集的结构和性能。
🦆
在遍历positions并更新岛屿数量时,如果同一个位置多次出现在positions中,当前的实现会如何处理?这是否会影响最终的岛屿数量统计?
在当前的实现中,每个位置在 `land` 数组中有一个对应的布尔标记,用来指示该位置是否已经是陆地。当一个位置在 `positions` 中多次出现时,如果该位置已经被标记为陆地,后续的出现将不会再次被计算为新岛屿。这意味着,尽管位置可能多次出现,但只有第一次将其转变为陆地时才会增加岛屿数量。因此,这种处理方式确保了岛屿数量的统计不会因为位置的重复出现而受到影响。

相关问题

岛屿数量

给你一个由 '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'