当前位置: 首页 > news >正文

【LeetCode 热题 100】139. 单词拆分——(解法一)记忆化搜索

Problem: 139. 单词拆分

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(N * L^2)
    • 空间复杂度:O(N + D)

整体思路

这段代码旨在解决经典的 “单词拆分” (Word Break) 问题。问题要求判断一个给定的非空字符串 s 是否可以被分割成一个或多个在字典 wordDict 中出现的单词。

该算法采用的是一种 自顶向下(Top-Down)的动态规划 方法,即 记忆化搜索 (Memoization)。它将“字符串s能否被拆分”的大问题,分解为“s的某个前缀能否被拆分”的子问题,并通过递归解决,同时缓存子问题的解以避免重复计算。

算法的核心逻辑步骤如下:

  1. 预处理

    • 建立快速查询字典:将输入的 List<String> wordDict 转换为一个 HashSet<String> words。这是一个关键的性能优化,因为它将查询一个单词是否在字典中的时间复杂度从 O(N*L)(遍历列表)降低到了平均 O(L)(哈希查找,L为单词长度)。
    • 计算最大词长 maxLen:遍历字典,找出其中最长单词的长度。这个 maxLen 将用于后续的剪枝优化。
  2. 状态定义与递归关系

    • 算法的核心是递归函数 dfs(i),其状态定义为:
      dfs(i) = 字符串 s 的前 i 个字符(即 s.substring(0, i))是否可以被成功拆分
    • 为了判断 s 的前 i 个字符能否被拆分,算法会尝试所有的最后一个单词的可能性。它会从后往前扫描,寻找一个切分点 j (j < i),使得:
      a. 后半部分 s.substring(j, i) 是一个在字典中的单词。
      b. 前半部分 s.substring(0, j) 可以被成功拆分(这通过递归调用 dfs(j) 来判断)。
    • 只要能找到任何一个满足上述两个条件的切分点 j,就说明 s 的前 i 个字符是可以被拆分的。
  3. 记忆化与剪枝

    • 记忆化 (Memoization):使用一个 memo 数组来存储每个子问题 dfs(i) 的计算结果。memo[i] 的值有三种状态:-1(未计算),0(不可拆分),1(可拆分)。在 dfs(i) 的开头,先检查 memo[i],如果不是-1,就直接返回已存的结果,避免重复的递归树展开。
    • 剪枝 (Pruning):在 dfs(i) 内部的循环中,利用预计算的 maxLen 来缩小搜索范围。切分点 j 无需从 i-1 一直回溯到 0,只需回溯到 i - maxLen 即可。因为如果 i-j > maxLen,那么 s.substring(j, i) 的长度必然大于字典中最长的单词,它不可能是字典的一部分。这个剪枝显著减少了不必要的 substringHashSet.contains 操作。
  4. 基础情况 (Base Case)

    • dfs(0) 代表对空字符串的判断。一个空字符串可以被视作成功拆分(因为它不需要任何单词),所以 dfs(0) 返回 1(代表true)。这是递归的终点。

完整代码

