算法题 匹配子序列的单词数

匹配子序列的单词数

问题描述

给定字符串 s 和一个字符串数组 words,返回 wordss 的子序列的单词数目。

子序列:通过删除 s 中的一些字符(也可以不删除)而不改变剩余字符相对位置所形成的新字符串。

示例

输入: s = "abcde", words = ["a","bb","acd","ace"]
输出: 3
解释: 有三个单词是s的子序列:"a","acd","ace"。

算法思路

暴力

  • 对每个单词都从头开始在 s 中匹配
  • 时间复杂度:O(words.length × s.length × avg_word_length)
  • 对于大量重复单词会重复计算

方法

  1. 预处理 + 二分查找:为每个字符预处理其在 s 中的位置,然后对每个单词使用二分查找
  2. 多指针:为每个单词维护一个指针,同时遍历 s
  3. 缓存:使用哈希表缓存已计算的结果,避免重复单词的重复计算

代码实现

方法一:预处理 + 二分查找

import java.util.*;

class Solution {
    /**
     * 使用预处理和二分查找判断子序列
     * 
     * @param s     源字符串
     * @param words 单词数组
     * @return 是s的子序列的单词数目
     */
    public int numMatchingSubseq(String s, String[] words) {
        // 1: 预处理 - 为每个字符记录其在s中出现的所有位置
        List<Integer>[] positions = new List[26];
        for (int i = 0; i < 26; i++) {
            positions[i] = new ArrayList<>();
        }
        
        for (int i = 0; i < s.length(); i++) {
            positions[s.charAt(i) - 'a'].add(i);
        }
        
        // 2: 使用缓存避免重复计算
        Map<String, Boolean> cache = new HashMap<>();
        int count = 0;
        
        // 3: 对每个单词判断是否为子序列
        for (String word : words) {
            if (cache.containsKey(word)) {
                if (cache.get(word)) {
                    count++;
                }
                continue;
            }
            
            boolean isSubseq = isSubsequence(word, positions);
            cache.put(word, isSubseq);
            if (isSubseq) {
                count++;
            }
        }
        
        return count;
    }
    
    /**
     * 使用二分查找判断单词是否为子序列
     * 
     * @param word      待检查的单词
     * @param positions 字符位置预处理数组
     * @return true表示是子序列,false表示不是
     */
    private boolean isSubsequence(String word, List<Integer>[] positions) {
        int prevIndex = -1; // 上一个匹配字符在s中的位置
        
        for (char c : word.toCharArray()) {
            List<Integer> charPositions = positions[c - 'a'];
            
            // 如果字符c在s中不存在,直接返回false
            if (charPositions.isEmpty()) {
                return false;
            }
            
            // 二分查找第一个大于prevIndex的位置
            int left = 0, right = charPositions.size();
            while (left < right) {
                int mid = left + (right - left) / 2;
                if (charPositions.get(mid) <= prevIndex) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }
            
            // 如果没有找到合适的位置
            if (left == charPositions.size()) {
                return false;
            }
            
            // 更新prevIndex为找到的位置
            prevIndex = charPositions.get(left);
        }
        
        return true;
    }
}

方法二:多指针

import java.util.*;

class Solution {
    /**
     * 使用多指针判断子序列
     * 为每个单词维护一个指针,同时遍历s
     */
    public int numMatchingSubseq(String s, String[] words) {
        // 使用缓存避免重复计算
        Map<String, Integer> wordCount = new HashMap<>();
        for (String word : words) {
            wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
        }
        
        // 为每个唯一单词创建指针
        Map<String, Integer> pointers = new HashMap<>();
        for (String word : wordCount.keySet()) {
            pointers.put(word, 0);
        }
        
        int matchedCount = 0;
        
        // 遍历s的每个字符
        for (char c : s.toCharArray()) {
            // 复制需要更新的单词列表
            List<String> toRemove = new ArrayList<>();
            
            // 检查每个单词的当前指针位置
            for (String word : pointers.keySet()) {
                int ptr = pointers.get(word);
                if (ptr < word.length() && word.charAt(ptr) == c) {
                    ptr++;
                    pointers.put(word, ptr);
                    
                    // 如果单词完全匹配
                    if (ptr == word.length()) {
                        matchedCount += wordCount.get(word);
                        toRemove.add(word);
                    }
                }
            }
            
            // 移除已完全匹配的单词
            for (String word : toRemove) {
                pointers.remove(word);
            }
        }
        
        return matchedCount;
    }
}

方法三:优化二分查找

import java.util.*;

class Solution {
    /**
     * 使用Collections.binarySearch优化的二分查找
     */
    public int numMatchingSubseq(String s, String[] words) {
        // 预处理字符位置
        List<Integer>[] positions = new List[26];
        for (int i = 0; i < 26; i++) {
            positions[i] = new ArrayList<>();
        }
        
        for (int i = 0; i < s.length(); i++) {
            positions[s.charAt(i) - 'a'].add(i);
        }
        
        Map<String, Boolean> cache = new HashMap<>();
        int count = 0;
        
        for (String word : words) {
            if (cache.computeIfAbsent(word, w -> isSubsequenceOptimized(w, positions))) {
                count++;
            }
        }
        
        return count;
    }
    
