前缀树(字典树)应用——实现 Trie (前缀树)、添加与搜索单词

2023-11-10

目录

1 前缀树原理简介

2 实现前缀树

2.1 题目描述

2.2 题目分析

2.3 代码实现

3 添加与搜索单词

3.1 题目描述

3.2 题目分析

3.3 代码实现

4 总结


1 前缀树原理简介

      先来简单介绍一下前缀树是什么。前缀树也叫字典树,常用语字符串的查找,为什么又叫前缀树呢?我们先来看看前缀树长什么样子:

                         

        如图所示,我们把"app"、“and”、“bad”以及“ban”放在树中,每个字符串都是从root开始的,然后根据第一个元素的不同分成了"a"开头与"b"开头两组,然后接着往下走,"a"开头的又分开了,而“b”开头的两个字符串还没有分开,再往下走,“b”开头的两个字符串也分开了。从整个树的结构可以看得出来,字符串两两之间的相同前缀部分是重合的,一旦分开就不会再组合了,因此在一组字符串中查找某一个字符串的时候可以避免很多不必要的查找。

        好了,简单介绍了前缀树之后,还有几点也必须考虑:既然前缀树也是树,那么每个结点的数据类型应当是什么呢?首先,根据前缀树的结构可以看出来,每个树的结点都是需要指向子结点的,那么子结点应该有几个呢?子结点的个数我们是无法确定的,就像图中的结点,有的结点有两个子结点,有的结点只有一个子结点,那该怎么办呢?

       实际上也很简单,如果前缀树中只可能出现多少种结点情况那就定义多少个子结点!比如说这里如果前缀树中的都是小写字母的话,那就直接定义26个子结点就好了。有了子结点,那每个结点需不需要再定义一个value来存放到底是'a'还是'b'呢?实际上根本就不需要,因为既然前面定义了26个子结点,那么每个子结点就已经确定好了,无需再定义一个value。现在,有了子结点,查找的时候就可以顺着子结点往下搜索了,那我什么时候才叫搜索到什么时候结束呢?比如说我要查找“and”,那就应该到'd'结束,那我怎么知道结束了呢?将26个结点全部遍历一次全为空?如果这样的话,如果树中还包含数字之类的话显然效率就很低了,因此,我们直接再给每个结点定义一个末尾标记变量,如果这个结点是其所在的这条线的最后一个结点,那就标记为末尾,那又怎么去标记呢?我们下面就用例题来说了。先来看看每个结点的数据类型:

struct TrieNode{
    TrieNode* child[26]; //子结点
    bool endFlag; //末尾标记,是则true,否则false

    TrieNode():endFlag(false)
    {
        for(auto &c:child)c=NULL; //初始化末尾标记为false,每个子结点为NULL
    }
};

2 实现前缀树

2.1 题目描述

实现一个 Trie (前缀树),包含 insertsearch, 和 startsWith 这三个操作。

示例:

Trie trie = new Trie();

trie.insert("apple");
trie.search("apple");   // 返回 true
trie.search("app");     // 返回 false
trie.startsWith("app"); // 返回 true
trie.insert("app");   
trie.search("app");     // 返回 true

说明:

  • 你可以假设所有的输入都是由小写字母 a-z 构成的。
  • 保证所有输入均为非空字符串。

2.2 题目分析

       题目就是要求实现一个前缀树,先来分析一下三个功能:

       ①insert。意思就是像前缀树中插入字符串。我们先来想一想插入的过程,由于前缀树的每个结点代表一个字母,那么其插入的过程肯定是一个一个字母进行插入的。那就对整个字符串进行遍历,如果当前字母所在的子结点为空,说明当前结点的下面还没有串入该字母,那就直接将该字母所在的子结点处开辟一个新的结点;如果当前字符所对应的子结点不为空,那就迭代到该子结点,继续遍历下一个字母……当字符串遍历结束后,那最后一个字母所在的结点自然而然就是末尾结点了,那就将其endFlag置为true;该段代码如下:

void insert(string word) {
        TrieTree *p=root;
        
        for(auto c:word)  //遍历字符串所有字符
        {
            if(p->child[c-'a']==NULL)p->child[c-'a']=new TrieTree(); //如果当前字符所对应的子结点为空,就开辟一个新的结点
            p=p->child[c-'a'];  //迭代至当前字符所对应的子结点
        }
        p->endFlag=true;  //末尾结点置为true
        return ;
    }

     ②search。意思就是查找当前树中是否存在某一字符串。其实根据上面插入的过程,也不难想到查找的方式了,依然是根据字符串每个字符来迭代至相应结点,如果该字符串中某一字符所对应的结点为空,那就说明这个字符串不存在了。如果均不为空,那是不是说明这个字符串就在树中呢?不一定,比如说在前面的树的结构图中查找“an”,实际上这个字符串是没有的,但是它的每个结点又都存在,因此,我们还需要引入另一个判断条件,即是这个字符串的末尾字符对应的结点是否也是末尾结点,如果也是末尾结点,那么就说明字符串存在了,否则就不存在。该段代码如下:

