【算法专题训练】35、前缀树查找

1、LCR 063. 单词替换

题目信息:

cpp 复制代码
在英语中,有一个叫做 词根(root) 的概念,它可以跟着其他一些词组成另一个较长的单词------我们称这个词为 继承词(successor)。
例如,词根an,跟随着单词 other(其他),可以形成新的单词 another(另一个)。
现在,给定一个由许多词根组成的词典和一个句子,需要将句子中的所有继承词用词根替换掉。如果继承词有许多可以形成它的词根,则用最短的词根替换它。
需要输出替换之后的句子。

示例 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"

示例 3:
输入:dictionary = ["a", "aa", "aaa", "aaaa"], sentence = "a aa a aaaa aaa aaa aaa aaaaaa bbb baba ababa"
输出:"a a a a a a a a bbb baba a"

示例 4:
输入:dictionary = ["catt","cat","bat","rat"], sentence = "the cattle was rattled by the battery"
输出:"the cat was rat by the bat"

示例 5:
输入:dictionary = ["ac","ab"], sentence = "it is abnormal that this solution is accepted"
输出:"it is ab that this solution is ac"

解题思路:

  • 1、审题:
  • 输入一组字符串数组,和单独的一句话(也是字符串),现在要求在一字符串数组中的子树作为词根,当句子中的单个字符串中存在前缀和词根相同的情况,则该字符串使用词根进行替换,并返回替换后的字符串
  • 2、解题:
  • 涉及到字符串前缀搜索,使用前缀树进行解决
  • 先根据字符串词根数组,构建前缀树,TrieNode为前缀树节点
  • 再遍历句子,根据空格将句子拆分为单个的字符串数组并进行遍历,遍历过程中获取到的字符串,在前缀树中进行搜索,判断是否存在前缀词根
    • 存在的话返回词根,否则返回空字符串
  • 最后将替换后的字符串数组重新组合成字符串,使用空格间隔

代码实现:

cpp 复制代码
/**
 * 在前缀树中查找单个字符串是否存在对应前缀
 * - 遍历字符串中字符,判断该字符对应的结点是否有空,如果为空,说明没有,直接返回空
 * - 如果存在该结点,则继续遍历,并判断该结点是否是单词结尾,是的话返回当前前缀
 * - 使用一个字符串保存遍历过的前缀
 */
string findPrex(TrieNode &root, string &word)
{
  std::string res = "";
  TrieNode *node = &root;
  for (auto ch : word)
  {
    int index = ch - 'a';
    if (node->isWord)
    {
      return res;
    }
    if (node->children[index] == nullptr)
    {
      break;
    }
    else
    {
      node = node->children[index];
      res += ch;
    }
  }
  return node->isWord ? res : "";
}

/**
 * 构造前缀树,遍历字符串数组,再遍历单个字符串中的字符
 * 根据单个字符判断前缀树节点是否存在,不存在则新创建前缀树节点,并重新赋值node
 * 最后设置节点的isWord标记为true
 */
TrieNode *buildTrie(vector<string> &dictionary)
{
  TrieNode *root = new TrieNode();
  TrieNode *node = nullptr;
  for (auto &word : dictionary)
  {
    node = root;
    for (auto &ch : word)
    {
      int index = ch - 'a';
      if (node->children[index] == nullptr)
      {
        node->children[index] = new TrieNode();
      }
      node = node->children[index];
    }
    node->isWord = true;
  }
  return root;
}

/**
 * 根据空格,将字符串拆分成字符串数组
 */
std::vector<std::string> splitBySpace(const std::string &sentence)
{
  std::vector<std::string> words;
  std::istringstream iss(sentence);
  std::string word;

  while (iss >> word)
  {
    words.push_back(word);
  }
  return words;
}

std::string joinWords(const std::vector<std::string> &words)
{
  std::ostringstream oss;
  // 遍历所有单词
  for (size_t i = 0; i < words.size(); ++i)
  {
    if (i > 0)
    {
      oss << " "; // 在单词间添加空格 -- 在单词前面加空格,除了第一个单词
    }
    oss << words[i];
  }

  return oss.str();
}

