热题100 - 79. 单词搜索

题目解析与优化思考全记录

一、初始思路与问题暴露

1.1 基础递归构想

面对单词搜索问题,首先形成递归思路: • 起点定位 :通过哈希表预存所有字符坐标(Map<Character, List<Location>>) • 路径验证 :每次递归需满足两点: • 当前字符匹配 • 新位置与上一位置相邻(曼哈顿距离为1) • 回溯机制 :使用二维布尔数组visited[][]记录访问状态

java 复制代码
// 初始递归框架
private boolean dfs(..., Location pre) {
    for (Location l : cache.get(c)) {
        if (!visited[x][y] && l.valid(pre)) {
            visited[x][y] = true;
            boolean res = dfs(..., l);
            visited[x][y] = false; // 回溯
        }
    }
}

1.2 暴露的性能问题

测试结果 :首次实现耗时1656ms 问题诊断

  1. 无效的位置遍历:每个递归步骤遍历所有缓存位置,而非直接相邻点
java 复制代码
// 错误示例:遍历所有'E'的位置(可能数百个)
List<Location> locations = cache.get('E');
for (Location l : locations) { // O(mn) 复杂度
    if (l.valid(pre)) { ... } // 实际有效位置仅4个
}
  1. 字符串拷贝开销word.substring()产生O(k²)时间开销
  2. 空间冗余 :独立visited数组占用O(mn)空间

二、回溯法的实现与瓶颈

2.1 第一版完整代码

java 复制代码
// 包含Location类和缓存机制(详见原始代码)

运行结果:1656ms,仅击败5%提交

2.2 关键瓶颈分析

瓶颈点 问题描述 理论复杂度
缓存位置遍历 每个步骤遍历O(mn)位置,而非相邻4个位置 O((mn)^k) → 指数爆炸
字符串截取 word.substring()产生大量临时对象 O(k²)
独立访问标记数组 额外空间占用,缓存命中率低 O(mn)
冗余校验 每次递归都需计算曼哈顿距离,而非直接方向遍历 O(1) → 累计开销大

三、逐步优化的关键步骤

3.1 优化1:方向遍历取代缓存遍历(关键突破)

修改策略

java 复制代码
// 仅检查四个相邻方向
List<Location> neighbours = new ArrayList<>();
if (pre != null) {
    int x = pre.getX(), y = pre.getY();
    // 生成上下左右四个坐标
}

效果 :耗时降至94ms,击败70%提交
复杂度变化:O((mn)^k) → O(mn * 3^k)

3.2 优化2:索引传递取代子字符串

修改点

java 复制代码
// 原递归参数
dfs(..., word.substring(1)) 

// 修改后
dfs(..., index+1)

效果 :耗时降至76ms
原理:消除字符串拷贝,时间复杂度从O(k²)→O(k)

3.3 优化3:原地修改取代visited数组

空间优化

java 复制代码
char original = board[x][y];
board[x][y] = '#'; // 标记已访问
// ... 递归 ...
board[x][y] = original; // 回溯

效果 :空间复杂度从O(mn)→O(k)
附带收益:提高CPU缓存命中率,耗时降至26ms


四、错误尝试的认知价值

4.1 缓存机制的误区

初始假设 :预存字符位置能加速查找
实际结果 :导致更差的复杂度
本质矛盾:单词搜索的连续性特征使得全局缓存失去意义,相邻关系才是核心

4.2 曼哈顿距离校验的冗余

错误实现

java 复制代码
// Location类中的校验方法
public boolean valid(Location l) {
    return Math.abs(x-l.x) + Math.abs(y-l.y) == 1;
}

问题本质:方向遍历已隐含相邻关系,显式计算反而增加开销

4.3 过度泛化的类设计

Location类缺陷

java 复制代码
class Location { // 40行类定义
    private int x, y;
    // 包含多个距离计算方法...
}

优化启示 :简单场景使用原生类型更高效,最终用int[2]数组替代


五、最终高效解法

5.1 整合所有优化的代码

java 复制代码
class Solution {
    public boolean exist(char[][] board, String word) {
        // 预处理与缓存检查
        Map<Character, List<int[]>> cache = new HashMap<>();
        // ... (同优化后代码)
        
        for (int[] start : cache.getOrDefault(word.charAt(0), new ArrayList<>())) {
            if (dfs(board, start[0], start[1], word, 0)) return true;
        }
        return false;
    }

    private boolean dfs(char[][] board, int x, int y, String word, int index) {
        // 整合索引传递与原地修改
        if (index == word.length()) return true;
        // ... (边界检查与递归逻辑)
    }
}

5.2 性能对比

版本 耗时 击败率 关键优化点
初始实现 1656ms 5% 基础回溯
方向遍历优化 94ms 70% 限定四个相邻方向
索引传递优化 76ms 85% 消除字符串拷贝
原地修改优化 26ms 95.4% 空间优化+CPU缓存优化

5.3 总结启示

  1. 问题特征优先:连续路径搜索问题中,局部方向遍历优于全局缓存
  2. 空间换时间慎用:额外的数据结构可能引入更大复杂度
  3. JVM特性利用:减少对象创建、提高缓存命中能带来意外增益
  4. 剪枝要精准:预处理检查与运行时剪枝需配合使用

保留错误思考的价值在于:正是通过分析缓存遍历的低效性,才深刻理解到方向遍历的优越性;正是经历了冗余类设计的痛苦,才体会到简单数据结构的精妙。这些认知跨越构成了算法优化的完整思维图谱。

相关推荐
sin258014 分钟前
蓝桥杯C++基础算法-0-1背包(优化为一维)
c++·算法·蓝桥杯
随行就市28 分钟前
树的深度优先(DFS)和广度优先(BFS)算法
算法·深度优先·宽度优先
多多*30 分钟前
Java 双端队列实战 实现滑动窗口 用LinkedList的基类双端队列Deque实现 洛谷[P1886]
java·开发语言·数据结构·算法·cocoa
程序员老冯头32 分钟前
第八节 MATLAB运算符
开发语言·算法·matlab
CYRUS_STUDIO41 分钟前
Android 自定义变形 HMAC 算法
android·算法·安全
uhakadotcom1 小时前
零基础玩转千卡训练!Modalities框架中文指南:从安装到实战的全解析
算法·面试·github
筑梦之月1 小时前
常用密码学算法分类
算法·密码学
篮l球场2 小时前
搜索二维矩阵
算法
网安秘谈2 小时前
密码学国密算法深度解析:SM2椭圆曲线密码与SM3密码杂凑算法
算法·密码学
小羊在奋斗3 小时前
【算法】动态规划:回文子串问题、两个数组的dp
算法·动态规划