bool search(string word) {
        TrieTree* p=root;
        
        for(auto c:word)
        {
            if(p->child[c-'a']==NULL)return false;  //只要有一个字符对应的结点为空,就证明不存在
            p=p->child[c-'a']; //迭代至该字符对应结点
        }
        return p->endFlag;  //返回末尾字符对应结点是否为末尾结点
        
    }

     ③startsWith。简单的讲,就是判断树中是否有该前缀,比如说在前面的树的结构图中查找“an”,很明显“an”是一个前缀,因此就返回true了。这样说的话,startsWith的实现还比search更简单,不用判断末尾字符对应结点是否为末尾结点了。对应代码如下:

bool startsWith(string prefix) {
        
        TrieTree* p=root;
 
        for(auto c:prefix)
        {
            if(p->child[c-'a']==NULL)return false;
            p=p->child[c-'a'];
        }
        return true;  //如果整个字符串都遍历完了,说明就存在了。
    }

2.3 代码实现

class Trie {
public:
    
    struct TrieTree   //结点结构体
    {
        TrieTree* child[26];
        bool endFlag;
        
        TrieTree():endFlag(false)
        {
            for(auto &a:child)a=NULL;  //TrieTree结构体构造函数,初始化各子结点为NULL
        }
        
    };
    
    TrieTree* root;
    /** Initialize your data structure here. */
    Trie() {
        root=new TrieTree();   //类构造函数,开辟空间给root
    }
    
    /** Inserts a word into the trie. */
    void insert(string word) {
        TrieTree *p=root;
        
        for(auto c:word)
        {
            if(p->child[c-'a']==NULL)p->child[c-'a']=new TrieTree();
            p=p->child[c-'a'];
        }
        p->endFlag=true;
        return ;
    }
    
    /** Returns if the word is in the trie. */
    bool search(string word) {
        TrieTree* p=root;

        for(auto c:word)
        {
            if(p->child[c-'a']==NULL)return false;
            p=p->child[c-'a'];
        }
        return p->endFlag;
    }

    /** Returns if there is any word in the trie that starts with the given prefix. */
    bool startsWith(string prefix) {
        
        TrieTree* p=root;
 
        for(auto c:prefix)
        {
            if(p->child[c-'a']==NULL)return false;
            p=p->child[c-'a'];
        }
        return true;
    }
    
};

/**
 * Your Trie object will be instantiated and called as such:
 * Trie obj = new Trie();
 * obj.insert(word);
 * bool param_2 = obj.search(word);
 * bool param_3 = obj.startsWith(prefix);
 */

添加与搜索单词

3.1 题目描述

设计一个支持以下两种操作的数据结构:

void addWord(word)
bool search(word)

search(word) 可以搜索文字或正则表达式字符串,字符串只包含字母 . 或 a-z 。 . 可以表示任何一个字母。

示例:

addWord("bad")
addWord("dad")
addWord("mad")
search("pad") -> false
search("bad") -> true
search(".ad") -> true
search("b..") -> true

说明:

你可以假设所有单词都是由小写字母 a-z 组成的。

3.2 题目分析

       光从题目需求来说,addWord的功能和前面的insert功能是相同的,而search功能就变复杂了一些,多了一个'.'用于模糊匹配,即是说如果字符串中某个字符是'.',那这个字符是可以表示任何一个字母的,那该怎么做呢?实际上,和前面一道题的search差别也不大,前面一个search遍历字符串时,只迭代到字符串当前字符所对应的子结点,而这里如果遍历到了'.'字符,那就说明26个子结点都是符合要求的,那么就应该遍历26个子结点进行相同的操作了,很显然,这里就需要用到递归了。其他的就不多说了,直接贴出代码:

bool search(string word) {
        
        return search(word,root,0,word.size());   //对search的重载函数开始递归

    }
    
    bool search(string &word,TrieNode* root,int index,int len)
    { 
        if(!root)return false;  //如果当前字符对应子结点为空,说明不存在该字符串
        
        if(index==len)return root->endFlag;
        
        if(word[index]=='.')   //如果当前字符为'.'
        {
            for(auto a:root->child)  //就依次递归26个子结点
            {
                if(search(word,a,index+1,len))return true; //如果其中某一次递归查找命中,那么就直接返回true
            }
            return false; //如果26个子结点都递归完了都没有命中的,说明不存在该字符串
        }

        return search(word,root->child[word[index]-'a'],index+1,len); //如果当前字符不是'.',那么就递归下一个字符
        
    }