string replaceWords(vector<string> &dictionary, string sentence)
{
  TrieNode *root = buildTrie(dictionary);
  // 分割句子成字符串数组
  std::vector<std::string> words = splitBySpace(sentence);
  // 遍历字符串数组
  for (int i = 0; i < words.size(); i++)
  {
    std::string word = words[i];
    // 根据单个字符串,查找词根
    std::string prexStr = findPrex(*root, word);
    if (!prexStr.empty())
    {
      words[i] = prexStr;
    }
  }
  return joinWords(words);
}

2、LCR 064. 实现一个魔法字典

题目信息:

cpp 复制代码
设计一个使用单词列表进行初始化的数据结构,单词列表中的单词 互不相同 。
如果给出一个单词,请判定能否只将这个单词中一个字母换成另一个字母,使得所形成的新单词存在于已构建的神奇字典中。

实现 MagicDictionary 类:
MagicDictionary() 初始化对象
void buildDict(String[] dictionary) 使用字符串数组 dictionary 设定该数据结构,dictionary 中的字符串互不相同
bool search(String searchWord) 给定一个字符串 searchWord ,判定能否只将字符串中 一个 字母换成另一个字母,
使得所形成的新字符串能够与字典中的任一字符串匹配。如果可以,返回 true ;否则,返回 false 。

示例:
输入
inputs = ["MagicDictionary", "buildDict", "search", "search", "search", "search"]
inputs = [[], [["hello", "leetcode"]], ["hello"], ["hhllo"], ["hell"], ["leetcoded"]]
输出
[null, null, false, true, false, false]

解释
MagicDictionary magicDictionary = new MagicDictionary();
magicDictionary.buildDict(["hello", "leetcode"]);
magicDictionary.search("hello"); // 返回 False
magicDictionary.search("hhllo"); // 将第二个 'h' 替换为 'e' 可以匹配 "hello" ,所以返回 True
magicDictionary.search("hell"); // 返回 False
magicDictionary.search("leetcoded"); // 返回 False

解题思路1:

  • 1、审题:
  • 构建一个类,首先输入一个字符串数组作为基础数据,然后调用search查询方法,根据输入的查询参数,
  • 判断是否存在值修改参数字符串中的一个字母就可以匹配到基础数据中的一个字符串
  • 2、解题:
  • 解法一:遍历解法
  • 使用数组保存字符串基础数据,在查询方法search中,使用两层for循环,
  • 第一层for循环为从基础数据数组中,遍历到一个字符串,然后与查询字符串进行内部字符的比较
  • 内层for循环则为字符的比较,使用变量diff标记当前两个字符串存在不同的字符个数,如果超过1个以上则直接返回false
    • 如果遍历到结束,并且diff只有1个不相同的字符,则命中选择返回true

代码实现1:

cpp 复制代码
class MagicDictionary
{
public:
    MagicDictionary()
    {
    }

    void buildDict(vector<string> dictionary)
    {
        m_words = dictionary;
    }

    bool search(string searchWord)
    {
        int diff;
        for (auto &word : m_words)
        {
            diff = 0;
            if (word.length() != searchWord.length()) // 先判断字符串长度
            {
                continue; // 当前单词长度不一致,则继续遍历后面的字符串
            }

            // 比较word,和searchWord相同位置的字符是否相同,不相同diff变量递增
            for (int i = 0; i < word.length(); i++)
            {
                if (word[i] != searchWord[i])
                {
                    diff++;
                    if (diff > 1)
                    {
                        break; // 当前两个字符串中字符不相同的个数超过1个,不再继续进行判断
                    }
                }
            }

            // 遍历结束后,判断不相同的字符是否只有一个
            if (diff == 1)
            {
                return true;
            }
        }
        return false;
    }
    std::vector<std::string> m_words;
};

解题思路2:

  • 解法二:递归实现
  • 使用前缀树进行构建
  • 然后在查询方法时,使用dfs进行深度优先遍历,遍历过程中分情况:
    • 1、遍历到单词结束,节点也结束,并且中间有变更一次,则命中了
    • 2、遍历到单词和节点值不一样,则变更一次,每个字符有26个字母选择项,要寻找到有节点的下一个节点值
    • 3、直到遍历到空节点或者字符串和节点为单词结尾的位置

