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

目录

一,字典树的定义

二,字典树的代码实现

完整代码+详细注释:

测试用例+测试结果:

三,处理其他字符 

四,内存优化与扩展

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;
};

 

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL 之存储引擎(MySQL Storage Engine)

MySQL 之存储引擎 常见存储引擎及其特点 ‌InnoDB‌&#xff1a; ‌特点‌&#xff1a;支持事务处理、行级锁定、外键约束&#xff0c;使用聚簇索引&#xff0c;适合高并发读写和事务处理的场景‌。‌适用场景‌&#xff1a;需要高可靠性、高并发读写和事务处理的场景‌。 ‌M…

CXL ALMP(ARB/MUX Link Management Packet)理解

前言&#xff1a; ALMP&#xff08;ARB/MUX Link Management Packet&#xff09; 是CXL协议中由ARB/MUX层生成和处理的专用管理报文&#xff0c;用于协调链路电源状态切换&#xff08;如L0s/L1&#xff09;和虚拟链路状态机&#xff08;vLSM&#xff09;同步。以下是其核心特性…

002 SpringCloudAlibaba整合 - Feign远程调用、Loadbalancer负载均衡

前文地址&#xff1a; 001 SpringCloudAlibaba整合 - Nacos注册配置中心、Sentinel流控、Zipkin链路追踪、Admin监控 文章目录 8.Feign远程调用、loadbalancer负载均衡整合1.OpenFeign整合1.引入依赖2.启动类添加EnableFeignClients注解3.yml配置4.日志配置5.远程调用测试6.服务…

计算机网络(3)TCP格式/连接

1、TCP三大特点&#xff1a;面向连接、可靠、基于字节流 2、如何唯一确定一个TCP连接&#xff1f;TCP四元组&#xff1a;源地址、源端口、目的地址、目的端口 源地址和目标地址的字段(32 位)是在 IP 头部中&#xff0c;作用是通过 IP 协议发送报文给对方主机源端口和目标端口…

vscode远程报错:Remote host key has changed,...

重装了Ubuntu系统之后&#xff0c;由20.04改为22.04&#xff0c;再用vscode远程&#xff0c;就出现了以上报错。 亲测有效的办法 gedit ~/.ssh/known_hosts 打开这个配置文件 删掉与之匹配的那一行&#xff0c;不知道删哪一行的话&#xff0c;就打开第一行这个 /.ssh/confi…

无符号整数和带符号整数的相互转换

无符号字符数x转换为带符号字符数时&#xff0c;当时&#xff0c;转换后仍然为x&#xff1b;当时&#xff0c;转换后变为。 带符号字符数y转换为无符号字符数时&#xff0c;当时&#xff0c;转换后变为&#xff1b;当时&#xff0c;转换后仍然为y。 无符号整数和带符号整数的…

浏览器报错:无法访问此网站 无法找到xxx.xxx.net的DNS地址。正在诊断该问题。尝试运行Windows网络诊断。DNS_PROBE_STARTED

&#x1f91f;致敬读者 &#x1f7e9;感谢阅读&#x1f7e6;希望我的文章能帮到您&#x1f7ea;如有兴趣可点关注了解更多内容 &#x1f4d8;博主信息 点击标题&#x1f446;有惊喜 &#x1f4c3;文章前言 &#x1f537;文章均为学习和工作中整理的笔记&#xff0c;分享记录…

2025-02-18 学习记录--C/C++-PTA 7-25 念数字

一、题目描述 ⭐️ 二、代码&#xff08;C语言&#xff09;⭐️ /*** 输入一个整数&#xff0c;输出每个数字对应的拼音。当整数为负数时&#xff0c;先输出fu字。*/#include <stdio.h>// 输出 正数 中 各位数 对应的 拼音 void getLetter(int num) {// 10个数字&#x…

VirtualBox 中使用 桥接网卡 并设置 MAC 地址

在 VirtualBox 中使用 桥接网卡 并设置 MAC 地址&#xff0c;可以按照以下步骤操作&#xff1a; 步骤 1&#xff1a;设置桥接网卡 打开 VirtualBox&#xff0c;选择你的虚拟机&#xff0c;点击 “设置” (Settings)。进入 “网络” (Network) 选项卡。在 “适配器 1” (Adapt…

