2-3树(2-3 Tree):原理、常见算法及其应用

目录

引言

2-3树的基本概念

常见算法

查找节点

插入节点

删除节点

2-3树的应用场景

1. 文件系统目录管理

应用原理

场景描述

2. 字典编码

应用原理

场景描述

总结

优势对比

自平衡特性

灵活的节点结构

高效的操作性能

简单的实现

广泛的应用场景

数据一致性


引言

2-3树(2-3 Tree)是一种自平衡的二叉树,它允许节点拥有一个或两个键值,并且每个内部节点可以有2或3个子节点。这种数据结构保证了树的高度在最坏情况下为O(logn),使得查找、插入和删除操作的时间复杂度均为)O(logn)。本文将从2-3树的基本概念出发,逐步介绍其原理、常见算法,并通过具体的Java代码示例来加深理解,最后探讨2-3树在算法中的实际应用场景,并总结2-3树的优势。

2-3树的基本概念

为了保证二叉树的平衡,提高树查找的效率,减少遍历的层级,我们允许一个结点保留多个键,并且链接的不止两条链

   

  2-结点:

  含有一个键和两条链,左链指向的键都小于该结点,右链指向的键都大于该结点

  3-结点:

  含有两个键和三条链,左链指向的键都小于该结点,右链指向的键都大于该结点,中链指向的的键介于该结点的两个键之间

特性

  • 树中每个节点最多有两个键值。
  • 每个节点至少有一个键值。
  • 对于每个节点,所有左子树的键值都小于该节点的键值。
  • 对于每个节点,所有右子树的键值都大于该节点的键值。
  • 对于包含两个键值的节点,中间子树的键值介于两个键值之间。

节点定义

class Node {int[] keys;Node[] children;boolean isLeaf;Node(int k1) {keys = new int[]{k1};children = new Node[2];isLeaf = true;}Node(int k1, int k2) {keys = new int[]{k1, k2};children = new Node[3];isLeaf = false;}
}

常见算法

public class TwoThreeTree {private Node root;public TwoThreeTree() {root = null;}// 插入新键值public void insert(int value) {root = insertRecursive(root, value);if (root.keys.length > 2) {// 如果根节点分裂,则创建新的根节点Node newRoot = new Node();newRoot.children[0] = root.children[0];newRoot.children[1] = new Node(root.keys[1]);newRoot.children[1].children[0] = root.children[1];newRoot.children[1].children[1] = root.children[2];newRoot.keys[0] = root.keys[1];root = newRoot;}}private Node insertRecursive(Node current, int value) {if (current == null) {if (value < current.keys[0]) {return new Node(value, current.keys[0]);} else {return new Node(current.keys[0], value);}}int i = 0;if (value < current.keys[0]) {current.children[i] = insertRecursive(current.children[i], value);} else if (current.keys.length == 2 && value > current.keys[0] && value < current.keys[1]) {current.children[i + 1] = insertRecursive(current.children[i + 1], value);} else {current.children[current.keys.length] = insertRecursive(current.children[current.keys.length], value);}if (current.children[i].keys.length == 2) {// 分裂节点int splitKey = current.children[i].keys[1];Node temp = new Node(current.children[i].keys[0]);current.children[i] = temp;current.keys[i] = splitKey;if (current.keys.length == 1) {current.keys[1] = current.children[i + 1].keys[0];current.children[i + 1] = new Node(current.children[i + 1].keys[1]);} else {Node newChild = new Node(current.children[i + 1].keys[0], splitKey);current.children[i + 1] = newChild;current.keys[i] = splitKey;}}return current;}
}

查找节点

2-3树的查找思路与二叉查找树相似,对于需要查找的键,从根结点开始遍历,小于往左,大于则往右,当找到3-结点时,若查找的键介于3-结点的两个键之间,则找中链接对应的结点,命中则返回。

  查找过程没命中时则继续递归查找子树。

  如图:查找键为H的结点,首先找根结点M,由于H<M,往下找左子树

   

  查找到左子树为3-结点,判断H>E并且H<J,则找中链结点的键

   

Java代码实现

public Node search(int value) {return searchRecursive(root, value);
}private Node searchRecursive(Node current, int value) {if (current == null) {return null;}if (value == current.keys[0]) {return current;} else if (current.keys.length == 2 && value == current.keys[1]) {return current;} else if (value < current.keys[0]) {return searchRecursive(current.children[0], value);} else if (current.keys.length == 2 && value > current.keys[0] && value < current.keys[1]) {return searchRecursive(current.children[1], value);} else {return searchRecursive(current.children[current.keys.length], value);}
}

插入节点

