「字符串」详解Trie(字典树|前缀树)并实现对应的功能 / 手撕数据结构(C++)

news2025/1/4 16:52:17

概述

在浏览器搜索栏里输入几个字,就弹出了以你的输入为开头的一系列句子。浏览器是怎么知道你接下来要输什么的?

来看看字典树干了什么。

字典树是一种高效记录字符串和查找字符串的数据结构。它以每个字符作为一个节点对字符串进行分割记录,节点形成树状结构,在录入或查找时只需沿着对应的路径进行操作即可。

结构如下

字典树class类trie由节点class类trie_node相连接构成,每个节点都有以下成员:一个计数器,本节点编号,一个记录此处是否是字符串结尾的变量和一个next指针数组,这个数组的每个元素都指向下一个trie_node节点或是空指针。

概念如下

根节点root编号为0,表示从此处开始构建字典树。它只有next数组,表示从此以后才是字符串的第一个字符。

对一个字符串进行枚举,取char str[i]=ch;

对于任何一个节点node和任何一个字符char ch,node->next[ch]表示从node节点再加上ch字符会走到的子节点。

如果node=node->next[ch]还不存在(空指针),那就为其创建一个新节点,标记其为整棵树的第i个节点,然后进入那个节点;

如果已经存在,那就将此处的计数器cnt+1,然后继续取下一个ch,继续向下走。

一直到字符串取完,在最后的节点处进行标记,表示到此为止是一个完整的字符串。

如上,遍历字符串"and",取第一个字符'a'。起始node=root,从root开始node=root->next['a']就进入了a节点,a节点的cnt++。现在node在节点a处。依次取'n'和‘’d',从n节点进入d节点后发现字符串已结束,那就在此处记录为结束位置。

形象记忆

输入的字符串被依次嵌入了这棵树,树上的节点被嵌入的次数越多,这个节点的颜色就越深(cnt越大),黑色的节点是某个字符串的终点。

接下来我们通过封装array类,实现动态数组的一些基本功能 。(Code和测试案例附后)

成员变量

定义class类trie,封装三个成员变量:const int branchestrie_node* root; size_t val_size;

