今天我们来聊聊LeetCode上这道经典的岛屿周长问题(463题)。作为一名经常刷题的开发者,我发现这道题虽然看起来简单,但其中蕴含着不少值得深思的算法思维。题目描述是这样的:给定一个二维网格,其中1代表陆地,0代表水域,我们需要计算这个岛屿的周长。
首先,我们需要明确几个关键条件:
每个陆地单元格默认贡献4条边(因为它是正方形),但当两个陆地单元格相邻时,它们共享的边不应该计入周长。这就是我们解题的核心思路。
注意:题目明确说明网格是矩形,且宽度和高度不超过100,这意味着我们不需要考虑极端情况下的性能问题。
最直观的解法是遍历整个网格,对于每个陆地单元格:
为什么只检查上方和左方?因为这样可以避免重复计算。如果我们检查所有四个方向(上、下、左、右),会导致同一条边被减去两次。
让我们深入分析给出的Python解决方案,理解其中的精妙之处。
python复制class Solution:
def islandPerimeter(self, grid: List[List[int]]) -> int:
r, c, ans = len(grid), len(grid[0]), 0
for i in range(r):
for j in range(c):
if grid[i][j] == 1:
ans += 4
if i > 0 and grid[i - 1][j] == 1:
ans -= 2
if j > 0 and grid[i][j - 1] == 1:
ans -= 2
return ans
这段代码非常简洁,但包含了几个关键点:
代码中的边界检查也很重要:
i > 0确保不会访问第0行上方的单元格j > 0确保不会访问第0列左侧的单元格这种处理方式避免了数组越界的问题,是编写网格类题目时的常见技巧。
这个算法的时间复杂度是O(n×m),其中n是网格的行数,m是网格的列数。因为我们只对每个单元格进行一次检查,没有嵌套的额外循环。
对于题目给定的约束(网格尺寸不超过100×100),这个复杂度完全在可接受范围内。
算法的空间复杂度是O(1),因为我们只使用了固定数量的额外变量(r, c, ans等),没有使用与输入规模相关的额外存储空间。
虽然上述解法已经很高效,但了解其他解法有助于拓宽思路。
另一种思路是计算每个陆地单元格的邻居数量:
python复制def islandPerimeter(grid):
perimeter = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == 1:
perimeter += 4
# 检查上方邻居
if i > 0 and grid[i-1][j] == 1:
perimeter -= 1
# 检查下方邻居
if i < len(grid)-1 and grid[i+1][j] == 1:
perimeter -= 1
# 检查左方邻居
if j > 0 and grid[i][j-1] == 1:
perimeter -= 1
# 检查右方邻居
if j < len(grid[0])-1 and grid[i][j+1] == 1:
perimeter -= 1
return perimeter
这种方法更直观,但效率稍低,因为每个共享边实际上被计算了两次(从两个相邻单元格的角度)。
还有一种思路是计算陆地和水域的交界处数量:
python复制def islandPerimeter(grid):
perimeter = 0
rows = len(grid)
cols = len(grid[0]) if rows > 0 else 0
for i in range(rows):
for j in range(cols):
if grid[i][j] == 1:
# 检查上方是否是边界或水
if i == 0 or grid[i-1][j] == 0:
perimeter += 1
# 检查下方
if i == rows-1 or grid[i+1][j] == 0:
perimeter += 1
# 检查左方
if j == 0 or grid[i][j-1] == 0:
perimeter += 1
# 检查右方
if j == cols-1 or grid[i][j+1] == 0:
perimeter += 1
return perimeter
这种方法直接计算岛屿的边缘,思路不同但同样有效。
在解决这类问题时,开发者常会遇到一些典型错误:
忘记处理网格边界是常见错误。例如,在第一行时不应该检查上方单元格,在第一列时不应该检查左侧单元格。我们的原始解法通过条件判断i > 0和j > 0巧妙地避免了这个问题。
如果同时检查四个方向的邻居而不做适当处理,会导致共享边被重复减去。原始解法只检查上方和左方邻居,避免了这个问题。
有些开发者可能会尝试从岛屿的某个点开始DFS或BFS遍历。虽然可行,但实现起来更复杂,而且对于这种计算型问题,简单的网格扫描通常更高效。
调试技巧:当你的结果与预期不符时,可以尝试在小网格上手动计算,逐步跟踪程序执行过程,找出计算偏差的位置。
虽然这个问题看起来是纯算法练习,但它有实际的应用场景:
在图像处理中,类似的算法可用于计算物体的周长或边缘长度。例如,计算显微镜图像中细胞的周长,或者卫星图像中湖泊的边界长度。
在游戏开发中,特别是策略游戏或模拟游戏中,可能需要计算地图上区域或领地的边界长度,用于资源分配或战斗计算。
GIS系统中经常需要计算地块、行政区划或其他地理特征的边界长度,类似的算法可以派上用场。
虽然当前解法已经足够高效,但我们可以思考进一步的优化可能:
如果我们确定岛屿是连续的(题目保证只有一个岛屿),可以在找到第一个陆地单元格后,使用DFS或BFS只遍历陆地部分,跳过水域。但对于小网格(100×100),这种优化可能不明显。
对于非常大的网格,可以考虑将网格分块,使用多线程并行计算各部分,最后合并结果。但需要注意边界处的协调。
按行主序(row-major)遍历网格(如我们当前的做法)通常比按列主序更高效,因为现代计算机的内存架构对连续内存访问更友好。
为了巩固这类问题的解法,可以尝试以下LeetCode题目:
这些题目都涉及网格遍历和连通区域分析,解法有相通之处。
在实现这类算法时,良好的编码习惯很重要:
使用有意义的变量名,如rows和cols比简单的r和c更易读。但在算法竞赛或面试中,简洁的变量名有时也被接受。
对于更复杂的网格问题,考虑将不同功能分解到辅助函数中,如is_valid_cell()检查坐标是否有效,count_neighbors()计算邻居数量等。
编写全面的测试用例,包括:
如果在面试中遇到这类问题,可以按照以下步骤展开:
记住,面试官不仅考察你的解题能力,还关注你的问题分析过程和沟通能力。
在实际编写代码时,我发现最有效的调试方法是在纸上画出小网格,手动模拟算法执行过程。这能帮助快速发现逻辑错误。对于网格类问题,边界条件总是最容易出错的地方,需要特别小心。