【算法与数据结构】字典树(Trie)详解

news2025/2/22 1:34:12

目录

一,字典树的定义

二,字典树的代码实现

完整代码+详细注释:

测试用例+测试结果:

三,处理其他字符 

四,内存优化与扩展

1. 内存优化

2. 扩展功能

五,扩展功能支持通配符匹配

六,相关题目

1,实现前缀树

2,添加与搜索单词 - 数据结构设计


 

一,字典树的定义

字典树,也叫前缀树或Trie树,是一种树形结构,是一种用于快速检索字符串的数据结构,每个节点代表一个字符,从根节点到某一节点的路径组成一个字符串。主要思想是利用字符串的公共前缀来节省存储空间。

【示例】:

给你n个单词,进行q次查询,每次询问一个单词,让你判断每次查询的字符串是否存在?

我们能想到的是暴力求解,遍历n个字符串,判断是否存在。

对于该问题,查找字符串,就像我们平时查字典的操作。首先确定它的第一个字母,再确定它的第二个字母......,最后就可以找到想要查找的单词。或者这个字符不存在于字典中。

下图是用字典树来存储字符串"abc","abb","bca"。

 上图中,红色节点表示:有一个以此节点为终点的单词。

比如在查找字符串"abb"时,每个字符都可以在树中查到,并且最后一个字符b节点为红色,说明该字符串存在与该字典树中。查找字符串"bc"时,每个租房有都可以在树种查到,但最后一个字符c节点不为红色,说明该字符串不存在于该字典树中。

从上面的示例中可以看出,对于字典树的每次查询,时间复杂度都是log级别的,比暴力求解更优。

二,字典树的代码实现

字典树的结构,通常每个节点包含一个指向子节点的指针数组,这里我们实现一个处理小写字母的字典树,每个字符对应的下标为ch-'a',所以数组的大小为26。另外,还需一个标记位来标记是否是单词的结尾。而当前字符保存在哪呢?其实当前节点的字符可以不用保存,因为我们知道下标,加上‘a',就可以拿到该字符了。

我们主要需要实现三种操作:包含插入、搜索和前缀匹配功能,这里我们用两个函数来实现,然后我们用一个二维数组来实现字典树的建树。

完整代码+详细注释:

#include <iostream>
#include <vector>
using namespace std;

//定义节点
struct TrieNode
{
	vector<TrieNode*> children;//存储字节点的指针数组
	bool isEnd;  //标记位
	TrieNode()
		:children(26, nullptr)
		, isEnd(false)
	{}
};

class Trie
{
public:
	Trie()
	{
		root = new TrieNode();
	}
	~Trie()
	{
		clear(root);
	}
	//插入单词
	void insert(const string& word)
	{
		TrieNode* node = root;
		for (char ch : word)
		{
			//计算索引
			int index = ch - 'a';  

			//当前节点为空,当前节点没有存储ch该字母,new一段空间出来
			if (node->children[index] == nullptr)
				node->children[index] = new TrieNode();

			//当前节点不为空,也就是当前节点已经存储ch该字母了,在ch的下面节点继续插入下一个字母
			node = node->children[index]; 
		}
		node->isEnd = true;
	}
	//搜索单词
	bool search(const string& word)
	{
		TrieNode* node = root;
		for (auto ch : word)
		{
			//计算索引
			int index = ch - 'a';

			//当前索引下为空,该字母不存在,则该字符串不存在
			if (node->children[index] == nullptr)
				return false;

			//当前索引下不为空,该字母存在,继续匹配下一个字母
			node = node->children[index];
		}
		//检查最后一个字母是否是结尾
		return node->isEnd;  
	}
	//前缀匹配
	bool startsWith(const string& prefix)
	{
		TrieNode* node = root;
		for (auto& ch : prefix)
		{
			int index = ch - 'a';
			if (node->children[index] == nullptr)
				return false;
			node = node->children[index];
		}
		return true; //不检查结尾状态
	}
private:
	//递归释放(析构辅助函数)
	void clear(TrieNode* node)
	{
		if (node == nullptr) return;
		for (int i = 0; i < 26; i++)
		{
			if (node->children[i])
				clear(node->children[i]);
		}
		delete node;
	}
	TrieNode* root; //根节点
};