    private boolean isSubsequenceOptimized(String word, List<Integer>[] positions) {
        int prevIndex = -1;
        
        for (char c : word.toCharArray()) {
            List<Integer> list = positions[c - 'a'];
            if (list.isEmpty()) return false;
            
            // 使用Collections.binarySearch找到插入位置
            int pos = Collections.binarySearch(list, prevIndex + 1);
            if (pos < 0) {
                pos = -pos - 1; // 转换为插入位置
            }
            
            if (pos >= list.size()) {
                return false;
            }
            
            prevIndex = list.get(pos);
        }
        
        return true;
    }
}

方法四:暴力

import java.util.*;

class Solution {
    /**
     * 暴力双指针,使用缓存优化
     */
    public int numMatchingSubseq(String s, String[] words) {
        Map<String, Boolean> cache = new HashMap<>();
        int count = 0;
        
        for (String word : words) {
            if (cache.computeIfAbsent(word, w -> isSubsequenceBrute(s, w))) {
                count++;
            }
        }
        
        return count;
    }
    
    private boolean isSubsequenceBrute(String s, String word) {
        int i = 0, j = 0;
        while (i < s.length() && j < word.length()) {
            if (s.charAt(i) == word.charAt(j)) {
                j++;
            }
            i++;
        }
        return j == word.length();
    }
}

算法分析

  • 时间复杂度

    • 预处理 + 二分查找:O(s.length + (word.length × log(s.length)))
    • 多指针:O(s.length × unique_words_count)
    • 暴力(带缓存):O(s.length × unique_words_count)
  • 空间复杂度

    • 预处理 + 二分查找:O(s.length + unique_words_count)
    • 多指针:O(unique_words_count × avg_word_length)
    • 暴力:O(unique_words_count × avg_word_length)

算法过程

1:s = “abcde”, words = [“a”,“bb”,“acd”,“ace”]

预处理

  • positions[‘a’] = [0]
  • positions[‘b’] = [1]
  • positions[‘c’] = [2]
  • positions[‘d’] = [3]
  • positions[‘e’] = [4]

单词检查

  1. “a”

    • 字符’a’:在positions[0]中找> -1的位置 → 找到0
    • 完全匹配
  2. “bb”

    • 第一个’b’:在positions[1]中找> -1的位置 → 找到1
    • 第二个’b’:在positions[1]中找> 1的位置 → 未找到
  3. “acd”

    • ‘a’:找到位置0,prevIndex=0
    • ‘c’:在positions[2]中找> 0的位置 → 找到2,prevIndex=2
    • ‘d’:在positions[3]中找> 2的位置 → 找到3,prevIndex=3
    • 完全匹配
  4. “ace”

    • ‘a’:找到位置0,prevIndex=0
    • ‘c’:找到位置2,prevIndex=2
    • ‘e’:在positions[4]中找> 2的位置 → 找到4,prevIndex=4
    • 完全匹配

结果:3个单词匹配

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准示例
    String[] words1 = {"a","bb","acd","ace"};
    System.out.println("Test 1: " + solution.numMatchingSubseq("abcde", words1)); // 3
    
    // 测试用例2:重复单词
    String[] words2 = {"a","a","a"};
    System.out.println("Test 2: " + solution.numMatchingSubseq("abcde", words2)); // 3
    
    // 测试用例3:空单词
    String[] words3 = {""};
    System.out.println("Test 3: " + solution.numMatchingSubseq("abcde", words3)); // 1
    
    // 测试用例4:无匹配
    String[] words4 = {"bb","cb","bd"};
    System.out.println("Test 4: " + solution.numMatchingSubseq("abcde", words4)); // 0
    
    // 测试用例5:完全匹配
    String[] words5 = {"abcde"};
    System.out.println("Test 5: " + solution.numMatchingSubseq("abcde", words5)); // 1
    
    // 测试用例6:长字符串
    String longS = "abcdefghijklmnopqrstuvwxyz";
    String[] words6 = {"ace","xyz","aeiou","bcdfg"};
    System.out.println("Test 6: " + solution.numMatchingSubseq(longS, words6)); // 4
    
    // 测试用例7:单字符s
    String[] words7 = {"a","b","c"};
    System.out.println("Test 7: " + solution.numMatchingSubseq("a", words7)); // 1
    
    // 测试用例8:大量重复单词
    String[] words8 = new String[5000];
    Arrays.fill(words8, "ace");
    System.out.println("Test 8: " + solution.numMatchingSubseq("abcde", words8)); // 5000
    
    // 测试用例9:边界情况
    String[] words9 = {"a", "z"};
    System.out.println("Test 9: " + solution.numMatchingSubseq("a", words9)); // 1
    
    // 测试用例10:空s
    String[] words10 = {"a", ""};
    System.out.println("Test 10: " + solution.numMatchingSubseq("", words10)); // 1 (只有空字符串匹配)
}

关键点

  1. 缓存

    • words 数组可能包含大量重复单词
    • 缓存可以将时间复杂度从 O(total_words) 降低到 O(unique_words)
  2. 二分查找

    • 预处理每个字符的位置,避免重复遍历 s
    • 对于长 s 和短单词,效率提升
  3. 子序列

    • 不需要连续,必须保持相对顺序
    • 空字符串是任何字符串的子序列
  4. 字符位置

    • 使用 ArrayList 存储每个字符的所有位置
    • 位置天然有序,适合二分查找
  5. 边界情况处理

    • 空字符串、单字符、重复字符等特殊情况
    • 字符在 s 中不存在的情况

常见问题

  1. 为什么需要缓存?

    • words 可能包含重复单词
    • 不缓存会导致重复计算,效率低下
  2. 二分查找?

    • 找第一个大于 prevIndex 的位置
    • 确保字符的相对顺序正确
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值