648. 单词替换

题目描述

在英语中,我们有一个叫做 词根 (root) 的概念,可以词根 后面 添加其他一些词组成另一个较长的单词------我们称这个词为 衍生词 (derivative )。例如,词根 help,跟随着 继承"ful",可以形成新的单词 "helpful"

现在,给定一个由许多 词根 组成的词典 dictionary 和一个用空格分隔单词形成的句子 sentence。你需要将句子中的所有 衍生词词根 替换掉。如果 衍生词 有许多可以形成它的 词根 ,则用 最短词根 替换它。

你需要输出替换之后的句子。

示例 1:

复制代码
输入:dictionary = ["cat","bat","rat"], sentence = "the cattle was rattled by the battery"
输出:"the cat was rat by the bat"

示例 2:

复制代码
输入:dictionary = ["a","b","c"], sentence = "aadsfasf absbs bbab cadsfafs"
输出:"a a b c"

题解:

cpp 复制代码
class Mycompare {
public:
	bool operator()(string s1,string s2) {
		return s1.size()<s2.size();
	}
};
class Solution {
public:
    string replaceWords(vector<string>& dictionary, string sentence) {
        vector<string> strs;
        int start = 0;
        int end=0;
        for(;end<sentence.size();end++){
            if(sentence[end]==' '){
                string sub = sentence.substr(start,end-start);
                start = end+1;
                strs.push_back(sub);
            }
        }
        string sub = sentence.substr(start,end-start);   
        strs.push_back(sub);
        sort(dictionary.begin(),dictionary.end(),Mycompare());
        string res ="";
        for(int i=0;i<strs.size();i++){
            bool flag = false;
            for(int j=0;j<dictionary.size();j++){
                if(strs[i].find(dictionary[j])==0){
                    res +=dictionary[j];
                    flag = true;
                    res+=" ";
                    break;
                }
            }
            if(!flag){
                res+=strs[i];
                res+=" ";
            }
        }
        return res.substr(0,res.size()-1);
    }
};

超出时间限制

注意

  • std::string::find(const string& substr) 返回的是 size_t 类型 (即 std::string::size_type),表示子串首次出现的索引位置
  • 如果没找到,返回的是 std::string::npos (一个特殊的 size_t 值,通常是 -1 的无符号形式)

当前的代码时间复杂度是:

  • 分割句子:O(L),L 是句子长度
  • 排序词典:O(M log M),M 是词典大小
  • 匹配过程:O(N × M × K) ,其中:
    • N = 单词数
    • M = 词典大小
    • K = 平均前缀比较长度(find 的开销)

⚠️ 瓶颈在双重循环 + string::find ------ 即使你按长度排序并 break,最坏情况下仍要遍历整个词典。

最优解法:使用 Trie(前缀树)

Trie 可以将匹配过程从 O(M·K) 降到 O(K) 每个单词,总时间复杂度降至 O(L + total_chars_in_dictionary)

而且 天然保证"最短前缀优先" ------ 一旦在 Trie 中遇到一个标记为词根的节点,立即返回,无需排序!

