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

news2024/9/30 7:33:52

目录

引言

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.coloradmin.cn/o/2179061.html

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

相关文章

【django】django项目使用https访问+ssl证书

目录 一、安装 django-sslserver 二、配置settings 三、启动项目测试 四、使用ssl证书 4.1 安装cryptography 4.2 生成证书代码 4.3 将生成的证书放到django项目根目录下 五、使用证书启动项目 5.1 本地测试启动 5.2 生产启动 六、生成docker镜像的dockerfile 七、…

《程序猿之Redis缓存实战 · Redis 与数据库一致性》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

多模态人像编辑:PortraitGen将2D肖像视频提升到4D 高斯场

这篇文章《Portrait Video Editing Empowered by Multimodal Generative Priors》&#xff0c;作者是来自中国科学技术大学。文章介绍了一种名为PortraitGen的肖像视频编辑方法&#xff0c;它使用多模态生成先验来实现一致性和富有表现力的风格化编辑。 文章地址&#xff1a;P…

electron教程(三)窗口设置

在main.js文件中&#xff0c;创建窗口时会设置窗口的大小&#xff0c;其实还有很多其他属性&#xff0c;可以根据实际需求选择设置&#xff0c;但部分属性存在局限性&#xff0c;官网也有明确告知&#xff1a;自定义窗口 | Electron (electronjs.org) 项目文件目录如下&#x…

6.1 微服务 服务发现 架构模式分类 应用实践

微服务 服务发现 架构模式分类 应用实践 目录概述需求&#xff1a; 设计思路实现思路分析1.类型-客户端发现2.类型-服务端服务发现3.工具-Eureka4.工具-Consul5.工具-zookper服务发现的挑战服务发现的最佳实践 参考资料和推荐阅读 Survive by day and develop by night. talk …

【三步 完全离线搭建 openwebui 】

完全离线linux 版open webui 的搭建 1.在具有网络连接的环境中下载whl 在有网络的环境&#xff0c;使用pip download可以保存所有的依赖包,可以使用-i 指定清华的镜像源加速下载速度。 # 命令&#xff1a; pip download <package_name> --only-binary:all: --wheel --…

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

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

前端考核总结

目录 JavaScript的基本数据类型有哪些&#xff1f;JavaScript中数据类型的检测方法JavaScript如何判断对象中的属性存在自身还是原型链上flex布局HTML5新标签Vue的基本概念Vue生命周期JavaScript中闭包的基本概念防抖节流双等号与三等号的区别显式转换 JavaScript的基本数据类型…

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

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

【零基础入门产品经理】学习准备篇 | 需要学一些什么呢?

前言&#xff1a; 零实习转行产品经理经验分享01-学习准备篇_哔哩哔哩_bilibili 该篇内容主要是对bilibili这个视频的观后笔记~谢谢美丽滴up主友情分享。 全文摘要&#xff1a;如何在0实习且没有任何产品相关经验下&#xff0c;如何上岸产品经理~ 目录 一、想清楚为什么…

Redis 基础数据改造

优质博文&#xff1a;IT-BLOG-CN 一、服务背景 基础数据查询服务&#xff1a;提供航司、机场、票台、城市等基础数据信息。 痛点一&#xff1a;因为基础数据不属于频繁更新的数据&#xff0c;所以每个应用都有自己和缓存&#xff0c;当基础数据更新后&#xff0c;各个应用缓存…

webGL入门(五)绘制多边形

代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><scri…

ARM 服务器上安装 OpenEuler (欧拉)

系统介绍 在 2019 年 7 月 19 日&#xff0c;华为宣布要在年底正式开源 openEuler 操作系统&#xff1b;在半年后的 12 月 31 日&#xff0c;华为正式开源了 openEuler 操作系统&#xff0c;邀请社区开发者共同来贡献。 一年后&#xff0c;截止到 2020 年12 月 25日&#xff…

计算机毕业设计 Java教务管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

[Cocoa]_[初级]_[使用NSNotificationCenter作为目标观察者实现时需要注意的事项]

场景 在开发Cocoa程序时&#xff0c;由于界面是用Objective-C写的。无法使用C的目标观察者[1]类。如果是使用第二种方案2[2],那么也需要增加一个代理类。那么有没有更省事的办法&#xff1f; 说明 开发界面的时候&#xff0c;经常是需要在子界面里传递数据给主界面&#xff0…

PIKACHU | PIKACHU 靶场 XSS 后台配置

关注这个靶场的其他相关笔记&#xff1a;PIKACHU —— 靶场笔记合集-CSDN博客 PIKACHU 自带了一个 XSS 平台&#xff0c;可以辅助我们完成 XSS 攻击&#xff0c;但是该后台需要配置数据库以后才能使用。本教程&#xff0c;就是教大家如何配置 PIKACHU XSS 平台的。 PIKACHU XS…

vulhub weblogic 靶场攻略

一&#xff1a;WebLogic 后台弱⼝令GetShell&#xff08;weak_password &#xff09; 漏洞描述 通过弱⼝令进⼊后台界⾯ , 上传部署war包 , getshell 影响范围 全版本&#xff08;前提后台存在弱⼝令&#xff09; 环境搭建 cd vulhub-master/weblogic/weak_password doc…

【STM32开发环境搭建】-4-在STM32CubeMX中新增Keil(MDK-ARM) 5的工程目录(包含指定路径的C和H文件)

案例背景&#xff1a; 由于Keil(MDK-ARM)5工程&#xff1a;DEMO_STM32F030C8T6.uvprojx是由STM32CubeMX工具生成的&#xff0c;如果我们在Keil工程中手动添加了一些c文件和h文件的Include Path包含路径&#xff0c;会在STM32CubeMX下一次生成uvprojx文件时&#xff0c;被删除&…

纯软件小白 学习DDR5

问题 1.你知道当你打开游戏加载存档时候计算机是在做什么吗&#xff1f; 由于你的CPU只有在数据被加载到DRAM的时候才可以工作&#xff0c;所以当你需要用数据的时候&#xff0c;数据会从SSD复制到DRAM这一过程需要时间&#xff0c;所以会有加载&#xff08;所有3D模型、纹理…

【从零开始实现stm32无刷电机FOC】【实践】【7.1/7 硬件设计】

目录 stm32电路磁编码器电路电机驱动电路电流采样电路电机选择本文示例硬件说明 为了承载和验证本文的FOC代码工程&#xff0c;本节设计了一个简易的三相无刷电机 硬件套件&#xff0c;主控采用非常常用的stm32f103c8t6单片机&#xff0c;电机编码器采用MT6701&#xff0c;电机…