测试用例+测试结果:


int main() {
	Trie trie;

	// 插入单词
	trie.insert("apple");
	trie.insert("app");
	trie.insert("banana");

	// 搜索测试
	cout << boolalpha;
	cout << "Search 'apple': " << trie.search("apple") << endl;    // true
	cout << "Search 'app': " << trie.search("app") << endl;        // true
	cout << "Search 'appl': " << trie.search("appl") << endl;      // false

	// 前缀匹配测试
	cout << "Prefix 'app': " << trie.startsWith("app") << endl;    // true
	cout << "Prefix 'ban': " << trie.startsWith("ban") << endl;    // true
	cout << "Prefix 'cat': " << trie.startsWith("cat") << endl;    // false

	return 0;
}

三,处理其他字符 

我们实现的Trie现在只适用于小写字母。

若要支持更多字符(如大写字母,数字,中文等),需要调整字符索引方式:

// 示例:扩展支持大写字母和数字(共62种字符)
int getIndex(char c) {
    if (c >= 'a' && c <= 'z') return c - 'a';          // 0-25
    else if (c >= 'A' && c <= 'Z') return 26 + c - 'A'; // 26-51
    else if (c >= '0' && c <= '9') return 52 + c - '0'; // 52-61
    else throw invalid_argument("Invalid character");
}

四,内存优化与扩展

1. 内存优化

  • 压缩字典树(Radix Tree):合并只有一个子节点的路径,减少节点数量。

  • 动态分配子节点:使用unordered_mapvector替代固定数组,节省空间(适用于稀疏字符集)。

2. 扩展功能

  • 词频统计:在节点中添加count字段,统计单词出现次数。

  • 前缀自动补全:遍历前缀后的所有分支,收集完整单词。

  • 模糊搜索:支持通配符(如app*e)或编辑距离匹配。

总结:

字典树通过共享前缀高效存储字符串集合,适合前缀匹配快速检索场景 。

五,扩展功能支持通配符匹配

对于通配符的情况,通常的处理方法是在搜索时遇到通配符时遍历所有可能的子节点。例如,处理"ap?e"时,在遍历到通配符的位置,需要检查所有子节点,继续匹配剩下的模式。这种方法可能需要递归或队列来管理多个可能的路径。

 如下图所示,当遍历到通配符?的位置时,需要遍历所有子节点l和p,然后继续匹配。直到匹配到e结束。

前面我们是通过vector来模拟字典树,这里用哈希表来实现。

那对于一个节点该存什么?一个标记位与前面实现的用途一样,一个哈希表来存字符和子节点的映射关系。通过该字符,可以找到它的所有字节点。

通配符?可以匹配任意单个字符,通配符*可以匹配任意长度字符(包括空)。

代码+注释:

bool wildcardSearch(TrieNode* node, const string& pattern, int pos)
{
	//访问到最后一个位置,直接返回
	if (pos == pattern.size())
		return node->isEnd;

	char c = pattern[pos];
	if (c == '?') //匹配任意单个字符
	{
		for (auto& pair : node->children)
		{
			//尝试匹配所有可能的节点
			if (wildcardSearch(pair.second, pattern, pos + 1));
			return true;
		}
		//走到这,说明*匹配成任何节点都找不到该字符串
		return false;
	}
	else if (c == '*') //匹配任意长度字符 包括空
	{
		return wildcardSearch(node, pattern, pos + 1) //跳过*
			|| wildcardSearch(node, pattern, pos);  //继续匹配当前字符
	}
	else //其他正常情况
	{
		if (node->children.count(c) == 0)
			return false;
		return wildcardSearch(node->children[c], pattern, pos+1);
	}
}

完整代码:

#include <iostream>
#include <unordered_map>
#include <string>
using namespace std;

struct TrieNode
{
	bool isEnd=false;//结尾标志
	unordered_map<char, TrieNode*> children; //存储子节点
};

