数据结构与算法10:递归树、Trie树、B+树

news2024/11/24 16:47:43

目录

【递归树】

【Trie 树】

【B+树】 

【每日一练:最长公共前缀】 


【递归树】

递归的思想是将大问题分解为小问题,然后再将小问题分解为更小的问题,直到问题的数据规模被分解得足够小,不用继续递归分解为止。如果把这个一层一层的分解过程画成图,其实就是一棵树,可以称之为“递归树”。比如之前在讲递归的时候提到的斐波那契数列的递归实现,如果画成递归树就是下面的样子:

// 斐波那契数列:1、1、2、3、5、8、13、21、34、...
// F(0)=1, F(1)=1, F(n)=F(n-1)+F(n-2)
func getFibonacci(n int) int {
	if n == 1 || n == 2 {
		return 1
	}
	return getFibonacci(n-1) + getFibonacci(n-2)
}

在 数据结构与算法07:高效的排序算法 中说过归并排序使用的是“分而治之”的思想,每次都将数据规模一分为二,如果画成递归树就是下面的样子:

可以看出来,归并排序的递归树是一棵满二叉树,在二分的过程中的时间复杂度是O(logn)。实际上,对于很多业务场景需要使用“二分思想”的情况下,基本上都可以拆解为二叉树这种数据结构来处理,比如归并排序、快速排序、二分查找、跳表等。

【Trie 树】

Trie树 也叫“字典树”,它是一种专门处理字符串匹配的数据结构,用来解决在一组字符串集合中快速查找某个字符串的问题。如果要快速查找某个字符串是否在指定的字符串仓库中,除了O(n)时间复杂度的全部遍历方法之外,还可以用Trie树来高效的解决这个问题。 

Trie 树是利用字符串之间的公共前缀将重复的前缀合并在一起,比如现在需要在 city、cat、car、door、dog、deep 这几个单词中快速查找某个单词是否存在,就可以把这几个单词组织成一个Trie树,如下所示:

Trie 树有以下特点:

  • 使用多叉树来表示,比如上面查找单词的Trie树的第一层使用26叉树来存储26个英文字母;
  • 根结点不包含字符,除根结点外每一个结点都只包含一个字符;
  • 查找某个单词的时候,就把根结点到某一个叶子结点的路径上经过的字符连接起来,如果一直到叶子结点还不匹配,那就是没找到;
  • 创建Trie树的过程需要扫描所有字符串,因此创建的时间复杂度是O(n),n表示所有字符串长度之和;
  • 查找字符串的时间复杂度是 O(k),k 表示要查找的字符串的长度;

使用Go语言实现一个Trie树的代码如下(点此查看源代码):

package main

import (
	"fmt"
)

// Trie树结构体
type TrieNode struct {
	Data  byte               //存储字符数据
	Next  map[byte]*TrieNode //下一个节点指针
	IsEnd bool               //是否到达叶子结点
}

// Trie树根节点
type TrieRoot struct {
	Len  int //字符个数
	Node map[byte]*TrieNode
}

// 初始化一个Trie树,并给根节点赋值
func NewTire() TrieRoot {
	tRoot := TrieRoot{
		Len:  0,
		Node: make(map[byte]*TrieNode),
	}
	return tRoot
}

// 插入字符
func (tn *TrieRoot) insert(str string) {
	cur := tn.Node
	//循环遍历单词的每个字符
	for i := 0; i < len(str); i++ {
		if cur[str[i]] == nil {
			newNode := &TrieNode{
				Data:  str[i],
				Next:  make(map[byte]*TrieNode),
				IsEnd: false,
			}
			cur[str[i]] = newNode
		}
		//判断是否到达叶子结点
		if i == len(str)-1 {
			cur[str[i]].IsEnd = true
		}
		cur = cur[str[i]].Next
	}
	tn.Len++
}

// 查找字符
func (tn *TrieRoot) find(str string) bool {
	if tn.Node == nil {
		return false
	}
	cur := tn.Node
	//循环遍历单词的每个字符
	for i := 0; i < len(str); i++ {
		//如果某个字符不存在,直接返回false
		if cur[str[i]] == nil {
			return false
		}
		if cur[str[i]].Data != str[i] {
			return false
		}
		if i == len(str)-1 {
			//判断最后一个比较的字符是否到达叶子结点,如果不需要精确匹配就去掉这个判断
			if cur[str[i]].IsEnd == true {
				return true
			}
		}
		cur = cur[str[i]].Next
	}
	return false
}