size_t 是C/C++标准在stddef.h中定义的(这个头文件通常不需要#include),size_t 类型专门用于表示长度,它是无符号整数。)

我们还要额外定义嵌套类trie_node,它只能被trie类使用,这就实现了结构功能的封装。

const int branches表示允许每个节点设置的边数(指针数组的长度),更大的数值可以实现更强的记录能力,我们数值它的值为128,那么数字字符、小写字母与大写字母就都可以储存在trie中。

(注意:node->next[ch]表示从node节点再加上ch字符会走到的子节点。

trie_node* root指向根节点。

size_t val_size数的大小(真实长度)。

(C++11标准以后提供你在类成员声明时进行初始化,所以size_t size=0是合法的)

class trie {
private:
	class trie_node {
	private:
		friend class trie;
        ...
	};
	const int branches;
	trie_node* root;
	size_t val_size = 0;
public:
    ...
}

定义class类trie_node,封装四个成员变量:int idxint cntstd::string str; trie_node**next

声明友员类friend class trie,这使得trie可以操控trie_node的私有成员,将trie_node的构造函数和析构函数定为私有,这样就只用trie能管理trie_node了。

int idx:本节点编号。

int cnt:记数器(本节点被访问的次数)。

string str:如果插入时某个字符串在此结束,那就保存下来(起始可以直接用bool 量声明是否有字符串结束,但我们希望维护的trie有更强大的功能)。

trie_node**next:指针数组,指向接下来的子节点。

另有构造函数接受一个branch,使该节点获得有branch个子节点。

析构函数无须函数体,完全由trie类代管,略去不表。

禁用拷贝构造和重载等于号:默认拷贝构造和等于号进行,指针变量赋值,这存在极大问题(两指针争抢堆上的数据同一块数据),另有深层拷贝解决,略去不表。

class trie_node {
private:
	friend class trie;
	int idx = 0;
	int cnt = 0;
	std::string str = "";
	trie_node** next;
	trie_node(int branch) {
		next = new trie_node *[branch]();
	};
    trie_node(const trie_node& another) = delete;
	~trie_node() {};
    trie_node& operator=(const trie_node& another) = delete;
};

创建销毁

提供唯一构造函数:trie(int branch=128),默认节点边数为128,不更改时无须传参。生成一个根节点。

禁用拷贝构造和重载等于号:同上文。

析构函数:~trie(),在堆上申请的树状数据结构需要递归清理,用erase函数解决。

trie(int branch=128):branches(branch) {
	root = new trie_node(branches);
}
trie(const trie& another) = delete;
~trie() {
	erase(root);
}
trie& operator=(const trie& another) = delete;

void erase():对每个node都遍历子节点,先删子节点,再删父节点。erase需要声明在private中被封存。

void erase(trie_node* node) {
	if (!node)return;
	for (int i=0;i<128;i++)
		erase(node->next[i]);
	delete node;
}

字符串插入

插入函数:void insert(const std::string str)接收一个字符串string(传入c风格字符串const char*也是合法的,它会作为参数初始化函数中的string str)

定义p指针将string迭代嵌入trie中。

枚举str中的字符ch,如果p->next[ch]不存在就进行构造再迭代,否则直接迭代。

void insert(const std::string str) {
	trie_node *p= root;//从根节点root开始
	for (const char& ch : str) {//枚举str
		if (p->next[ch] == nullptr) {//不存在就构造
			p->next[ch] = new trie_node(branches);
			p->idx=++val_size;//新节点需要编号
		}
		p->cnt++;//计数器计数
		p = p->next[ch];
	}
	p->str = str;//在字符串结束的位置保存字符串
}

字符串查询

我们提供四种查询。

完整查询:bool query_string(const std::string str),查询str是否完整记录在案,流程与插入基本一致,遇到空节点或末尾节点无记录则返回false,否则返回true。

前缀查询:bool query_prefix(const std::string str),与上一个函数基本一致,但不判断末尾节点。

前缀字符串集查询:std::vector<std::string> query_prefix(const std::string str),返回一个所有以str为前缀的字符串数组。

查询前缀时与上个函数相同,随后使用深度优先搜索进行搜索所有以str为前缀的字符串并收集。

大小查询:size_t size(),返回val_size。

bool query_string(const std::string str)const{
	trie_node* p = root;
	for (const char& ch : str) {
		if (p->next[ch] == nullptr) return false;
		else p = p->next[ch];
	}
	if (p->str.empty())return false;
	else return true;
}
bool query_prefix(const std::string str)const{
	trie_node* p = root;
	for (const char& ch : str) {
		if (p->next[ch] == nullptr) return false;
		else p = p->next[ch];
	}
	return true;
}
std::vector<std::string> query_prefix_all(const std::string str)const{
	std::vector<std::string> ans;
	trie_node* p = root;
	for (const char& ch : str) {
		if (p->next[ch] == nullptr) return {};
		else p = p->next[ch];
	}
	DFS(p,ans);
	return ans;
}
size_t size()const{
	return size;
}

void DFS(const trie_node* node, std::vector<std::string>& ans):对每个node都遍历子节点,先存本节点,再存子节点。DFS需要声明在private中被封存。

void DFS(const trie_node* node, std::vector<std::string>& ans) {
	if (node->str.empty() == false)ans.push_back(node->str);//此处有记录就加入ans
	for (int i = 0; i < branches; i++)
		if (node->next[i])DFS(node->next[i], ans);//有子节点就进去看看
}

复杂度 

时间复杂度:插入:O(n) 查询:O(m)

空间复杂度:插入:O(n*m) 查询:O(1)

n:插入字符串数目

m:插入/查询字符串长度

Code

#pragma once
#include <string>
#include <vector>
class trie {
private:
	class trie_node {
	private:
		friend class trie;
		int idx = 0;
		int cnt = 0;
		std::string str = "";
		trie_node** next;
		trie_node(int branch) {
			next = new trie_node *[branch]();
		};
		trie_node(const trie_node& another) = delete;
		~trie_node() {};
		trie_node& operator=(const trie_node& another) = delete;
	};
	const int branches;
	trie_node* root;
	size_t val_size = 0;
	void erase(trie_node* node) {
		if (!node)return;
		for (int i=0;i<128;i++)
			erase(node->next[i]);
		delete node;
	}
	void DFS(const trie_node* node, std::vector<std::string>& ans)const{
		if (node->str.empty() == false)ans.push_back(node->str);
		for (int i = 0; i < branches; i++)
			if (node->next[i])DFS(node->next[i], ans);
	}
public:
	trie(int branch=128):branches(branch) {
		root = new trie_node(branches);
	}
	trie(const trie& another) = delete;
	~trie() {
		erase(root);
	}
	trie& operator=(const trie& another) = delete;
	void insert(const std::string str) {
		trie_node *p= root;
		for (const char& ch : str) {
			if (p->next[ch] == nullptr) {
				p->next[ch] = new trie_node(branches);
				p->idx=++val_size;
			}
			p->cnt++;
			p = p->next[ch];
		}
		p->str = str;
	}
	bool query_string(const std::string str)const{
		trie_node* p = root;
		for (const char& ch : str) {
			if (p->next[ch] == nullptr) return false;
			else p = p->next[ch];
		}
		if (p->str.empty())return false;
		else return true;
	}
	bool query_prefix(const std::string str)const{
		trie_node* p = root;
		for (const char& ch : str) {
			if (p->next[ch] == nullptr) return false;
			else p = p->next[ch];
		}
		return true;
	}
	std::vector<std::string> query_prefix_all(const std::string str)const{
		std::vector<std::string> ans;
		trie_node* p = root;
		for (const char& ch : str) {
			if (p->next[ch] == nullptr) return {};
			else p = p->next[ch];
		}
		DFS(p,ans);
		return ans;
	}
	size_t size()const{
		return val_size;
	}
};

测试 

#include "trie.h"
#include <iostream>
using namespace std;
int main()
{   
    trie Trie;
    Trie.insert("hello");
    Trie.insert("hello world"); 
    string str = "hello world and you";
    Trie.insert(str);
    vector<string>&& ans1 = Trie.query_prefix_all("hello");
    for (const string& i : ans1)cout << i << endl;
    cout << endl;
    
    Trie.insert("Hello");
    Trie.insert("World!");
    Trie.insert("Hello World!");
    cout << (Trie.query_string("Hello ") ? "YES" : "NO") << endl;
    cout << (Trie.query_prefix("Hello ") ? "YES" : "NO") << endl;
    cout << endl;

    vector<string>&& ans2 = Trie.query_prefix_all("");
    for (const string& i : ans2)cout << i << endl;
    cout << endl;
    return 0;
}

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

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

相关文章

Flink-DataWorks第二部分:数据集成(第58天)

系列文章目录 数据集成 2.1 概述 2.1.1 离线&#xff08;批量&#xff09;同步简介 2.1.2 实时同步简介 2.1.3 全增量同步任务简介 2.2 支持的数据源及同步方案 2.3 创建和管理数据源 文章目录 系列文章目录前言2. 数据集成2.1 概述2.1.1 离线&#xff08;批量&#xff09;同步…

【文献阅读】DAVE

核心思想 文章的核心思想是提出了一种名为DAVE&#xff08;Detect-and-Verify Paradigm for Low-Shot Counting&#xff09;的少样本计数方法。DAVE旨在通过一个新颖的检测和验证范式来提高低样本情况下的对象计数性能。这种方法特别关注在只有少量标注样本&#xff08;少样本…

OpenAI Gym: Understanding `action_space` notation (spaces.Box)

题意&#xff1a;OpenAI Gym: 理解action_space表示法&#xff08;spaces.Box&#xff09; 问题背景&#xff1a; I want to setup an RL agent on the OpenAI CarRacing-v0 environment, but before that I want to understand the action space. In the code on github line…

【Nuxt】Layout 布局和渲染模式

NuxtLayout app.vue <NuxtLayout><NuxtPage/></NuxtLayout>然后默认的布局 需要 写在 ~/layouts/default.vue 下面&#xff0c;其他自定义的布局也在写在 layouts 目录下。 default.vue <template><div class"app-container"><d…

比特币使用ord蚀刻符文---简单笔记

说明 毕竟符文热度过了&#xff0c;今年四月份做的笔记分享出来 蚀刻符文需要先同步完区块数据&#xff0c;和index文件&#xff0c;不然蚀刻会失败&#xff0c;在testnet和signet网络也一样。 创建钱包&#xff08;会输出助记词&#xff09;&#xff1a; ord --bitcoin-da…

【系统架构设计师】二十五、大数据架构设计理论与实践②

目录 四、 Kappa架构 4.1 Kappa架构介绍 4.2 Kappa架构的优缺点 4.3 常见Kappa 架构变形 4.3.1 Kappa 架构 4.3.2 混合分析系统的Kappa 架构 五、Lambda 架构与 Kappa 架构的对比和设计选择 5.1 Lambda架构与Kappa 架构的特性对比 5.2 Lambda架构与Kappa 架构的设计选…

2024最新数据库管理工具 Navicat Premium 简体中文版安装

Navicat Premium 是一款由 PremiumSoft 公司开发的多数据库管理工具。它支持多个数据库系统&#xff0c;包括 MySQL、MariaDB、MongoDB、SQL Server、Oracle、PostgreSQL 和 SQLite 等&#xff0c;使用户能够在一个平台上管理和操作多种数据库&#xff0c;简化了数据库管理任务…

软考基本介绍

一,基本了解 计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软件考试&#xff09;为国家级考试。 考试设置了27个专业资格&#xff0c;涵盖5个专业领域&#xff0c; 3个级别层次&#xff08;初级、中级、高级&#xff09;。 中国计算机技术职业…

品牌网络维权面临的挑战

在品牌治理渠道的过程中&#xff0c;知识产权的运用至关重要。然而&#xff0c;现实情况却不尽如人意&#xff0c;大量的侵权链接涌现&#xff0c;而下架速度迟缓&#xff0c;致使品牌渠道陷入混乱。店铺侵权背后的利益驱动是低价销售吸引消费者&#xff0c;这不仅损害品牌形象…

【HarmonyOS NEXT星河版开发学习】综合测试案例a-京东登录页面

个人主页→VON 收录专栏→鸿蒙开发小型案例总结​​​​​ 基础语法部分会发布于github 和 gitee上面&#xff08;暂未发布&#xff09; 前言 该案例有一些难度&#xff0c;将前面所学到的全部知识点做了一个全面的总结&#xff0c;代码量也不是很少。里面的一些细节一定要仔细…

MSR020/MSR040低温漂、低功耗电压基准

MSR020/MSR040 是低温漂、低功耗、高精度 CMOS 电压基准&#xff0c; 具有 0.05% 初始精度和低功耗的特点。 该器件的低输出电压迟滞和低长期输出电压漂移的 特性&#xff0c;可以进一步提高稳定性和系统可靠性。 此外&#xff0c;器 件的小尺寸和低工作电流的特性使其非…

lvs-nat模式原理及部署方法

一、环境准备 1.准备三台rhel9服务器 服务器名称 主机名 ip地址备注LVS调度服务器lvs.timinglee.org eth0:172.25.254.100&#xff08;外网&#xff09; eth1:192.168.0.100(内网) 关闭selinux和防火墙webserver2网站服务器webserver1.timinglee.orgeth0&#xff1a;192.168.…

【竞技宝】奥运会:法国国奥淘汰埃及国奥晋级决赛

法国国奥在巴黎奥运会男足半决赛跟埃及国奥相遇&#xff0c;赛前大部分球迷和媒体&#xff0c;都一边倒看好法国国奥能轻松获胜。首先&#xff0c;法国国奥整体实力高出一个档次。最后&#xff0c;法国国奥坐拥主场作战的优势。所以&#xff0c;法国国奥正常发挥的话&#xff0…

五条关于有效部署和维护MQTT解决方案的最佳实践

节选自论文《Message Queuing Telemetry Transport (MQTT) 在森林产品应用中的分析与概述》&#xff08;Analysis and Overview of Message Queuing Telemetry Transport (MQTT) as Applied to Forest Products Applications&#xff09;&#xff0c;发表于《IEEE TRANSACTIONS…

使用Cisco进行模拟配置OSPF路由协议

OSPF路由协议 1.实验目的 1&#xff09;理解OSPF 2&#xff09;掌握OSPF的配置方法 3&#xff09;掌握查看OSPF的相关信息 2.实验流程 开始 → 布置拓扑 → 配置IP地址 → 配置OSPF路由并验证PC路由的连通性 → 查看路由器路由信息 → 查看路由协议配置与统计信息 → 查看O…

锡耶纳大学与 NocoBase:教育管理系统的全新篇章

关于锡耶纳大学 锡耶纳大学&#xff08;意大利语&#xff1a;Universit degli Studi di Siena&#xff0c;简称UNISI&#xff09;建于 1240 年&#xff0c;是欧洲最古老的大学之一。如今&#xff0c;锡耶纳大学以其法学院和医学院闻名。这所著名的大学坐落在意大利托斯卡纳的中…

Redis和数据库一致性如何保证

三种经典的缓存模式 Cache-Aside Pattern&#xff08;旁路缓存模式&#xff09;Read-Through/Write-through&#xff08;读写穿透&#xff1a;和1类似&#xff09;Write-behind &#xff08;异步批量从缓存写数据库&#xff09; 旁路缓存模式 读流程 读的时候&#xff0c;先…

HTML5+CSS3笔记(Xmind格式):第四天

Xmind鸟瞰图&#xff1a; 简单文字总结&#xff1a; HTML5CSS3知识总结&#xff1a; 媒体查询&#xff1a; 1.媒体查询格式&#xff1a;media 设备类型 and 设备特性 2.screen&#xff1a;设置屏幕 3.max-width(最大宽度),min-width(最小宽度) 4.可以通过媒体…

阿里国际推出首个专业版AI Search,为什么它会是下一个B2B谷歌?

点击访问我的技术博客https://ai.weoknow.comhttps://ai.weoknow.com 经历过「千模大战」的喧嚣&#xff0c;一年半之后&#xff0c;生成式 AI 的应用层创新终于步入爆发期。 年初的 Sora 激起一阵 AI 视频生成的浪潮。涟漪未散&#xff0c;OpenAI 新的 SearchGPT 又燃起了 A…

CSP-J 复赛模拟题4 解析

需要注意的是&#xff0c;每次操作询问之后都不会对原序列进行修改&#xff0c;即每次操作都是在原序列上直接进行的。 就是说把到l到r之间的所有数用cnt每次加p就行了 根据解析写代码1&#xff1a; #include <bits/stdc.h> #define LL long long using namespace std;…