3.3 代码实现

class WordDictionary {
    
public:
    
    struct TrieNode
    {
        TrieNode *child[26];

        bool endFlag;
        
        TrieNode():endFlag(false)
        {
            for(auto &c:child)c=NULL;
        }
        
    };
    /** Initialize your data structure here. */
    WordDictionary() {
        root=new TrieNode();
    }
    
    /** Adds a word into the data structure. */
    void addWord(string word) {
        
        TrieNode* p=root;
        
        for(auto c:word)
        {
            if(!p->child[c-'a'])p->child[c-'a']=new TrieNode();
            p=p->child[c-'a'];
        }
        p->endFlag=true;
        return ;
    }
    
    /** Returns if the word is in the data structure. A word could contain the dot character '.' to represent any one letter. */

    bool search(string word) {
        
        return search(word,root,0,word.size());

    }
    
    bool search(string &word,TrieNode* root,int index,int len)
    { 
        if(!root)return false;  //如果当前字符对应子结点为空,说明不存在该字符串
        
        if(index==len)return root->endFlag;
        
        if(word[index]=='.')   //如果当前字符为'.'
        {
            for(auto a:root->child)  //就依次递归26个子结点
            {
                if(search(word,a,index+1,len))return true; //如果其中某一次递归查找命中,那么就直接返回true
            }
            return false; //如果26个子结点都递归完了都没有命中的,说明不存在该字符串
        }

        return search(word,root->child[word[index]-'a'],index+1,len); //如果当前字符不是'.',那么就递归下一个字符
        
    }
    private:
    
    TrieNode* root;
};

/**
 * Your WordDictionary object will be instantiated and called as such:
 * WordDictionary obj = new WordDictionary();
 * obj.addWord(word);
 * bool param_2 = obj.search(word);
 */

4 总结

      根据以上两个例子可以看出,前缀树的方法用来查找字符串是十分方便的,每一个不管是插入还是查找,其时间复杂度均为O(M),其中M为参数字符串的长度。但是前缀树也是有缺点的,虽然从树的结构图上看只有几个结点,实际上每个结点都有26个子结点,只不过隐藏了其他部分结点,这对内存来说无疑是不友好的。因此,前缀树的方法是很典型的用空间换取时间的例子。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

前缀树(字典树)应用——实现 Trie (前缀树)、添加与搜索单词 的相关文章

  • linux源代码.tar.xz解压

    刚开始学习linux内核 在linux内核官网https www kernel org 下载 我下载的版本是 linux 2 6 34 14 tar xz 由于我的linux中没有安装 xz的解压缩软件 需要下载 http download
  • linux dd命令小结

    为什么写本文 最近使用dd命令比较多 它是linux下功能强大的数据复制工具 这篇博文对它的使用做个小结 一来加深记忆 二来方便自己以后查阅 dd命令的功能 dd的主要功能是拷贝文件 默认从标准输入拷贝到标准输出 这意味dd可以在管道中使用