往2-3树中插入结点的思路和二叉树一致,首先进行查找,根据2-3树的特性将结点挂到合适的位置,保持树的平衡。由于2-3树既包含2-结点同时也包含3-结点,因此在插入时针对不同类型的结点:

  一、 往2-结点中插入新键

  插入K:往2-结点中插入新键时,我们可以保证树层级不变的基础上,将2-结点转化为3-结点

  二、 往只有一个3-结点中插入新键

  往3-根结点中插入S

  将中键提升为根结点,左右两键成为左右子树

  三、 往一个父结点为2-结点的3-结点中插入新键

  往该2-3树中插入元素Z

  将3-结点的中间元素往上提

  四、 往一个父结点为3-结点的3-结点中插入新键

  往该2-3树中插入元素D

  将插入后形成的3-结点往上提

  将形成的3-父节点再次取中间值往上提升一层

  五、 插入结点到根结点的路径上是3-结点

  往该2-3树中插入D

  将插入后形成的3-结点取中间值往上提

   

  将形成的3-根结点再次分解

   

Java代码实现

public class TwoThreeTree {private Node root;public TwoThreeTree() {root = null;}// 插入新键值public void insert(int value) {root = insertRecursive(root, value);if (root.keys.length > 2) {// 如果根节点分裂,则创建新的根节点Node newRoot = new Node();newRoot.children[0] = root.children[0];newRoot.children[1] = new Node(root.keys[1]);newRoot.children[1].children[0] = root.children[1];newRoot.children[1].children[1] = root.children[2];newRoot.keys[0] = root.keys[1];root = newRoot;}}private Node insertRecursive(Node current, int value) {if (current == null) {if (value < current.keys[0]) {return new Node(value, current.keys[0]);} else {return new Node(current.keys[0], value);}}int i = 0;if (value < current.keys[0]) {current.children[i] = insertRecursive(current.children[i], value);} else if (current.keys.length == 2 && value > current.keys[0] && value < current.keys[1]) {current.children[i + 1] = insertRecursive(current.children[i + 1], value);} else {current.children[current.keys.length] = insertRecursive(current.children[current.keys.length], value);}if (current.children[i].keys.length == 2) {// 分裂节点int splitKey = current.children[i].keys[1];Node temp = new Node(current.children[i].keys[0]);current.children[i] = temp;current.keys[i] = splitKey;if (current.keys.length == 1) {current.keys[1] = current.children[i + 1].keys[0];current.children[i + 1] = new Node(current.children[i + 1].keys[1]);} else {Node newChild = new Node(current.children[i + 1].keys[0], splitKey);current.children[i + 1] = newChild;current.keys[i] = splitKey;}}return current;}
}

删除节点

删除节点时需要考虑三种情况:

  1. 节点是一个叶节点。
  2. 节点是一个2-节点。
  3. 节点是一个3-节点。

Java代码实现

