LeetCode 524 通过删除字母匹配到字典里最长单词

发布于:2025-03-27 ⋅ 阅读:(26) ⋅ 点赞:(0)

🔍 从字符串中找出最长可生成子序列单词:双指针与优先级筛选(Java 实现)

🌈 一、问题描述

🎯 核心任务

给定字符串 s 和单词列表 dictionary,找出列表中最长的单词,该单词可通过删除 s 中的某些字符(不改变顺序)得到。

  • 优先级:长度优先 → 字母序最小(长度相同)
  • 返回:符合条件的单词(无则返回 ""

📝 约束条件

  • 时间复杂度:O (n*m)(n = 字典长度,m=s 长度)
  • 空间复杂度:O (1)(仅常数额外空间)

🌰 示例

输入 s 输入 dictionary 输出 解释
"abpcplea" ["ale","apple","monkey","plea"] "apple" 最长子序列(5 位)
"abc" ["ab","ac"] "ab" 长度相同,字母序更小
"a" ["a","b"] "a" 唯一有效单词
"abc" ["def"] "" 无匹配单词

⚡ 二、解决方案:双指针 + 优先级筛选法

🔮 核心原理

  1. 子序列判断(双指针法)
    • 用两个指针遍历单词和字符串,匹配则同步移动,否则仅移动字符串指针。
  2. 优先级筛选
    • 长度优先:保留更长单词
    • 字母序次之:String.compareTo() 实现字典序比较

🚀 算法步骤(三步法)

  1. 遍历字典:对每个单词执行子序列检查。
  2. 子序列判断:双指针法验证是否为 s 的子序列。
  3. 优先级更新:根据长度和字母序更新最优解。

🧩 三、代码实现(Java)

import java.util.List;

class Solution {
    public String findLongestWord(String s, List<String> dictionary) {
        String result = ""; // 初始最优解
        for (String word : dictionary) { // 遍历每个单词
            if (isSubsequence(word, s)) { // 检查是否为子序列
                // 优先级比较:长度 > 字母序
                if (word.length() > result.length() || 
                    (word.length() == result.length() && word.compareTo(result) < 0)) {
                    result = word; // 更新最优解
                }
            }
        }
        return result; // 返回最终结果
    }

    private boolean isSubsequence(String word, String s) {
        int i = 0, j = 0; // 双指针:i→word,j→s
        while (i < word.length() && j < s.length()) {
            if (word.charAt(i) == s.charAt(j)) { // 字符匹配
                i++; // 移动单词指针
            }
            j++; // 始终移动字符串指针
        }
        return i == word.length(); // 单词遍历完成 → 是子序列
    }
}

📊 四、复杂度分析

维度 说明
时间 O (n*m)(n = 字典长度,m=s 长度)
空间 O (1)(仅常数变量:i, j, result 等)

🔍 五、代码逐行解析(以示例 s="abpcplea"dictionary=["apple"] 为例)

  1. 子序列判断
    • word="apple"s="a b p c p l e a"
    • 指针移动:
      i=0 (a) vs j=0 (a) → 匹配,i=1, j=1  
      i=1 (p) vs j=1 (b) → 不匹配,j=2 (p) → 匹配,i=2, j=3  
      i=2 (p) vs j=3 (c) → 不匹配,j=4 (p) → 匹配,i=3, j=5  
      i=3 (l) vs j=5 (l) → 匹配,i=4, j=6  
      i=4 (e) vs j=6 (e) → 匹配,i=5(完成)→ 返回 true  
      
  2. 优先级比较
    • 初始 result=""(长度 0)→ "apple" 长度 5 → 更新。

🧪 六、测试用例

输入 s 输入 dictionary 预期输出 验证点
"abc" ["a","b","abc"] "abc" 最长子序列(3 位)
"ab" ["a","b","ab"] "ab" 长度相同,字母序最小
"abcde" ["a","c","e"] "e" 最长单字符(1 位)
"xyz" ["xya","xbz"] "" 非子序列(顺序不匹配)

💡 七、扩展思考

🔀 变种问题:最短子序列

  • 问题:找最短的有效单词(长度优先降序)。
  • 解法:修改优先级为长度升序,字母序升序。

🚀 优化策略

  1. 预处理字典:按长度降序 + 字母序升序排序,找到第一个有效单词后提前终止。
    dictionary.sort((a, b) -> {
        if (a.length() != b.length()) return b.length() - a.length(); // 长度降序
        else return a.compareTo(b); // 字母序升序
    });
    
  2. 剪枝优化:遍历字典时,若当前单词长度小于最优解长度,跳过。

👥 类似题目

🔄 八、两种方法对比(双指针法 vs 动态规划法)

维度 双指针法 动态规划法
时间 O(n*m) O (n*m)(预处理 O (m))
空间 O(1) O (m)(存储 DP 表)
实现难度 低(双指针逻辑简单) 高(需维护二维数组)
适用场景 通用场景 多次查询子序列(预处理优化)

🎨 九、可视化流程图

遍历字典每个单词 → 双指针检查子序列  
↓  
是子序列 → 比较长度和字母序 → 更新最优解  
↓  
返回最优解(或"")

🌟 十、总结:子序列问题的通用解法

✨ 核心优势

  • 时空平衡:线性时间复杂度,常数空间,适合大规模数据。
  • 逻辑简洁:双指针法直观易懂,避免复杂数据结构。

🚦 适用场景

  • 文本匹配(如日志分析、关键词提取)
  • 字符串编辑(删除字符生成目标单词)
  • 数据校验(验证输入格式合法性)

💡 编程启示

  1. 双指针技巧:处理子序列问题的标配,通过同步遍历实现线性时间判断。
  2. 优先级设计:明确比较规则(长度→字母序),利用字符串内置方法(compareTo)简化实现。

📚 十一、延伸阅读

🎁 最后的话

这道题教会我们如何用双指针的 “舞蹈” 优雅地检查子序列,同时用优先级规则筛选最优解。就像整理书架:先按高度(长度)排序,再按书名(字母序)排列,最终找到最合适的 “那本书”。

下次遇到字符串匹配问题时,记住双指针的魔力:让字符在遍历中自然匹配,让优先级在比较中浮出水面。代码的简洁,正是算法之美的最佳注脚! 📜✨