Fiddler笔记

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

【数据结构初阶第十节】队列(详解+附源码)

好久不见。。。别不开心了&#xff0c;听听喜欢的歌吧 必须有为成功付出代价的决心&#xff0c;然后想办法付出这个代价。云边有个稻草人-CSDN博客 目录 一、概念和结构 二、队列的实现 Queue.h Queue.c test.c Relaxing Time&#xff01; ————————————《有没…

idea无法联网,离线安装插件

插件地址&#xff1a;https://plugins.jetbrains.com/ JetBrains Marketplace 如果无法进入&#xff0c;可以试试 配置hosts 3.163.125.103 plugins.jetbrains.com ip 变了&#xff0c;可以查询个最新的&#xff1a; https://tool.chinaz.com/speedtest/plugins.jetbrai…

二十多年前的苹果电源Power Mac G4 Mdd 电源接口

在1999年&#xff0c;苹果推出了最初的Power Mac G4电脑。第一代Power Mac G4有与G3系列相似的外壳和两种主板设置&#xff0c;分别使用PCI和AGP显示总线。第二代电脑被昵称为快银或水银机&#xff0c;来自2001年的它们有更高速的PowerPC 7450系列芯片&#xff0c;增强了L2缓存…

qt:按钮的常见操作(简单方向键项目)

1.圆形按钮 首先&#xff0c;设置圆形按钮&#xff0c;首先要将setGeometry(x位置&#xff0c;y位置&#xff0c;长&#xff0c;宽)中的长和宽设置为相等&#xff0c;再使用一下模板 q2->setStyleSheet("QPushButton {"" background-color: black;"…

SAP-ABAP:外部断点设置详解

在 SAP 中打外部断点&#xff08;External Breakpoint&#xff09;是调试 ABAP 程序的一种常用方法&#xff0c;尤其是在调试标准程序、增强或用户出口时。外部断点允许开发人员在特定用户或特定会话中触发断点&#xff0c;而不会影响其他用户。以下是使用外部断点时需要注意的…

springboot399-中文社区交流平台(源码+数据库+纯前后端分离+部署讲解等)

&#x1f495;&#x1f495;作者&#xff1a; 爱笑学姐 &#x1f495;&#x1f495;个人简介&#xff1a;十年Java&#xff0c;Python美女程序员一枚&#xff0c;精通计算机专业前后端各类框架。 &#x1f495;&#x1f495;各类成品Java毕设 。javaweb&#xff0c;ssm&#xf…

(9/100)每日小游戏平台系列

项目地址位于&#xff1a;小游戏导航 新增一个跳跃小方块&#xff01; 游戏简介 跳跃小方块&#xff08;Jumping Square&#xff09;是一款轻松有趣的休闲小游戏&#xff0c;考验玩家的反应速度和操作技巧。玩家需要控制一个蓝色小方块&#xff0c;通过点击屏幕或按下空格键进…

React实现自动滚动表格

在 React 中实现一个自动滚动的表格&#xff0c;可以通过 CSS 动画和 JavaScript 定时器来实现。以下是一个完整的示例代码&#xff0c;包含示例数据和自动滚动功能。 实现思路&#xff1a; ** 自动滚动&#xff1a;** 使用 setInterval 实现表格的自动滚动。 手动滚动&…

数字滤波器的设计实现及应用(论文+仿真)

3.1系统总体设计 对于本次毕业设计的课题基于DSP的IIR数字滤波器来说&#xff0c;在此选用了TI公司的DSP芯片TMS320F5402芯片来作为数字滤波器的主控制器&#xff0c;另外再采用高速AD模拟到数字转换芯片来进行输入信号的采样&#xff0c;以此将采集到的信号输出给主控制器进行…

Blackbox.AI:高效智能的生产力工具新选择

前言 在当今数字化时代&#xff0c;一款高效、智能且功能全面的工具对于开发者、设计师以及全栈工程师来说至关重要。Blackbox.AI凭借其独特的产品特点&#xff0c;在众多生产力工具中脱颖而出&#xff0c;成为了我近期测评的焦点。以下是我对Blackbox.AI的详细测评&#xff0…