class FuzzyTrie
{
public:
	FuzzyTrie()
	{
		root = new TrieNode();
	}
	~FuzzyTrie()
	{
		clear(root);
	}
	void insert(const string& word)
	{
		//插入逻辑与Trie一样
		TrieNode* node = root;
		for (auto ch : word)
		{
			if (node->children[ch] == nullptr)
				node->children[ch] = new TrieNode();
			node = node->children[ch];
		}
		node->isEnd = true;
	}
	//通配符搜索接口
	bool wildcardMatch(const string& pattern)
	{
		return wildcardSearch(root, pattern, 0);
	}
private:
	//通配符匹配辅助函数
	bool wildcardSearch(TrieNode* node, const string& pattern, int pos)
	{
		//访问到最后一个位置,直接返回
		if (pos == pattern.size())
			return node->isEnd;

		char c = pattern[pos];
		if (c == '?') //匹配任意单个字符
		{
			for (auto& pair : node->children)
			{
				//尝试匹配所有可能的节点
				if (wildcardSearch(pair.second, pattern, pos + 1));
				return true;
			}
			//走到这,说明*匹配成任何节点都找不到该字符串
			return false;
		}
		else if (c == '*') //匹配任意长度字符 包括空
		{
			return wildcardSearch(node, pattern, pos + 1) //跳过*
				|| wildcardSearch(node, pattern, pos);  //继续匹配当前字符
		}
		else //其他正常情况
		{
			if (node->children.count(c) == 0)
				return false;
			return wildcardSearch(node->children[c], pattern, pos+1);
		}
	}
	void clear(TrieNode* node)
	{
		if (node == nullptr)
			return;
		for (auto& pair : node->children)
		{
			clear(pair.second);
		}
		delete node;
	}
	TrieNode* root;
};

 

六,相关题目

1,实现前缀树

题目链接:208. 实现 Trie (前缀树) - 力扣(LeetCode)

class Trie {
public:
    Trie() :children(26),isend(false){}
    
    void insert(string word) {
        Trie* node=this;
        for(char ch:word)
        {
            ch-='a';
            if(node->children[ch]==nullptr)
            node->children[ch]=new Trie();
            node=node->children[ch];
        }
        node->isend=true;
    }
    Trie* searchPrefix(string prefix)
    {
        Trie* node=this;
        for(char ch:prefix)
        {
            ch-='a';
            if(node->children[ch]==nullptr)
            return nullptr;
            node=node->children[ch];
        }
        return node;
    }
    
    bool search(string word) {
        Trie* node=this->searchPrefix(word);
        return node!=nullptr&&node->isend;
    }
    
    bool startsWith(string prefix) {
        return searchPrefix(prefix)!=nullptr;
    }
    private:
    vector<Trie*> children;
    bool isend;
};

/**
 * 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);
 */

2,添加与搜索单词 - 数据结构设计

题目链接:211. 添加与搜索单词 - 数据结构设计 - 力扣(LeetCode)

struct TrieNode
{
    vector<TrieNode*> children;
    bool isend;
    TrieNode():children(26,nullptr),isend(false){}
};
void insert(const string& s,TrieNode* root)
{
    TrieNode* node=root;
    for(char ch:s)
    {
        ch-='a';
        if(node->children[ch]==nullptr)
        node->children[ch]=new TrieNode();
        node=node->children[ch];
    }
    node->isend=true;
}
class WordDictionary {
public:
    WordDictionary() {
        trie=new TrieNode();
    }
    
    void addWord(string word) {
        insert(word,trie);
    }
    
    bool search(string word) {
        //遇到.匹配任意字符
        return dfs(word,0,trie);
    }
    bool dfs(string& word,int index,TrieNode* node)
    {
        if(index==word.size())
        return node->isend;

        char ch=word[index];
        if(ch>='a'&&ch<='z')
        {
            TrieNode* children=node->children[ch-'a'];
            if(children!=nullptr&&dfs(word,index+1,children))
            return true;
        }
        else if(ch=='.')
        {
            for(int i=0;i<26;i++)
            {
                TrieNode* child=node->children[i];
                if(child!=nullptr&&dfs(word,index+1,child))
                return true;
            }
        }

        return false;
    }
    private:
    TrieNode* trie;
};

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2300762.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JavaEE进阶】MyBatis通过注解实现增删改查