func main() {
	tire := NewTire()
	tire.insert("city")
	tire.insert("cat")
	tire.insert("car")
	tire.insert("door")
	tire.insert("dog")
	tire.insert("deep")
	fmt.Println("字符串数量:", tire.Len) //6

	fmt.Println(tire.find("cat")) //true
	fmt.Println(tire.find("ca"))  //false,因为是精确匹配
	fmt.Println(tire.find("cd"))  //false
}

上面代码中查找的时候使用的精确匹配,也就是判断到达叶子结点才算匹配完成,如果要实现前缀匹配,比如在“cat”中查找“ca”,就把查找方法中的 if cur[str[i]].IsEnd == true {} 条件去掉即可。

想一想,手机输入法的自动补充词语,和搜索引擎的关键词自动补全,是不是和这个很类似:

【B+树】 

我之前在 MySQL底层数据结构的深入分析 这篇文章中大概分析了B树和B+树,B树是⼀种多叉平衡查找树,而且⾮叶⼦节点和叶⼦节点都会存储数据;B+树是只有叶⼦节点才会存储数据。这里的 B 一般被解读为 balance,也就是平衡树。

在MySQL中查询数据的时候,通过索引可以让查询数据的效率更高,这是关注的时间复杂度;同时在存储空间方面希望索引不要消耗太多的内存空间,这是关注的空间复杂度。如果使用二叉搜索树作为数据库的索引,会导致一颗数据库索引树太过“高”和“瘦”,如果把树存储在硬盘中,那么每个节点的读取访问都对应一次磁盘 IO 操作,树的高度就等于每次查询数据时磁盘 IO 操作的次数。比起内存读写操作,磁盘 IO 操作非常耗时,所以应该减少磁盘 IO 操作,降低树的高度。如果把二叉树改造成M叉树(M>2),高度自然会变低,如下图所示的二叉树变成五叉树:

M叉树虽然比二叉树低,但是M也不能太大,因为不管是内存中的数据还是磁盘中的数据,操作系统都是按页(一页大小通常是 4KB)来读取的,一次读取一页的数据,如果要读取的数据量超过一页的大小,就会触发多次 IO 操作。所以在选择M大小的时候,要尽量让每个节点的大小等于一个页的大小,这样的话读取一个节点只需要一次磁盘 IO 操作。

MySQL使用的B+树是从B树演化而来的, 而B树是从多叉树演化而来的,对于一个B+树来说,M的值是根据页的大小事先计算好的,每个节点最多只能有M个子节点。在往数据库中写入数据的时候有可能使索引中某些节点的子节点个数超过M,从而导致这个节点的大小超过了一个页的大小,读取这样的节点就会导致多次磁盘 IO 操作。对于这种情况,B+树会将这个节点分裂成两个节点,对于父节点也这样操作,一直影响到根节点。这就是一个由下到上的分裂过程,如下面的动画:

B+树生成过程

这里总结一下 B+树的特点:

  • 每个节点中子节点的个数不能超过M,也不能小于 M/2;
  • M叉树只存储索引,并不真正存储数据,有点类似跳表(关于跳表参考:数据结构与算法05:跳表和散列表);
  • 通过链表将叶子节点串联在一起,这样可以方便按区间查找;
  • 一般情况下,根节点存储在内存中,其他节点存储在磁盘中。

【每日一练:最长公共前缀】 

力扣14. 最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""。

示例 1:输入:strs = ["flower","flow","flight"],输出:"fl"
示例 2:输入:strs = ["dog","racecar","car"],输出:"",解释:输入不存在公共前缀。

(点此查看源代码)

思路1:先计算数组中最短的字符串长度,在这个长度范围内,从头比较数组中每一个字符串相同位置的字符是否都相同。空间复杂度 O(1),时间复杂度 O(S),S 是所有字符串中字符数量的总和。最坏情况下,输入数据为n个长度为m的相同字符串,会进行 S = m * n 次比较;最好的情况下,只需要进行 n * min(Len(N)) 次比较,其中 min(Len(N)) 是数组中最短字符串的长度。

func longestCommonPrefix1(strs []string) string {
	length := len(strs)
	if length == 0 {
		return ""
	}
	minLen := len(strs[0])
	tmp := 0
	for i := 1; i < length; i++ {
		tmp = len(strs[i])
		if tmp < minLen {
			minLen = len(strs[i])
		}
	}
	result := ""
	for j := 0; j < minLen; j++ {
		for k := 0; k < length-1; k++ {
			if strs[k][j] != strs[k+1][j] {
				return result
			}
		}
		result += string(strs[0][j])
	}
	return result
}

