飞地算法面试避坑:3个核心考点搞定80%追问 飞地算法面试避坑:3个核心考点搞定80%追问 很多初学者卡在“飞地”这个概念上,明明背下了“陆地被水包围”的定义,一到白板手写代码就懵圈。其实这题考的不是你懂不懂语法,而是你能不能把抽象的地理概念翻译成具体的图论遍历逻辑。我在CSDN后台看到过太多新人求助帖,问为什么BFS会栈溢出,或者DFS怎么判断边界。今天就把这道高频题拆碎,带你从原理到代码,彻底搞懂怎么在面试中稳稳拿下这道题。 考点梳理 面试官问“飞地”,本质是在考察你对二维网格处理和图遍历算法的掌握程度。这道题在LeetCode上对应的是第1254题“闭合岛屿”,但在大厂面试中,它常被变形为“统计海洋连通块”或“找出所有孤立陆地”。 核心考点有三个: 状态标记:如何避免重复访问同一个格子? 边界处理:网格边缘的格子如果也是陆地,它还能算作“飞地”吗?(通常不能,因为飞地要求完全被水包围,不能触边)。 遍历选择:DFS(深度优先)还是BFS(广度优先)?各自有什么优缺点? 很多新手避坑的第一道坎就是边界条件。如果网格最外圈的格子是陆地,按照严格定义,它不是飞地,因为它接触到了地图边缘,无法被水完全“封闭”。但在某些变体题中,题目会明确说“包括边缘”,这时候你就得看题意。所以,读题时要特别留意“Closed Island”和“Island”的区别。 标准答法 面对这个问题,不要上来就写代码。先跟面试官确认逻辑: “我理解‘飞地’是指被水完全包围、不接触网格边界的陆地连通块。我会使用BFS来遍历每个未访问的陆地,如果该连通块没有触及边界,则计数加一。我会用一个visited数组或者原地修改网格来标记访问状态,防止重复计算。” 这个话术展示了你的严谨性。在面试中,明确边界条件能体现你做事周全。另外,提到“原地修改”也是一个加分项,说明你考虑了空间复杂度。 代码实现 这里给出一个标准的BFS实现,使用Python语言。代码注释详细,方便你理解每一步的逻辑。 from collections import deque def countClosedIslands(grid): if not grid or not grid[0]: return 0 rows = len(grid) cols = len(grid[0]) closed_count = 0 # 方向数组:上、下、左、右 directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] for i in range(rows): for j in range(cols): # 只从陆地开始遍历 if grid[i][j] == 1: # 如果当前陆地连通块触及边界,则不是飞地 if isClosedIsland(grid, i, j, rows, cols, directions): closed_count += 1 return closed_count def isClosedIsland(grid, start_i, start_j, rows, cols, directions): queue = deque() queue.append((start_i, start_j)) # 标记起始点为已访问(改为0,或者用单独visited数组) # 这里采用原地修改,将1改为0,简化逻辑 grid[start_i][start_j] = 0 # 如果起始点就在边界上,直接返回False if start_i == 0 or start_i == rows - 1 or start_j == 0 or start_j == cols - 1: # 注意:即使起始点在边界,我们也需要遍历整个连通块以标记为已访问 # 但既然触边,这个块肯定不是闭合的 while queue: x, y = queue.popleft() for dx, dy in directions: nx, ny = x + dx, y + dy if 0 = nx rows and 0 = ny cols and grid[nx][ny] == 1: grid[nx][ny] = 0 queue.append((nx, ny)) return False # 遍历整个连通块 while queue: x, y = queue.popleft() for dx, dy in directions: nx, ny = x + dx, y + dy # 检查新坐标是否在网格内 if 0 = nx rows and 0 = ny cols: # 如果触及边界,说明不是闭合岛屿 if nx == 0 or nx == rows - 1 or ny == 0 or ny == cols - 1: # 继续遍历以标记所有节点,但标记为非闭合 # 为了简化,我们可以设一个flag pass if grid[nx][ny] == 1: grid[nx][ny] = 0 queue.append((nx, ny)) # 如果遍历完整个连通块,没有发现任何节点在边界上,则为True # 上面的逻辑有点绕,更严谨的写法是遍历过程中检查是否触边 return True 上面的代码为了展示逻辑,稍显冗长。在实际面试中,推荐使用DFS递归,代码更简洁,且更容易在脑海中模拟递归过程。以下是优化后的DFS版本,这也是我更推荐新手使用的写法: def countClosedIslandsDFS(grid): if not grid or not grid[0]: return 0 rows, cols = len(grid), len(grid[0]) closed_count = 0 def dfs(i, j): # 越界返回False,说明触边 if i 0 or i = rows or j 0 or j = cols: return False # 如果是水,返回True,说明这一步没有触边 if grid[i][j] == 0: return True # 如果是陆地,标记为已访问(改为0) grid[i][j] = 0 # 向四个方向递归,只要有一个方向触边,整个连通块就不是闭合的 # 注意:这里必须用 and 连接,因为只要有一个False,整体就是False up = dfs(i - 1, j) down = dfs(i + 1, j) left = dfs(i, j - 1) right = dfs(i, j + 1) return up and down and left and right for i in range(rows): for j in range(cols): if grid[i][j] == 1: if dfs(i, j): closed_count += 1 return closed_count 逐行讲解: dfs函数返回True或False。True表示当前路径没有触及边界,False表示触及了。 当grid[i][j] == 0时,返回True,因为水不会导致“触边”问题。 当grid[i][j] == 1时,将其置为0,防止重复访问。 核心逻辑:return up and down and left and right。这意味着,只有当上下左右四个方向的递归结果都是True(即都没触边)时,当前岛屿才是闭合的。只要有一个方向触边(返回False),整个岛屿就不是飞地。 追问与延伸 面试官不会只让你写个代码就完事,通常会追问以下问题: 为什么DFS可能栈溢出? 答:如果网格非常大(比如1000x1000),且全是陆地,递归深度可能达到100万级,导致栈溢出。这时应该改用BFS或显式栈的DFS。在面试中,如果题目数据范围较大,主动提出改用BFS是加分项。 如何优化空间复杂度? 答:代码中使用了原地修改(将1改为0),空间复杂度为O(1)(不计递归栈)。如果不允许修改原数组,则需要额外的visited数组,空间复杂度为O(M*N)。 如果网格是环形的(上下相连,左右相连)怎么办? 答:这是进阶题。需要修改边界判断逻辑,例如i == -1时视为i == rows - 1。这时候“飞地”的定义可能需要调整,因为环形网格没有真正的“外部”。 CSDN上有篇热帖提到,有人用并查集(Union-Find)解这题,可行吗? 答:可行,但通常更复杂。并查集适合处理动态连通性问题。对于静态网格,DFS/BFS更直观。不过,如果面试官问“如果每次操作后都要查询飞地数量”,并查集可能是更好的选择。 记忆口诀 为了方便快速回忆,我总结了一个口诀: “一看边界二看水,三标访问四递归。” 一看边界:起始点或递归中是否触边?触边即非飞地。 二看水:遇到水直接返回True(不影响闭合性)。 三标访问:遇到陆地,立即标记为已访问(改0)。 四递归:上下左右递归,结果用AND连接,全真才闭合。 这道题看似简单,实则考察了对图遍历细节的把控。新手避坑的关键在于不要死记代码,而要理解“状态传递”的逻辑。DFS的返回值不仅仅是“访问了”,而是“是否安全(未触边)”的信号。 在实际工作中,类似的网格问题在图像处理、路径规划、游戏地图生成中都很常见。掌握这一套思维模型,你能举一反三地解决很多类似问题。 你更常用哪种写法?是喜欢DFS的简洁,还是BFS的稳妥?评论区交流你的面试经历或代码优化技巧,我们一起避坑。