代码实现2:

cpp 复制代码
class MagicDictionary
{
public:
    MagicDictionary() : root(new TrieNode())
    {
    }
    ~MagicDictionary()
    {
        delete root;
        root = nullptr;
    }

    /**
     * 构建前缀树
     * - 双层for循环,外层for循环遍历字符串数组,内层for循环遍历每个字符串中的字符
     * - 并且在字符的位置新建结点
     */
    void buildDict(vector<string> dictionary)
    {
        TrieNode *node = nullptr;
        for (auto word : dictionary)
        {
            node = root;
            for (int i = 0; i < word.length(); i++)
            {
                int index = word[i] - 'a';
                // 判断当前遍历到字符的结点是否存在,不存在则新建
                if (node->children[index] == nullptr)
                {
                    node->children[index] = new TrieNode();
                }
                // 当前结点赋值,一直往前缀树下一层行走
                node = node->children[index];
            }
            node->isWord = true;
        }
    }

    bool dfs(TrieNode *node, const string &searchWord, int pos, bool isChange)
    {
        // 当前结点为空,遍历到空节点了
        if (node == nullptr || pos > searchWord.length())
        {
            return false;
        }

        // 查询位置i遍历到字符串末尾了,并且遍历到的结点为单词结尾标记
        if (pos == searchWord.length() && node->isWord && isChange)
        {
            return true;
        }

        // 遍历当前结点的子节点,从26个子节点中查找,只要其中有一个满足条件的就返回true
        // 碰到相同的字符,则直接传入isChange,如果不同则isChange需要判断和变更处理
        for (int i = 0; i < 26; i++)
        {
            // 判断26个子节点中是否为空
            bool newChange = false;
            if (searchWord[pos] == (i + 'a')) // 字符相同,则直接使用isChange
            {
                newChange = isChange;
            }
            else
            {
                // 如果不同,则判断isChange是否变更过,变更过又遇到不同的,直接过滤了
                if (isChange)
                {
                    continue;
                }
                else
                {
                    newChange = true;
                }
            }

            if (dfs(node->children[i], searchWord, pos + 1, newChange))
            {
                return true;
            }
        }

        return false;
    }

    /**
     * 字符串查询:
     * - 从根节点开始遍历,并记录当前遍历到的字符位置,和字符串中单个字符是由有变更的标记
     */
    bool search(string searchWord)
    {
        return dfs(root, searchWord, 0, false);
    }

    TrieNode *root;
};

3、总结

  • 前缀树构建,每个节点包含子节点个数为26,根据字符在字母表中的位置,查找对应的子节点在数组中的位置
  • 前缀树查找,也是同样的思路,根据字符位置查找对应结点位置,并不断赋值,直到最后结点的单词标记位为true。
相关推荐
LYFlied2 小时前
【每日算法】LeetCode 62. 不同路径(多维动态规划)
前端·数据结构·算法·leetcode·动态规划
车企求职辅导2 小时前
新能源汽车零部件全品类汇总
人工智能·算法·车载系统·自动驾驶·汽车·智能驾驶·智能座舱
HUST2 小时前
C 语言 第九讲:函数递归
c语言·开发语言·数据结构·算法·c#
yaoh.wang2 小时前
力扣(LeetCode) 119: 杨辉三角 II - 解法思路
数据结构·python·算法·leetcode·面试·职场和发展·跳槽
CoderCodingNo2 小时前
【GESP】C++五级真题(埃氏筛思想考点) luogu-B3929 [GESP202312 五级] 小杨的幸运数
数据结构·c++·算法
机器学习之心2 小时前
基于PSO-GA混合算法的施工进度计划多目标优化,以最小化总成本并实现资源均衡,满足工期约束和资源限制,MATLAB代码
算法·matlab·多目标优化·pso-ga混合算法
bbq粉刷匠2 小时前
Java--二叉树概念及其基础应用
java·数据结构·算法
CodeByV2 小时前
【算法题】前缀和
算法
高洁013 小时前
知识图谱构建
人工智能·深度学习·算法·机器学习·知识图谱