目录 &#x1f343;前言 &#x1f340;打印日志 &#x1f334;传递参数 &#x1f38b;增(Insert) &#x1f6a9;返回主键 &#x1f384;删(Delete) &#x1f332;改(Update) &#x1f333;查(Select) &#x1f6a9;起别名 &#x1f6a9;结果映射 &#x1f6a9;开启驼…

Simulink Ststeflow教程 — 2 创建和编辑状态

目录 2.1 创建和编辑状态 2.1.1 状态的创建 2.1.2 创建连接节点 2.1.3 转移 2.1.5 默认转移 在Stateflow模型中&#xff0c;将包含有状态的Stateflow框图称状态图而将不包含任何状态的Stateflow框图称为流程图。其中&#xff0c;状态图是Stateflow最常用的一种形式&#x…

Fiddler笔记

文章目录 一、与F12对比二、核心作用三、原理四、配置1.Rules:2.配置证书抓取https包3.设置过滤器4、抓取App包 五、模拟弱网测试六、调试1.线上调试2.断点调试 七、理论1.四要素2.如何定位前后端bug 注 一、与F12对比 相同点&#xff1a; 都可以对http和https请求进行抓包分析…

线上就医全流程医药机构接入文档接口代码-医保就医接口php-demo版本

2025年2月18日11:28:03 国密算法开发库推荐 lpilp/guomi 我测试过php 7.2 - 8.0都可以兼容&#xff0c;如果有能力可以自己开发 目前已经开发了核心的接口的测试demo,并且封装了工具类直接写业务逻辑即可&#xff0c;并且已经有线上项目在使用&#xff0c;如果需要demo代码可…

智能选路+NAT实验 作业

拓扑图 配置ip 防火墙安全区域划分 用户配置 dns透明

LLM:RAG

原文链接&#xff1a;LLM&#xff1a;RAG 1、RAG 概览 RAG&#xff08;Retrieval-Augmented Generation&#xff0c;检索增强生成&#xff09;是一种结合了信息检索&#xff08;IR&#xff09;和 LLM 的技术。它的核心思想是在 LLM 生成回答之前&#xff0c;通过检索相关文档…

基于Qt 和微信小程序的用户管理系统:WebSocket + SQLite 实现注册与登录

目录 一. 概要 二. 技术栈 三. 系统功能设计 3.1 功能模块 3.2 数据表设计 四. 具体实现 4.1 Qt 服务端 4.1.1 初始化 WebSocket 服务器 4.1.2 用户管理界面 4.2 微信小程序端 4.2.1 注册功能 4.2.2 登录功能 五. 运行效果 六. 源码下载 一. 概要 在物联网和智能设备…

策略+适配器模式详解

文章目录 1.策略模式1.目录结构2.Strategy.java 策略接口3.StrategyA.java 策略A4.StrategyB.java 策略B5.StrategyC.java 策略C6.Context.java 策略上下文7.Client.java 客户端8.小结 2.适配器模式1.目录结构2.CustomPaymentProcessor.java 自己的支付接口3.PayPalPaymentServ…

未来游戏:当人工智能重构虚拟世界的底层逻辑

未来游戏&#xff1a;当人工智能重构虚拟世界的底层逻辑 在《赛博朋克2077》夜之城的霓虹灯下&#xff0c;玩家或许已经注意到酒吧里NPC开始出现微表情变化&#xff1b;在《艾尔登法环》的开放世界中&#xff0c;敌人的战术包抄逐渐显露出类人智慧。这些细节预示着游戏产业正站…

Zabbix——Rocky9安装zabbix相关步骤记录

安装Zabbix 安装MariaDB 这里用MariaDB演示 https://mariadb.org/download/?trepo-config&dRedHatEnterpriseLinux9&v10.11&r_mneusoft 通过这个网址获得连接 选择对应的repo 根据系统版本和要安装的版本选择对应的repo 安装 新建一个repo文件&#xff0c;例…

startai产品精修教程