cpp 复制代码
class Solution {
    // 定义 Trie 节点结构
    struct Trie {
        string word;                // 如果该节点是一个词根的结尾,则存储该词根;否则为空
        Trie* children[26] = {};    // 指向子节点的指针数组,对应 'a' 到 'z'
    };

public:
    string replaceWords(vector<string>& dict, string sentence) {
        // 创建 Trie 根节点
        Trie* root = new Trie();

        // 1️⃣ 将词典中的所有词根插入 Trie
        for (auto& w : dict) {
            Trie* cur = root;       // 从根节点开始插入
            for (char c : w) {
                int idx = c - 'a';  // 将字符转换为 0~25 的索引
                // 如果当前字符对应的子节点不存在,则创建新节点
                if (!cur->children[idx]) {
                    cur->children[idx] = new Trie();
                }
                // 移动到子节点
                cur = cur->children[idx];
            }
            // 只有当该节点尚未存储词根时,才保存当前词(避免长词覆盖短词)
            // 注意:由于我们不预先对 dict 排序,这里"先插入的优先";
            // 但 LeetCode 测试用例中,即使后插入更短的词,也不会覆盖,
            // 所以更安全的做法是先对 dict 按长度排序,或在查询时保证最短匹配。
            // 不过本题中,只要在查询时"首次命中就返回",就能保证最短前缀。
            if (cur->word.empty()) {
                cur->word = w;
            }
        }

        // 2️⃣ 分割句子并逐个处理单词
        string res;                 // 最终结果字符串
        string word;                // 临时存储每个单词
        istringstream iss(sentence); // 使用 stringstream 按空格分割句子

        while (iss >> word) {       // 依次读取每个单词
            Trie* cur = root;       // 从 Trie 根节点开始查找
            for (char c : word) {
                // 如果当前路径已断(节点为空 或 子节点不存在),跳出
                if (!cur || !cur->children[c - 'a']) {
                    break;
                }
                // 进入下一个字符对应的子节点
                cur = cur->children[c - 'a'];

                // ✅ 关键:一旦发现当前路径构成一个词根(word 非空),立即替换!
                // 因为我们是从前往后遍历单词,第一个匹配的词根一定是最短的
                if (!cur->word.empty()) {
                    word = cur->word; // 替换为词根
                    break;            // 停止继续匹配更长的前缀
                }
            }
            // 将处理后的单词加入结果(注意处理空格)
            if (!res.empty()) {
                res += " ";
            }
            res += word;
        }

        return res;
    }
};

补充

使用 stringstream(适合空格、制表符等空白字符分割)

cpp 复制代码
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

int main() {
    std::string input = "apple\tbanana  cherry\n  date   \t\telderberry";

    std::stringstream ss(input);
    std::string word;
    std::vector<std::string> tokens;

    // 使用 >> 操作符从 stringstream 中逐个读取非空白 token
    while (ss >> word) {
        tokens.push_back(word);
    }

    // 输出结果
    for (const auto& w : tokens) {
        std::cout << "'" << w << "'\n";
    }

    return 0;
}
cpp 复制代码
'apple'
'banana'
'cherry'
'date'
'elderberry'
  • std::stringstreamoperator>> 默认以任意空白字符(包括空格、\t\n\r\f\v)作为分隔符。
  • 它会自动跳过多余的空白(包括开头、结尾和中间连续的空白),非常适合解析由空白分隔的"单词"或"字段"。
相关推荐
Hello:CodeWorld22 分钟前
C 风格变参 vs C++ 变参模板:核心区别与选型指南
c语言·c++·算法
8Qi82 小时前
LeetCode 516:最长回文子序列
算法·leetcode·职场和发展·动态规划
youngerwang3 小时前
【从搬运工到协处理器:网卡芯片架构、算法、验证与边缘演进深度剖析】
网络·算法·架构·芯片
KaMeidebaby3 小时前
卡梅德生物技术快报|纯化重组蛋白实操详解
人工智能·python·tcp/ip·算法·机器学习
手写码匠4 小时前
从零实现 Prompt 工程引擎:结构化提示、自动优化与多轮自省体系
人工智能·深度学习·算法·aigc
无限码力4 小时前
阿里算法岗 0530笔试真题 - 多约束条件下的元素匹配统计
算法·阿里笔试真题·阿里机试真题·阿里算法岗笔试
lqqjuly4 小时前
MLA — 多头潜在注意力深度解析
深度学习·神经网络·算法
吴可可1235 小时前
SolidWorks草图转三维DWG技巧
算法
redaijufeng5 小时前
C++雾中风景7:闭包
c++·算法·风景
小欣加油6 小时前
leetcode287寻找重复数
数据结构·c++·算法·leetcode