随机推荐

  • SpringBoot集成微信支付JSAPIV3保姆教程

    前言 最近为一个公众号h5商城接入了微信支付功能 查找资料过程中踩了很多坑 以此文章记录一下和大家分享 前期准备 公众号认证 微信支付功能需要开通企业号并进行资质认证 费用一年300 且需企业营业执照等信息 对公账户打款验证 登录微信公众平
  • 《人工智能算法图解》书籍分享(包邮送书)

    文章目录 人工智能介绍 书籍分享 抽奖包邮送书 人工智能介绍 人工智能算法是一种能够模拟人类智能行为的计算机算法 它通过分析和处理大量的数据 利用机器学习 深度学习和自然语言处理等技术 实现自主学习 推理和决策的能力 人工智能算法的发展经历
  • ​外包公司干了不到3个月,我离职了...(防坑指南)

    外包公司干了不到3个月 我离职了 当项目快要做完的时候 我就担心自己是不是要被 释放了 直到外包HR wx找我聊项目 我就不担心了 结果确实是要被 释放 从杭州到深圳 来的也突然 离职也有点突然 也是意料之中 本来想写 年终总结 结果现在要
  • Java五子棋

    提示 本人大二时的java大作业 当时没有学数据库 只是学到界面哪里 所以做出的条件有限 哈哈 看看就好 有帮助了 就拿走 不谢 Java五子棋 前言 主要就是涉及到java界面编程 实现Runable接口重写run方法 实现多线程 来控制
  • SQL中declare申明变量

    原文地址 http blog csdn net yanpingsz article details 5633660 在sql语句中添加变量 declare local variable data type 声明时需要指定变量的类型 可以使用
  • python定时运行,多进程

    可以通过另开一条线程 去专门做这件事情 py2代码如下 如果是py3请自行调整下语法 coding utf8 import threading import time 真正要执行的函数 def t1 print ok 每隔10秒钟执行 de
  • 五大车载操作(VOS)系统优劣对比,车载系统架构分析-QNX系统性能分析

    如果你认为本系列文章对你有所帮助 请大家有钱的捧个钱场 点击此处赞助 赞助额0 1元起步 多少随意 声明 本文只用于个人学习交流 若不慎造成侵权 请及时联系我 立即予以改正 锋影 email 174176320 qq com 导读 车载操作
  • UART、TTL和RS232的区别

    UART TTL和RS232的区别 串行通信 UART TTL RS232 学习硬件的开始接触的就是串口 但是一直没搞懂UART TTL和RS232这些的关系 总感觉相互之间有所交叉 无法完全区分开 于是有了这篇博文 但是 这篇博文自我感觉
  • 分离轴定理(SAT):凸多边形相交检测

    引言 在计算机图形学 游戏开发 碰撞检测等领域 凸多边形相交检测是一个常见而重要的问题 为了快速准确地判断两个凸多边形是否相交 分离轴定理 Separating Axis Theorem 简称 SAT 成为了一种高效而可靠的算法 本文将深入
  • css里各个元素的书写顺序

    1 位置相关 position top left index float display 2 大小相关 width height margin padding 3 文字相关 font line height color letter spa
  • Python 怎么利用Python绘制二元高次隐函数的函数图像及其极值点——以某双核论文模型方程为例

    项目场景 几日前 在研究某双核期刊的某篇论文时 发现论文上的函数图像绘制得似乎有些不精确 原函数方程为 0 2045 y 2 3 4 y 3 2 x y 2 0 45 2 0 论文原文中函数图像如下图 问题描述 可以很明显地看出 极值点附近
  • Gof23设计模式之模板方法模式

    1 定义 定义一个操作中的算法骨架 而将算法的一些步骤延迟到子类中 使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤 2 结构 模板方法 Template Method 模式包含以下主要角色 抽象类 Abstract Clas
  • execjs随心所欲运行抠出来的js代码:报错什么都不是问题 execjs._exceptions.ProgramError: ReferenceError: $ is not defined

    起因 今天扣出一段js想用execjs执行 报错 未定义 也就是说execjs不能执行jquery 决定试试用nodejs来执行 execjs exceptions ProgramError ReferenceError is not de
  • Windows10 安装 Vue3

    一 安装Node js 官网下载Node js https nodejs org en download 下载完成后 双击 msi文件 将默认安装路径按照喜好修改 其余的设置默认即可 不需要勾选安装的附加选项 node v 二 更新Node
  • lattice

    lattice 在实际的语音识别系统中 最优路径不一定与实际字序列匹配 我们一般希望能够得到得分最靠前的多条候选路径 即N best 为了紧凑地保存候选路径 我们一般采用lattice 词图 来保存识别的候选序列 lattice本质上是一个
  • Ubuntu18.04上安装RTX 2080Ti显卡驱动

    文章目录 1 安装Linux系统 1 1下载Linux镜像文件 1 2 制作系统盘 1 3 安装Linux系统 1 4 配置linux系统 2 安装英伟达显卡驱动 2 1 预备工作 2 2 安装显卡驱动 3 安装cuda 4 安装cudnn
  • 代数余子式的几何意义,点积和叉乘的几何意义

    1 点乘的几何意义 a b c d e f ad be cf 结果是一个标量 也可以写为 a b a b cos 以下说明点乘的几何意义 就是一个向量在另一个单位向量 如果另一个向量是单位向量 上的投影长度 a b b a cos a b
  • thinkphp5.1开发app接口版本控制(路由设置)

    使用thinkphp5 1开发app接口进行版本控制 在index controller下创建v1和v2目录 v1下创建版本1的文件 如下图 在route route php中 如下图 v获取版本例如 v1 v2 下面第一个路由其实是 接口
  • 如何为服务网格选择入口网关_如果使用服务网格,是否需要API网关?

    如何为服务网格选择入口网关 这篇文章可能无法突破API网关和Service Mesh周围的噪音 但是 这是2020年 围绕这些主题仍然存在很多困惑 我选择编写此内容是为了帮助带来真正的具体解释 以帮助阐明差异 重叠之处以及何时使用它们 如果
  • 前缀树(字典树)应用——实现 Trie (前缀树)、添加与搜索单词

    目录 1 前缀树原理简介 2 实现前缀树 2 1 题目描述 2 2 题目分析 2 3 代码实现 3 添加与搜索单词 3 1 题目描述 3 2 题目分析 3 3 代码实现 4 总结 1 前缀树原理简介 先来简单介绍一下前缀树是什么 前缀树也叫