力扣刷题-面试题 17.13. 恢复空格、字典树、前缀树的应用

基本概念

  1. Trie 树

    又称单词查找树、前缀树,是一种树形结构。典型应用是用于统计、排序和保存大量的字符串(但不仅限于字符串)。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,比哈希表更快。

  2. 基本性质

    ①.根节点不包含字符,除根节点外每个节点都只包含一个字符

    ②.从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串

    ③.每个节点的所有子节点包含的字符都不相同

  3. 基本操作

    ①.插入:把一个单词插入到字典树

    ②.查询前缀:判断某个单词是否为一个单词的前缀

    ③.查询单词:判断某个单词是否已经存在

基本原理

  1. 字典树的本质

    Trie 树的本质,就是利用字符串之间的公共前缀,将重复的前缀合并在一起。

  2. 构建原理

    Trie 树的插入操作就是将单词的每个字母逐一插入Trie树。插入前先判断字母对应的节点是否存在,存在则移动到下一层继续插入,不存在则创建对应的节点。

实现方法

// TrieNode 节点类,由 a-z 小写字母构成的字典树
class TrieNode
{
private:
    int count;//包含子节点数量,可以用于判断是否叶子节点
    bool isEnd;//标记是否单词结尾
    vector<TrieNode*> children;//存储子节点指针
public:
    // 构造
    TrieNode():count(0),isEnd(false),children(26,NULL) {}
    // 析构
    ~TrieNode()
    {
        for(int i = 0;i < 26;i++ )
        {
            if( children[i] ) delete children[i];
        }
    }
    // 对外系列接口
    int size() { return count ;} // 返回子节点数量
    TrieNode* insertNode(char c) // 插入一个子节点,并返回其指针
    {
        if( c  <  'a' || c > 'z' ) return NULL;
        if( children[c - 'a'] == NULL)
        {
            children[c - 'a'] = new TrieNode();
            count++;
        }
        return children[c - 'a'] ;
    }
    TrieNode* getNode( char c )//返回指定子节点
    {
        if( c  <  'a' || c > 'z' ) return NULL;
        return children[c - 'a'] ;
    }
    bool idWordEnd(){ return isEnd;}//返回是否单词结尾
    void setEnd() { isEnd = true ;}//标记本节点为单词结尾
};
// Trie 类,封装操作接口
class Trie {
private:
    TrieNode * root;//根节点
public:
    // 构造
    Trie() : root( new TrieNode() ){}
    // 析构
    ~Trie()
    {
        delete root;
    }
    // 插入一个单词
    void insert(string word) {
        TrieNode * p = root;
        for(int i = 0;i < word.size();i++ )
        {
            p = p->insertNode(word[i]);
        }
        p->setEnd() ;
    }
    //逆序插入一个单词
    void insertReverse(string word) {
        TrieNode * p = root;
        for(int i = word.size() -1;i >-1;i-- )
        {
            p = p->insertNode(word[i]);
        }
        p->setEnd() ;
    }
    //根据单词返回节点
    TrieNode *getNode(string word)
    {
        TrieNode * p = root;
        for(int i = 0;i < word.size();i++ )
        {
            p = p->getNode(word[i]) ;
            if( p == NULL ) return NULL;
        }
        return p;
    }
    // 判断指定单词是否存在
    bool search(string word) {
        TrieNode * p = getNode(word);
        if( p )
        	return  p->idWordEnd();
        return false;
    }
    //判断指定前缀是否存在
    bool startsWith(string prefix) {
        TrieNode * p = getNode(prefix);
        return p != NULL;
    }
};

字典树应用

你不小心把一个长篇文章中的空格、标点都删掉了,并且大写也弄成了小写。像句子"I reset the computer. It still didn’t boot!“已经变成了"iresetthecomputeritstilldidntboot”。在处理标点符号和大小写之前,你得先把它断成词语。当然了,你有一本厚厚的词典dictionary,不过,有些词没在词典里。假设文章用sentence表示,设计一个算法,把文章断开,要求未识别的字符最少,返回未识别的字符数。

示例:

输入:
dictionary = [“looked”,“just”,“like”,“her”,“brother”]
sentence = “jesslookedjustliketimherbrother”
输出: 7
解释: 断句后为"jess looked just like tim her brother",共7个未识别字符。

