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

news2024/11/16 19:46:51

概述

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

来看看字典树干了什么。

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

结构如下

字典树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,封装四个成员变量: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() {};
};

创建销毁

提供唯一构造函数: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) {
	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) {
	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) {
	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() {
	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 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) {
		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) {
		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) {
		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) {
		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() {
		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/1975419.html

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

相关文章

48 集合应用案例

编写代码时除了要准确地实现功能之外&#xff0c;还要考虑代码的优化&#xff0c;尽量找到一种更快、更好的方法实现预定功能。Python 字典和集合都使用哈希表来存储元素&#xff0c;元素查找速度非常快&#xff0c;关键字 in 作用于字典和集合时比作用于列表要快得多。 impor…

【数据结构之单链表的实现(不带头)】

1.单链表 1.1概念与结构 链表是一种物理存储结构上非连续&#xff0c;非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针连接次序实现的。 可以用下图便于理解 节&#xff08;结&#xff09;点&#xff1a; 与顺序表不同的是&#xff0c;链表里面的每节“车…

三十种未授权访问漏洞合集

未授权访问漏洞介绍 未授权访问可以理解为需要安全配置或权限认证的地址、授权页面存在缺陷&#xff0c;导致其他用户可以直接访问&#xff0c;从而引发重要权限可被操作、数据库、网站目录等敏感信息泄露。---->目录遍历 目前主要存在未授权访问漏洞的有:NFS服务&a…

百度飞桨 OCR识别

百度飞桨 OCR识别代码 import warnings import time import cv2 as cv import paddlehub as hub # Load the image img cv.imread("1.jpg") height, width, channels img.shape imglist [img] ocr hub.Module(name"ch_pp-ocrv3", enable_mkldnnTrue) …

从Axure入门,开始了解产品

​不少想要求职产品经理的小伙伴在问一个问题&#xff1a;我是一个纯小白&#xff0c;一点基础都没有&#xff0c;我该如何入门产品呢&#xff1f;当然想要入门产品&#xff0c;很多人都有自己的一套方法&#xff0c;这里推荐其中的一种方法&#xff0c;从原型工具&#xff0c;…

ModuleNotFoundError: No Module Named openai

题意&#xff1a;Python 无法在环境中找到名为 openai 的模块 问题背景&#xff1a; import requests from bs4 import BeautifulSoup import openai #write each line of nuclear.txt to a list with open(nuclear.txt, r) as f:lines f.readlines()#remove the newline cha…

Spring源码-ClassPathXmlApplicationContext的refresh()都做了什么?

AbstractApplicationContext的refresh方法 /*** 用给定的父类创建一个新的ClassPathXmlApplicationContext* Create a new ClassPathXmlApplicationContext with the given parent,* 从给定的XML文件加载定义* loading the definitions from the given XML files.* param confi…

UE5 从零开始制作跟随的大鹅

文章目录 二、绑定骨骼三、创建 ControlRig四、创建动画五、创建动画蓝图六、自动寻路七、生成 goose八、碰撞 和 Physics Asset缺点 # 一、下载模型 首先我们需要下载一个静态网格体&#xff0c;这里我们可以从 Sketchfab 中下载&#xff1a;Goose Low Poly - Download Free …

十条线路:畅享张北草原天路玩法

2024年6月6日&#xff0c;张家口市政府新闻办召开新闻发布会&#xff0c;发布10条草原天路精品旅游线路&#xff0c;同时就草原天路今年改造提升重点工作进行介绍。其中&#xff0c;10条精品旅游线路包含5条玩转天路经典线路和5条穿越天路新玩法线路。 1、寻“天路之巅”网红打…

Java并发编程 使用锁和状态位来控制线程的执行顺序

Java线程生命周期的认识 对于线程的生命周期&#xff0c;在Java和操作系统中&#xff0c;在概念上有一点小小的不同。 在操作系统层面上&#xff0c;线程的生命周期如下&#xff1a; 1.新建 2.就绪 3.阻塞 4.运行 5.终止 而在Java层面上&#xff0c;则把线程的阻塞状态又划分…

详细分析Flask部署云服务器(图文介绍)

目录 前言1. 安装配置2. 代码部署3. 服务配置4. 自启动前言 Nginx信息补充阅读: Nginx从入门到精通(全)Nginx配置静态网页访问(图文界面)本文着重提供思路逻辑 1. 安装配置 最好的方式是安装docker,通过docker安装nginx,推荐阅读:Docker零基础从入门到精通(全)包环…

与用户有关的接口

1.获取用户详细信息 跟着黑马程序员继续学习SpringBoot3Vue3 用户登录成功之后跳转到首页&#xff0c;需要获取用户的详细信息 打开接口文档 使用Token令牌解析得到用户名 我们需要根据用户名查询用户&#xff0c;获取详细信息 但是请求参数是无&#xff0c;由于都需要携…

标题生成器:开启创意写作的新篇章

文章目录 角色与目标标题生成器的功能标题生成器的优势指导原则限制与澄清应用场景对创意写作的影响智能体发布到微信公众号配置公众号菜单配置自动回复自动回复文本链接自动回复二维码图片 标题生成器的未来发展总结 博主介绍&#xff1a;全网粉丝10w、CSDN合伙人、华为云特邀…

C++入门基本语法(1)

一、命名空间namespace 定义变量、函数时&#xff0c;定义的名称可能会和头文件中或者自己重复使用的名称冲突&#xff1b;namespace可以对标识符的名称进行本地化&#xff0c;以避免冲突的问题&#xff1b; ## 例如&#xff1a; ## 出现这种问题的原因&#xff1a; &#x…

MySQL系列之--详细安装教程和启动方法

文章目录 安装教程打开或关闭方式方式1&#xff1a;方式2&#xff1a; 客户端连接方式客户端连接方式1&#xff1a;客户端连接方式2&#xff1a;MySQL环境变量的配置 安装教程 1、mysql官网下载最新的符合本系统的版本 2、点击.msi文件进入安装页面 选择默认的选项开发者安…

品味食家巷蛋奶酪饼,感受西北美食魅力

在广袤的西北大地&#xff0c;美食的丰富多样令人叹为观止。而食家巷蛋奶酪饼&#xff0c;宛如一颗璀璨的明珠&#xff0c;散发着独特的魅力。 这款蛋奶酪饼&#xff0c;是传统工艺与现代口味的完美融合。而当你继续品尝&#xff0c;鸡蛋的鲜嫩和奶酪的浓郁醇厚便会在口中交融…

跟《经济学人》学英文:2024年08月03日这期 GPT, Claude, Llama? How to tell which AI model is best

GPT, Claude, Llama? How to tell which AI model is best Beware model-makers marking their own homework 原文&#xff1a; When Meta, the parent company of Facebook, announced its latest open- source large language model (LLM) on July 23rd, it claimed that…

vue2 使用 tinymce富文本编辑器

注意&#xff1a; 在vue2中使用tinymce有版本限制的&#xff0c;最新版都是支持v3的&#xff0c;官方也说明了&#xff1b; vue2中不能使用tinymce/tinymce-vue 为4以上的版本&#xff1b; 使用步骤&#xff1a; 1、vue项目中安装 tinymce&#xff1b; npm install tinymce5.…

用TensorFlow训练自己的第一个模型

现在学AI的一个优势就是&#xff1a;前人栽树后人乘凉&#xff0c;很多资料都已完善&#xff0c;而且有很多很棒的开源作品可以学习&#xff0c;感谢大佬们 项目 项目源码地址 视频教程地址 我在大佬的基础上基于此模型还加上了根据特征值缓存进行快速识别的方法&#xff0c;…

【教程】Python语言的地球科学常见数据——全球大气再分析数据

a、多年数据的读取 b、趋势分析 c、多时间尺度统计。 ECMWF 中心推出的 ERA5 全球大气再分析数据提供了大量大气、陆地和海洋气候变量的逐小时数据。这些数据在 30km 网格上覆盖了全球&#xff0c;在时间跨度上从 1979 至今。该数据能够提供全球范围的格点气象数据。 将针对该…