算法题 单词搜索

79. 单词搜索

问题描述

给定一个 m x n 的字符网格 board 和一个字符串 word,判断 word 是否存在于网格中。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格指水平或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例
在这里插入图片描述

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
解释:路径 A → B → C → C → E → D 可以构成单词

算法思路

回溯法(DFS)

  1. 遍历起点
    遍历网格的每个位置作为搜索起点。
  2. DFS 搜索
    • 若当前字符匹配,向四个方向(上、右、下、左)递归搜索下一字符
    • 使用原地修改标记已访问(避免额外空间)
    • 搜索后恢复网格状态(回溯)
  3. 终止条件
    • 成功:匹配完整个单词
    • 失败:越界或字符不匹配

代码实现

class Solution {
    // 四个移动方向:上、右、下、左
    private static final int[][] DIRECTIONS = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
    
    public boolean exist(char[][] board, String word) {
        int m = board.length, n = board[0].length;
        // 遍历每个起点
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(board, word, i, j, 0)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * 回溯搜索函数
     * 
     * @param board 字符网格
     * @param word  目标单词
     * @param i     当前行坐标
     * @param j     当前列坐标
     * @param index 当前匹配的字符索引
     * @return      是否匹配成功
     */
    private boolean dfs(char[][] board, String word, int i, int j, int index) {
        // 终止条件1:越界或字符不匹配
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length 
            || board[i][j] != word.charAt(index)) {
            return false;
        }
        
        // 终止条件2:已匹配整个单词
        if (index == word.length() - 1) {
            return true;
        }
        
        // 标记当前单元格已访问(原地修改)
        char temp = board[i][j];
        board[i][j] = '#';  // 使用特殊字符标记
        
        // 向四个方向递归搜索
        for (int[] dir : DIRECTIONS) {
            int newI = i + dir[0], newJ = j + dir[1];
            if (dfs(board, word, newI, newJ, index + 1)) {
                return true;
            }
        }
        
        // 回溯:恢复网格状态
        board[i][j] = temp;
        return false;
    }
}

代码注释

代码部分说明
DIRECTIONS定义四个移动方向(上、右、下、左)
exist() 双层循环遍历所有可能的搜索起点
dfs() 边界检查检查坐标是否越界或字符不匹配
index == word.length()-1成功匹配整个单词
board[i][j] = '#'原地修改标记已访问
for (int[] dir : DIRECTIONS)遍历四个方向递归搜索
board[i][j] = temp回溯时恢复网格状态

算法过程

board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"

  1. 起点 (0,0)

    • 匹配 ‘A’ → 标记为 ‘#’
    • 向右 (0,1):匹配 ‘B’ → 标记
    • 向右 (0,2):匹配 ‘C’ → 标记
    • 向下 (1,2):匹配 ‘C’ → 标记
    • 向下 (2,2):匹配 ‘E’ → 标记
    • 向左 (2,1):匹配 ‘D’ → 成功(index=5
  2. 搜索路径
    (0,0) → (0,1) → (0,2) → (1,2) → (2,2) → (2,1)

复杂度分析

  • 时间复杂度:O(mn × 3^L)
    • 最坏情况:遍历所有起点 (m×n),每个起点 DFS 搜索约 3^L 次(L 为单词长度)
    • 每次递归有 3 个方向可选(因不会回退)
  • 空间复杂度:O(L)
    • 递归调用栈深度最大为 L(单词长度)

关键点

  1. 回溯框架
    • 选择:匹配当前字符
    • 递归:向四个方向搜索下一字符
    • 撤销:恢复网格状态
  2. 原地标记
    通过修改网格为特殊字符标记访问状态,避免额外空间
  3. 方向处理
    使用方向数组简化代码,避免重复逻辑
  4. 剪枝优化
    一旦找到有效路径立即返回,不再继续搜索

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 示例测试
    char[][] board1 = {
        {'A','B','C','E'},
        {'S','F','C','S'},
        {'A','D','E','E'}
    };
    System.out.println(solution.exist(board1, "ABCCED")); // true
    System.out.println(solution.exist(board1, "SEE"));    // true
    System.out.println(solution.exist(board1, "ABCB"));   // false
    
    // 单字符测试
    char[][] board2 = {{'A'}};
    System.out.println(solution.exist(board2, "A"));      // true
    
    // 重复字符测试
    char[][] board3 = {
        {'A','A','A','A'},
        {'A','A','A','A'},
        {'A','A','A','A'}
    };
    System.out.println(solution.exist(board3, "AAAAAAAA")); // true
    
    // 无解测试
    char[][] board4 = {
        {'X','Y','Z'},
        {'Z','Y','X'}
    };
    System.out.println(solution.exist(board4, "XYZZ"));   // false
}

常见问题

  1. 为什么需要回溯恢复网格状态?
    因为不同路径搜索需要独立的访问状态,恢复状态确保后续搜索的正确性。

  2. 为什么是 3 个方向而不是 4 个?
    从当前单元格出发时,不会回退到上一个单元格(已标记访问),每个节点最多有 3 个新方向。

  3. 如何处理空单词?
    题目保证 word 非空,无需特殊处理。

  4. 最坏情况性能如何?
    最坏情况需遍历所有路径(如全 ‘A’ 网格中搜索 “AAA…”),但实际通过起点剪枝可提前终止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值