热题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. 剪枝要精准:预处理检查与运行时剪枝需配合使用

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

相关推荐
木子.李3475 小时前
排序算法总结(C++)
c++·算法·排序算法
闪电麦坤956 小时前
数据结构:递归的种类(Types of Recursion)
数据结构·算法
Gyoku Mint6 小时前
机器学习×第二卷:概念下篇——她不再只是模仿,而是开始决定怎么靠近你
人工智能·python·算法·机器学习·pandas·ai编程·matplotlib
纪元A梦6 小时前
分布式拜占庭容错算法——PBFT算法深度解析
java·分布式·算法
px不是xp7 小时前
山东大学算法设计与分析复习笔记
笔记·算法·贪心算法·动态规划·图搜索算法
枫景Maple8 小时前
LeetCode 2297. 跳跃游戏 VIII(中等)
算法·leetcode
鑫鑫向栄8 小时前
[蓝桥杯]修改数组
数据结构·c++·算法·蓝桥杯·动态规划
鑫鑫向栄8 小时前
[蓝桥杯]带分数
数据结构·c++·算法·职场和发展·蓝桥杯
小wanga8 小时前
【递归、搜索与回溯】专题三 穷举vs暴搜vs回溯vs剪枝
c++·算法·机器学习·剪枝