func main() {
	fmt.Println(longestCommonPrefix1([]string{"flower", "flow", "flight"})) //fl
	fmt.Println(longestCommonPrefix1([]string{"dog", "racecar", "car"}))    //""
}

思路2:依次假设最长公共前缀长度为0到len(strs[0]) ,在每一轮循环中只要strs中存在比当前最长公共前缀(LCP)长度更短的字符串,或者strs中存在和当前 index 字符不相同的字符串,就返回上一轮的最长公共前缀,如果一直没返回,说明strs[0]就是最长公共前缀。时间复杂度: O(N * len(strs(0)),空间复杂度: O(1)。

func longestCommonPrefix2(strs []string) string {
	if len(strs) == 0 {
		return ""
	}
	for i := 0; i < len(strs[0]); i += 1 {
		for _, str := range strs {
			// 只要strs中存在比当前长度i更短的string,立刻返回上一轮LCP,即strs[0][:i]
			// 只要strs中存在当前index字符与LCP该index不相同的字符串,立刻返回上一轮LCP,即strs[0][:i]
			if len(str) <= i || strs[0][i] != str[i] {
				return strs[0][:i]
			}
		}
	}
	return strs[0] // 如果一直没返回,说明strs[0]本身就是LCP,返回它
}

func main() {
	fmt.Println(longestCommonPrefix2([]string{"flower", "flow", "flight"})) //fl
	fmt.Println(longestCommonPrefix2([]string{"dog", "racecar", "car"}))    //""
}

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

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

相关文章

Effective第三版 中英 | 第2章 创建和销毁对象 | 用私有构造器或者枚举类型强化 Singleton 属性

文章目录 Effective第三版前言第二章 创建和销毁对象用私有构造器或者枚举类型强化 Singleton 属性 Effective第三版 前言 大家好&#xff0c;这里是 Rocky 编程日记 &#xff0c;喜欢后端架构及中间件源码&#xff0c;目前正在阅读 effective-java 书籍。同时也把自己学习该书…

如何在本地配置Github的项目--Python

如何在本地配置Github的项目 0. 引言1. 初步预览2. 配置环境2.1 环境已经给出2.2 环境未曾给出 3. 数据配置4. 依次调试5. 配置完成总结 0. 引言 Github上存在大量的代码。当下载下来后可能会存在疑惑&#xff1a;如何在本地配置对应的项目呢&#xff1f; 为了帮助新手解决这一…

【Android开发基础】购物车代码整理

文章目录 一、数据库设计二、Home界面三、购物车模块四、添加五、源代码 这个月总算忙完了&#xff0c;总算能够抽出时间来&#xff0c;认真写一下博客了。整理一下购物车的代码 一、数据库设计 基于SqLite简单设计一个数据存储逻辑 实体&#xff08;接收数据&#xff09; im…

【数据加密】古典密码Playfair

文章目录 一、引言1、主要任务2、分支3、密码体制分类4、攻击密码系统 二、普莱费厄体制1、构造字母表&#xff0c;设为密钥矩阵2、设立加密方法3、加密解密4、字典集合5、结果 一、引言 1、主要任务 解决信息的保密性和可认证问题&#xff0c;保证信息在生成、传递、处理、保…

Swin-Transformer详解

Swin-Transformer详解 0. 前言1. Swin-Transformer结构简介2. Swin-Transformer结构详解2.1 Patch Partition2.2 Patch Merging2.3 Swin Transformer Block2.3.1 W-MSA2.3.2 SW-MSA 3. 模型配置总结 0. 前言 Swin-Transformer是2021年微软研究院发表在ICCV上的一篇文章&#x…

数据的存储(浮点型)

目录 浮点型存储的规则 1.前面我们已经学过了整形在数据中的存储是以原码&#xff0c;反码&#xff0c;补码的形式在内存中存储的&#xff0c;那么浮点数是以什么样的形式存储的呢&#xff1f; 接下来我们通过一段代码来观察——> int main() {int n 9;float* p (float*…

String AOP的使用

面向切面编程&#xff0c;面向特定方法编程&#xff0c;以方法为对象&#xff0c;在不修改原方法的基础上&#xff0c;对方法进行操作扩展等&#xff0c;底层是通过动态代理实现的 使用开发步骤&#xff1a; 1、创建一个类&#xff0c;加上Aspect声明为一个AOP切面类&#xff…

2023 重新开始

感觉搞 IT 的日子最近都有点不太好过。 早上接到公司电话说今天是一个大日子。 为什么是大日子&#xff0c;相信所有人都是懂的。这次公司将会经历一次非常大的裁员&#xff0c;很不幸也在列表中。不过感觉这个好像也没有什么关系。 因为早就在意料之中的事情&#xff0c;经历…

c语言之结构体(初阶)

目录 1&#xff1a;结构体类型的声明 2&#xff1a;结构体初始化 3&#xff1a;结构体成员访问 4&#xff1a;结构体传参 1&#xff1a;结构体类型的声明 1&#xff1a;为啥要有结构体&#xff0c;因为当我们描述一个复杂对象的时候&#xff0c;可能平时我们的一个类型不能…

常见的五种排序

&#x1f436;博主主页&#xff1a;ᰔᩚ. 一怀明月ꦿ ❤️‍&#x1f525;专栏系列&#xff1a;线性代数&#xff0c;C初学者入门训练&#xff0c;题解C&#xff0c;C的使用文章&#xff0c;「初学」C &#x1f525;座右铭&#xff1a;“不要等到什么都没有了&#xff0c;才下…

批量提取某音视频文案(二)

牙叔教程 简单易懂 之前写过一篇 批量提取某音视频文案 , 在之前的教程中, 我用的是微软的语音转文字功能, 今天我们换个方法, 使用 逗哥配音 的 文案提取 功能 准备工作 下载视频和音频 我在github找到的是这个仓库 https://github.com/Johnserf-Seed/TikTokDownload 注意一…

VLANIF虚接口案例实践

1&#xff09;拓扑 2&#xff09;需求&#xff1a; -所有PC能够ping通自己的网关 -实现vlan间互通&#xff0c;实现所有的PC互通 3&#xff09;配置步骤&#xff1a; 第一步&#xff1a;给pc配置IP地址 第二步&#xff1a;交换机创建vlan,做access和trunk -所有的交换机都配…

传统图形学对nerf的对比与应用落地

作者今年参加了China3DV的盛会&#xff0c;大会的发表、线下讨论、学者、工业界等等的交流着实对于Nerf有了更深的思考&#xff0c;以下是作者的抛砖引玉&#xff0c;如有不当之处敬请指出~ 传统图形学与nerf的简介&#xff1a; 传统图形学&#xff1a;显示表达几何表达方式&…

【CloudCompare教程】010:点云的裁剪功能(分段、裁剪、筛选)

本文讲解CloudCompare点云的裁剪功能(分段、裁剪、筛选)。 文章目录 一、点云的分段二、点云的裁剪三、点云的筛选一、点云的分段 加载案例点云数据,如下图所示: 选中图层点云,点击工具栏中的【分割】工具。 点击【激活线状选择】工具: 在需要裁剪的点云上绘制现状裁剪范…

使用免费的SSL证书将nginx配置的普通网站修改为HTTPS网站

一、需求说明 已经在Centos8系统中使用nginx搭建了网站;但是该网站没有实现HTTPS协议不安全;现需要将网站升级为HTTPS站点。 Linux环境对Nginx开源版源码下载、编译、安装、开机自启https://blog.csdn.net/xiaochenXIHUA/article/details/130265983?spm=1001.2014.3001.5501

chatgpt赋能python:Python交易接口简介

Python交易接口简介 Python作为一种高级编程语言&#xff0c;被广泛用于各种不同的领域&#xff0c;其中包括金融市场交易。Python交易接口提供了一种优雅而简单的方式&#xff0c;使得交易者能够方便地执行自己的交易策略。 什么是Python交易接口&#xff1f; Python交易接…

Effective第三版 中英 | 第2章 创建和销毁对象 | 考虑静态工厂方法而不是构造函数

文章目录 Effective第三版第2章 创建和销毁对象前言考虑静态工厂方法而不是构造函数 Effective第三版 第2章 创建和销毁对象 前言 大家好&#xff0c;这里是 Rocky 编程日记 &#xff0c;喜欢后端架构及中间件源码&#xff0c;目前正在阅读 effective-java 书籍。同时也把自己…

基于SSM的人才招聘网站

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

模拟实现库函数:strcpy

目录 通过cplusplus网站了解函数功能&#xff1a; 断言assert的使用&#xff1a; 关于const&#xff1a; 本篇你最应该了解的内容&#xff1a; 通过cplusplus网站了解函数功能&#xff1a; 要模拟实现库函数&#xff0c;首先我们需要了解这个函数的参数&#xff0c;函数的…

主机加固介绍

最近公司做服务器安全&#xff0c;开始在市场了解产品&#xff0c;对这一块算是短暂的研究了一段时间&#xff0c;有一点心得给大家分享一下。 主机加固 最近主机加固的概念被炒得火热&#xff0c;主机加固的功能也正在被致力于服务器安全的相关人士所关注。 那么究竟什么是主…