public void delete(int value) {root = deleteRecursive(root, value);if (root.keys.length == 0) {// 如果根节点为空,则删除根节点root = root.children[0];}
}private Node deleteRecursive(Node current, int value) {if (current == null) {return null;}if (value == current.keys[0]) {// 删除2-节点或3-节点的第一个键值if (current.isLeaf) {// 删除叶节点if (current.keys.length == 2) {current.keys[0] = current.keys[1];current.keys[1] = 0;}current.keys[0] = 0;return current;} else {// 替换键值后删除int replacement = findMin(current.children[0]);current.keys[0] = replacement;current.children[0] = deleteRecursive(current.children[0], replacement);return current;}} else if (current.keys.length == 2 && value == current.keys[1]) {// 删除3-节点的第二个键值if (current.isLeaf) {current.keys[1] = 0;return current;} else {int replacement = findMin(current.children[2]);current.keys[1] = replacement;current.children[2] = deleteRecursive(current.children[2], replacement);return current;}} else if (value < current.keys[0]) {current.children[0] = deleteRecursive(current.children[0], value);return current;} else if (current.keys.length == 2 && value > current.keys[0] && value < current.keys[1]) {current.children[1] = deleteRecursive(current.children[1], value);return current;} else {current.children[current.keys.length] = deleteRecursive(current.children[current.keys.length], value);return current;}
}private int findMin(Node node) {while (node.children[0] != null) {node = node.children[0];}return node.keys[0];
}

2-3树的应用场景

1. 文件系统目录管理

2-3树可以用于文件系统的目录管理,以加速文件查找速度。

应用原理
  • 文件系统的目录项作为键值存储在2-3树的节点中。
  • 查找文件时,根据文件名在树中查找对应的文件元数据。
  • 插入和删除文件时,自动调整树的结构,保持较低的高度。
场景描述

在文件系统中,为了快速查找文件,可以使用2-3树来组织目录结构。当用户访问文件时,根据文件名可以在树中迅速定位到文件的位置,而不需要遍历整个目录结构。2-3树的自平衡特性确保了目录结构的高效性,即使在频繁的文件操作下也能保持良好的性能。

2. 字典编码

在字典编码中,2-3树可以用来存储单词及其解释。

应用原理
  • 单词作为键值存储在2-3树的节点中。
  • 单词的解释和其他相关信息作为键值对应的数据存储。
  • 用户在查询单词时,根据单词名称快速查找、插入或更新相应的信息。
场景描述

在开发电子词典或在线词典时,可以利用2-3树来管理单词及其解释。用户在查询单词时,可以通过2-3树快速定位到所需的信息。此外,2-3树还可以用于实现自动补全等功能,提高用户体验。2-3树的自平衡特性使得在处理大量的词汇时依然保持高效。

总结

2-3树作为一种高效的数据结构,在计算机科学中有广泛的应用。通过允许节点拥有一个或两个键值来保持树的平衡,2-3树在最坏的情况下也能够保证操作的时间复杂度为O(logn)。掌握2-3树的概念和相关算法对于深入理解计算机科学的核心知识至关重要。

优势对比

自平衡特性
  • 2-3树:通过分裂和合并操作来保持树的平衡,确保了树的高度在最坏情况下为O(logn),使得查找、插入和删除操作的时间复杂度均为O(logn)。
灵活的节点结构
  • 2-3树:允许节点拥有一个或两个键值,提供了比传统二叉树更多的灵活性,特别是在处理大量数据时。
高效的操作性能
  • 2-3树:由于自平衡特性,它在频繁的插入和删除操作下依然保持良好的性能。
简单的实现
  • 2-3树:相比其他复杂的自平衡树(如红黑树),2-3树的实现较为简单,容易理解和实现。
广泛的应用场景
  • 2-3树:适用于多种应用场景,如文件系统目录管理、字典编码、网络路由表等,展示了其在实际应用中的强大功能。
数据一致性
  • 2-3树:通过分裂和合并操作来维护数据的一致性,确保了在并发操作下的数据完整性。

综上所述,2-3树以其自平衡特性、灵活的节点结构、高效的性能、简单的实现以及广泛的应用场景,在许多实际应用中表现出色。选择2-3树作为数据结构可以带来诸多好处,特别是在需要高效操作和数据一致性的场景下。

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

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

相关文章

遥感图像分割

遥感图像分割是一种应用于遥感图像的计算机视觉技术&#xff0c;用于将图像划分为不同的区域&#xff0c;每个区域代表地表的不同特征&#xff0c;如水体、森林、城市区域等。这种分割帮助我们更好地理解和分析地球表面的变化&#xff0c;对于环境监测、城市规划、农业、灾害管…

AR技术在电商行业的应用及优势有哪些?

AR&#xff08;增强现实&#xff09;技术在电商行业的应用广泛且深入&#xff0c;为消费者带来了全新的购物体验&#xff0c;同时也为商家带来了诸多优势。以下是AR技术在电商行业的主要应用场景及其优势&#xff1a; 一、应用场景 1、虚拟商品展示与试用 家具AR摆放&#x…

济南站活动回顾|IvorySQL中的Oracle XML函数使用示例及技术实现原理

近日&#xff0c;由中国开源软件推进联盟PG分会 & 齐鲁软件园联合发起的“PostgreSQL技术峰会济南站”在齐鲁开源社举办。瀚高股份IvorySQL作为合作伙伴受邀参加此次活动。 瀚高股份IvorySQL技术工程师 向逍 带来「IvorySQL中的Oracle XML函数兼容」的议题分享。在演讲中&a…

前端vue-form表单的验证

form表单验证的完整步骤

二叉树的中序遍历(java)

概述 关于二叉树&#xff0c;我们都不陌生&#xff0c;许多基于递归的问题发起点都是一个二叉树的root节点。对于各种二叉树的问题&#xff0c;我们也是通过dfs进行求解。例如求二叉树的深度、最近公共祖先等 算法分析 关于二叉树的中序遍历&#xff0c;我们都知道应该先访…

【C++单调队列】1438. 绝对差不超过限制的最长连续子数组|1672

本文时间知识点 C队列、双向队列 LeetCode1438. 绝对差不超过限制的最长连续子数组 给你一个整数数组 nums &#xff0c;和一个表示限制的整数 limit&#xff0c;请你返回最长连续子数组的长度&#xff0c;该子数组中的任意两个元素之间的绝对差必须小于或者等于 limit 。 如…

Flume实战--Flume中的选择器、自动容灾(故障转移)、负载均衡的详解与操作

本文详细介绍了Apache Flume的关键特性&#xff0c;包括选择器、拦截器、故障转移和负载均衡。选择器负责将数据分发到多个Channel&#xff0c;拦截器用于修改或丢弃Event。故障转移机制能够在Sink故障时自动切换&#xff0c;而负载均衡则在多个Sink间分配负载。文章还提供了自…

CANoe_DBC能够打开但是无法使用“BusType”

解决DBC文件在CAPL中调用问题&#xff1a;从CANdb到CAPL的顺畅过渡 在汽车电子和嵌入式系统开发中&#xff0c;DBC&#xff08;Database CAN&#xff09;文件作为描述CAN&#xff08;Controller Area Network&#xff09;通信协议的重要工具&#xff0c;广泛应用于网络设计、测…

工作日志:ruoyi-vue-plus echarts根据窗口大小变化

1、echarts根据窗口大小变化。 onMounted(() > {// 折线图type EChartsOption echarts.EChartsOption;var chartDom document.getElementById(chartDom)!;var myChart echarts.init(chartDom);var option: EChartsOption;option {grid: {left: 35,top: 10,bottom: 30,r…

jenkins部署Maven和NodeJS项目

在 Java 项目开发中&#xff0c;项目的编译、测试、打包等是比较繁琐的&#xff0c;属于重复劳动的工作&#xff0c;浪费人力和时间成本。以往开发项目时&#xff0c;程序员往往需要花较多的精力在引用 jar 包搭建项目环境上&#xff0c;跨部门甚至跨人员之间的项目结构都有可能…

1.8 软件业务测试

欢迎大家订阅【软件测试】 专栏&#xff0c;开启你的软件测试学习之旅&#xff01; 文章目录 前言1 概述2 方法3 测试策略4 案例分析 前言 在软件开发生命周期中&#xff0c;业务测试扮演着至关重要的角色。本文详细讲解了业务测试的定义、目的、方法以及测试策略。 本篇文章参…

C++队列、双向队列

前言 C算法与数据结构 打开打包代码的方法兼述单元测试 队列 队列&#xff08;Queue&#xff09;是一种基本的线性数据结构&#xff0c;它遵循先进先出&#xff08;First In First Out, FIFO&#xff09;的原则。这意味着最先被添加到队列中的元素将会是最先被移除的。和生活…

命令回显echo

命令回显 通常&#xff0c;make在执行命令行之前会把要执行的命令行进行输出。我们称之为“回显”&#xff0c;就好像我们输入命令执行一样。 如果要执行的命令行以字符“”开始&#xff0c;则make在执行时这个命令就不会被回显。典型的用法是我们在使用“echo”命令输出一些信…

Github 2024-09-29 php开源项目日报 Top10

根据Github Trendings的统计,今日(2024-09-29统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量PHP项目10Blade项目1Java项目1ASP项目1Coolify: 开源自助云平台 创建周期:1112 天开发语言:PHP, Blade协议类型:Apache License 2.0Star数量…

Java多线程几个哈希表的区别

HashMap 首先HashMap肯定是不行的,并没有加解锁操作,一旦多线程同时写的话,直接就会发生覆盖之类的操作 排除HashMap先,主要对比HashTable和ConcurrentHashMap HashTable vs ConcurrentHashMap 1. 加锁粒度不同 HashTable HashTable是对整个哈希表进行加锁操作,任何增删改查操…

数据结构串的kmp相关(求next和nextval)

傻瓜版&#xff0c;用来演示手算过程&#xff0c;个人理解用的&#xff0c;仅供参考。

CICD Jenkins实现Pipline

一、安装 1、由于 Jenkins 是基于 Java 的&#xff0c;首先需要确保你的系统中安装了 Java。推荐使用 OpenJDK 11。可以通过以下命令安装&#xff1a; apt update apt install openjdk-11-jdk2、在安装 Jenkins 之前&#xff0c;你需要将其仓库添加到你的系统中。首先&#x…

DotNetty ChannelRead接收数据为null

问题&#xff1a;C#使用Dotnetty和Java netty服务器通讯&#xff0c;结果能正确发送数据到服务器&#xff0c;却始终接收不到服务器返回的数据。 解决&#xff1a;一定一定要注意服务器和客户端使用的编码一定要完全一样才行 我先前在客户端添加了StringDecoder,服务器却没有…

【Spring Boot 入门一】构建你的第一个Spring Boot应用

一、引言 在当今的软件开发领域&#xff0c;Java一直占据着重要的地位。而Spring Boot作为Spring框架的延伸&#xff0c;为Java开发者提供了一种更加便捷、高效的开发方式。它简化了Spring应用的搭建和配置过程&#xff0c;让开发者能够专注于业务逻辑的实现。无论是构建小型的…

8.12 矢量图层面要素单一符号使用八(随机标记填充)

8.12 矢量图层面要素单一符号使用八(随机标记填充)_qgis随机填充-CSDN博客 目录 前言 随机标记填充&#xff08;Random Marker Fill&#xff09; QGis设置面符号为随机标记填充&#xff08;Random Marker Fill&#xff09; 二次开发代码实现随机标记填充&#xff08;Rando…