1.把产品放置ps画布中 打开startai插件选择产品精修功能&#xff0c;选择金属材质即可哦 调节一下参数就可以啦&#xff0c;最终效果图 下载地址&#xff1a;StartAI画图软件官网_PS插件StartAI绘画软件生成器_Photoshop图像处理插件

【YOLOv8】

文章目录 1、yolov8 介绍2、创新点3、模型结构设计3.1、backbone3.2、head 4、正负样本匹配策略5、Loss6、Data Augmentation7、训练、推理8、分割 Demo附录——V1~V8附录——相关应用参考 1、yolov8 介绍 YOLOv8 是 ultralytics 公司在 2023 年 1 月 10 号开源的 YOLOv5 的下…

【保姆级教程】DeepSeek R1+RAG,基于开源三件套10分钟构建本地AI知识库

一、总体方案 目前在使用 DeepSeek 在线环境时&#xff0c;页面经常显示“服务器繁忙&#xff0c;请稍后再试”&#xff0c;以 DeepSeek R1 现在的火爆程度&#xff0c;这个状况可能还会持续一段时间&#xff0c;所以这里给大家提供了 DeepSeek R1 RAG 的本地部署方案。最后实现…

Oracle视图(基本使用)

视图 视图是通过定制的方式显示一个或者多个表的数据。 视图可以视为“虚拟表”或“存储的查询”。 视图的优点&#xff1a; 提供了另外一种级别的表安全性隐藏了数据的复杂性简化了用户的SQL命令隔离基表结构的改变通过重命名列&#xff0c;从另一个角度提供数据。 视图里…

梁文锋亲自挂名DeepSeek发布新论文

由 DeepSeek 联合创始人梁文锋亲自挂名的研究团队&#xff0c;在 arXiv 上发表了一篇题为“Native Sparse Attention: Hardware-Aligned and Natively Trainable Sparse Attention”的论文&#xff0c;提出了一种全新的注意力机制架构 NSA&#xff08;Native Sparse Attention&…

低代码(Low Code)全解析:从概念到应用,从选择到价值

​在数字化浪潮席卷全球的当下&#xff0c;企业对软件开发的效率与灵活性愈发重视&#xff0c;低代码平台应运而生并迅速掀起技术热潮。 本文基于笔者 6 年的低代码实践经验&#xff0c;深入剖析低代码的诸多方面&#xff0c;涵盖其定义、发展历程、国内平台对比、开发流程、与…

C++--STL库-List

目录 1.list 的基本使用 1.1 创建和初始化 1.2. 插入元素 1.3. 删除元素 1.4. 访问元素 1.5 遍历 1.6 总结 list是C标准库&#xff08;STL&#xff09;中的双向链表容器&#xff0c;属于<list>头文件。 它的特点是&#xff1a; 动态大小&#xff1a;可以随时插入…

尚硅谷 java 学习Day19 抽象类与抽象方法、接口、内部类

6-5 抽象类(abstract)与抽象方法&#xff08;important&#xff09; 一、什么叫抽象类&#xff1a; 有时候将一个父类设计的非常抽象&#xff0c;以至于它没有具体的实例&#xff0c;这样的类称为抽象类 abstract关键字的使用&#xff1a; ​ 1、abstract:抽象的 ​ 2、abs…

HomeAssistant 发现MQTT设备(温度,湿度,开关)

要通过 MQTT 将温度、湿度数据以及一个灯的开关状态传输到 Home Assistant 并实现设备自动发现&#xff0c;可以按照以下步骤操作&#xff1a; 1.前期准备工作 安装MQTT服务器(EMQX)配置好(可以在HA加载项中安装,也可以在NAS上Docker安装) HA的集成中安装MQTT,并且连接上(EM…

手写数字识别的神经网络 2层神经网络的类 代码详解

源代码和图解来自鱼书 目录 2层神经网络的类 源代码&#xff1a; 详解&#xff1a; 1. 类的初始化 (__init__) 2. 前向传播 (predict) 3. 损失函数 (loss) 4. 准确率计算 (accuracy) 5. 数值梯度计算 (numerical_gradient) 6. 反向传播计算梯度 (gradient) 总结&#…