import java.util.*;class Solution {/*** 判断字符串 s 是否可以被字典中的单词拆分。* @param s 目标字符串* @param wordDict 字典单词列表* @return 如果可以拆分则返回 true,否则返回 false*/public boolean wordBreak(String s, List<String> wordDict) {// 1. 预处理:计算字典中单词的最大长度,用于后续剪枝。int maxLen = 0;for (String word : wordDict) {maxLen = Math.max(maxLen, word.length());}// 2. 预处理:将 List 转换为 HashSet,以实现 O(1) 平均时间复杂度的单词查询。Set<String> words = new HashSet<>(wordDict);int n = s.length();// memo: 记忆化数组。memo[i] 存储 dfs(i) 的结果。// -1: 未计算, 0: false (不可拆分), 1: true (可拆分)。int[] memo = new int[n + 1];Arrays.fill(memo, -1);// 启动对整个字符串 s (长度为 n) 的递归求解。return dfs(n, maxLen, s, words, memo) == 1;}/*** 记忆化搜索函数。* @param i 当前要判断的前缀的长度,即 s.substring(0, i)* @param maxLen 字典中单词的最大长度(用于剪枝)* @param s 原始字符串* @param words 字典的 HashSet* @param memo 记忆化数组* @return 1 表示可以拆分,0 表示不可以*/private int dfs(int i, int maxLen, String s, Set<String> words, int[] memo) {// 基础情况:长度为 0 的前缀(空字符串)总是可以被“拆分”的。if (i == 0) {return 1;}// 记忆化检查:如果该子问题已经计算过,直接返回结果。if (memo[i] != -1) {return memo[i];}// 核心循环:尝试所有可能的最后一个单词。// j 是切分点,s.substring(j, i) 是尝试的最后一个单词。// 剪枝优化:j 的下界被 maxLen 限制,避免了不必要的检查。for (int j = i - 1; j >= Math.max(i - maxLen, 0); j--) {// 检查两个条件:// 1. s.substring(j, i) 是否在字典中。// 2. 剩余的前缀 s.substring(0, j) 是否也可以被拆分(递归调用)。if (words.contains(s.substring(j, i)) && dfs(j, maxLen, s, words, memo) == 1) {// 如果找到一种可行的拆分方式,记录结果 1 并立即返回。return memo[i] = 1;}}// 如果循环结束后仍未找到任何可行的拆分方式,记录结果 0 并返回。return memo[i] = 0;}
}

时空复杂度

时间复杂度:O(N * L^2)

  • N 是字符串 s 的长度。
  • L 是字典 wordDict 中单词的最大长度 (maxLen)。
  1. 状态数量:由于记忆化的存在,每个子问题 dfs(i)i0N)只会被实际计算一次。总共有 O(N) 个不同的状态。
  2. 每个状态的计算时间:在 dfs(i) 函数内部,主要的开销来自 for 循环。
    • 循环最多执行 L 次(因为 j 的范围被 maxLen 限制)。
    • 在循环内部,s.substring(j, i) 操作需要 O(L) 的时间,因为它需要复制一个长度最多为 L 的子串。
    • words.contains(...)HashSet 上的操作,其时间复杂度与被检查字符串的长度成正比(用于计算哈希值),因此也是 O(L)。
    • 因此,循环内部单次迭代的复杂度是 O(L)。
    • 总的来说,dfs(i) 的计算时间是 L * O(L) = O(L^2)
  3. 综合分析
    总时间复杂度 = (状态数量) × (每个状态的计算时间) = O(N) * O(L^2)
    预处理部分(构建 HashSet)的时间复杂度为 O(M*k)M为字典词数,k为平均词长),通常被 O(N*L^2) 主导。

空间复杂度:O(N + D)

  • N 是字符串 s 的长度。
  • D 是字典中所有字符的总数。
  1. 记忆化数组 memo:创建了一个大小为 N+1 的数组,占用 O(N) 空间。
  2. 递归调用栈:递归的最大深度可以达到 N(例如,当 s = "aaaa..."dict = ["a"] 时)。因此,递归栈占用的空间是 O(N)
  3. 字典 wordsHashSet 需要存储字典中的所有单词。如果字典中有 M 个单词,平均长度为 k,则其空间为 O(M*k),我们记为 D

综合分析
算法所需的总空间是 O(N) (memo) + O(N) (stack) + O(D) (set)。因此,最终的空间复杂度为 O(N + D)

参考灵神

http://www.xdnf.cn/news/1346257.html

相关文章:

  • 浏览器开发CEFSharp+X86+win7(十三)之Vue架构自动化——仙盟创梦IDE
  • STM32F1 EXTI介绍及应用
  • 光耦合器:电子世界的 “光桥梁“
  • ZYNQ启动流程——ZYNQ学习笔记11
  • X00238-非GNSS无人机RGB图像卫星图像视觉定位python
  • 25年8月通信基础知识补充1:中断概率与遍历容量、Sionna通信系统开源库、各种时延区分
  • Android 16环境开发的一些记录
  • Prometheus+Grafana监控redis
  • 制造企业用档案宝,档案清晰可查
  • 81 柔性数组造成的一些奇怪情况
  • 农业-学习记录
  • 关于 WebDriver Manager (自动管理浏览器驱动)
  • 当下一次攻击发生前:微隔离如何守护高敏数据,防范勒索攻击下的数据泄露风险!
  • 一、Python IDLE安装(python官网下的环境安装)
  • 腾讯云EdgeOne安全防护:快速上手,全面抵御Web攻击
  • Datawhale AI夏令营---coze空间共学
  • 【图像算法 - 21】慧眼识虫:基于深度学习与OpenCV的农田害虫智能识别系统
  • 关于日本服务器的三种线路讲解
  • 在自动驾驶中ESKF实现GINS时,是否将重力g作为变量考虑进去的目的是什么?
  • ASPICE过程能力确定——度量框架
  • Unity--判断一个点是否在扇形区域里面(点乘和叉乘的应用)
  • 视觉语言大模型应用开发——基于 CLIP、Gemini 与 Qwen2.5-VL 的视频理解内容审核全流程实现
  • ref 简单讲解
  • flutter geolocator Android国内定位失败问题解决
  • JVM 调优全流程案例:从频繁 Full GC 到百万 QPS 的实战蜕变
  • 【大模型本地运行与部署框架】Ollama的cmd常用命令
  • Linux 软件编程(九)网络编程:IP、端口与 UDP 套接字
  • 【Python】两条命令永久切国内源
  • 本地组策略编辑器图形化工具
  • 力扣(在排序数组中查找元素的第一个和最后一个位置)