来源:力扣(LeetCode)

  1. 题目分析

    ①.动态规划

    定义 dp[i] 表示考虑截止到位置 i 时最少的未识别的字符数量。

    为方便初始化,在字符串开头增加一个不可识别字符 “#”,则dp[0] = 1。

    若存在一个位置 j 把前 i 个字符构成的子串 [0,i] 分为两部分,并且子串 [j,i] 是字典里的单词,如下图所示:

    在这里插入图片描述

    dp[i] 可以转换成 dp[j-1],遍历找到所有的 j ,然后dp[i] 取所有 j 位置的最小值即可,所以状态转移方程为dp[i] = min(dp[i],dp[j-1]);

    若不存在一个位置 j,则 dp[i] = dp[i-1] + 1。

    ②.字典树

    用 j 在范围 [0,i] 遍历所有子串 [j,i] 时,每次都从头到尾截取子串,存在大量的重复判断,可以使用字典树进行优化:
    从 j = i 开始倒叙遍历,若 [j,i] 不是字典是中的前缀,则直接中断循环即可,若 [j,i] 是字典是中的前缀,再判断是否是字典中的单词。

  2. 代码示例

    class TrieNode
    {
    private:
        int count;//包含子节点数量,可以用于判断是否叶子节点
        bool isEnd;//标记是否单词结尾
        vector<TrieNode*> children;//存储子节点指针
    public:
        // 构造
        TrieNode():count(0),isEnd(false),children(26,NULL) {}
        // 析构
        ~TrieNode()
        {
            for(int i = 0;i < 26;i++ )
            {
                if( children[i] ) delete children[i];
            }
        }
        // 对外系列接口
        int size() { return count ;} // 返回子节点数量
        TrieNode* insertNode(char c) // 插入一个子节点,并返回其指针
        {
            if( c  <  'a' || c > 'z' ) return NULL;
            if( children[c - 'a'] == NULL)
            {
                children[c - 'a'] = new TrieNode();
                count++;
            }
            return children[c - 'a'] ;
        }
        TrieNode* getNode( char c )//返回指定子节点
        {
            if( c  <  'a' || c > 'z' ) return NULL;
            return children[c - 'a'] ;
        }
        bool idWordEnd(){ return isEnd;}//返回是否单词结尾
        void setEnd() { isEnd = true ;}//标记本节点为单词结尾
    };
    // Trie 类,封装操作接口
    class Trie {
    private:
        TrieNode * root;//根节点
    public:
        // 构造
        Trie() : root( new TrieNode() ){}
        // 析构
        ~Trie()
        {
            delete root;
        }
        // 插入一个单词
        void insert(string word) {
            TrieNode * p = root;
            for(int i = 0;i < word.size();i++ )
            {
                p = p->insertNode(word[i]);
            }
            p->setEnd() ;
        }
        //逆序插入一个单词
        void insertReverse(string word) {
            TrieNode * p = root;
            for(int i = word.size() -1;i >-1;i-- )
            {
                p = p->insertNode(word[i]);
            }
            p->setEnd() ;
        }
        //根据单词返回节点
        TrieNode *getNode(string word)
        {
            TrieNode * p = root;
            for(int i = 0;i < word.size();i++ )
            {
                p = p->getNode(word[i]) ;
                if( p == NULL ) return NULL;
            }
            return p;
        }
        // 判断指定单词是否存在
        bool search(string word) {
            TrieNode * p = getNode(word);
            if( p )
            	return  p->idWordEnd();
            return false;
        }
        //判断指定前缀是否存在
        bool startsWith(string prefix) {
            TrieNode * p = getNode(prefix);
            return p != NULL;
        }
        //
    };
    class Solution {
    public:
        int respace(vector<string>& dictionary, string sentence) {
            Trie * trie = new Trie();
            for(int i = 0;i < dictionary.size();i++ )
            {
                string word = dictionary[i];
                trie->insertReverse(word);
            }
            sentence = '#'+sentence;
            vector<int> dp(sentence.size(),0);
            dp[0] = 1;
            for( int i = 1;i < sentence.size();i++)
            {
                dp[i] = dp[i-1]+1;
                string temp = "";
                for(int j = i;j > -1;j--)
                {
                    temp += sentence[j] ;
                    TrieNode * p = trie->getNode(temp);
                    if( p  ) //是后缀
                    {
                        if( p->idWordEnd() )
                            dp[i] = min(dp[i],dp[j-1]);
                    }
                    else
                    {
                        break;
                    }
                }
            }
            return dp[sentence.size()-1] -1 ;
        }
    };
    

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值