当前位置: 首页 > news >正文

【算法与数据结构】字典树(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;
};

 

 

相关文章:

  • java爬虫抓取网页搜索数据
  • 蓝桥杯 Java B 组 之树的基础(二叉树遍历)
  • Linux Socket编程:TCP开发指南
  • MoE硬件部署
  • 动态规划从入坟走向入坑
  • Python 和 R机器学习(2)随机森林
  • 通信:Winform(结合C#) TCP服务端
  • 探秘 Python 枚举类型:从基础到实战的深度指南
  • uniapp Flex 布局使用记录
  • CE RED 增加网络安全 添加新网络安全类型
  • python爬虫系列课程2:如何下载Xpath Helper
  • curl 命令详细介绍
  • 负载均衡(SLB)后端实例不健康:腾讯云如何协助解决
  • 【C++设计模式 – 工厂(Factory)模式】—— 对象创建的优雅解耦方案
  • Linux面试题
  • Bom详解和Dom详解
  • 本地使用docker部署DeepSeek大模型
  • 【保姆级教程】DeepSeek R1+RAG,基于开源三件套10分钟构建本地AI知识库
  • Java全栈项目-田径运动会管理系统
  • 23种设计模式 - 责任链
  • 占地57亩的“潮汕豪宅”面临强制拆除:曾被实施没收,8年间举行5次听证会
  • 市场监管总局召开平台企业支持个体工商户发展座谈会
  • 深圳拟出让3宗居住用地,共计用地面积6.77公顷
  • 5吨煤炭“瞬间蒸发”?掺水炭致企业损失千万,腐败窝案曝光
  • 宝通科技:与宇树合作已签约,四足机器人在工业场景落地是重点商业化项目
  • 线下哪些商家支持无理由退货?查询方法公布