【开卷数据结构 】2-3树

简介: 【开卷数据结构 】2-3树

2-3树 的定义


Q:什么是二叉排序树


A:二叉排序树或者是一棵空树,或者是具有如下性质的二叉树


1)若它的左子树不空,则 左子树 上所有结点的值 均小于 它的根结点的值

2)若它的右子树不空,则 右子树 上所有结点的值 均大于 它的根结点的值

3)左、右子树也分别是一棵二叉排序树

通过引入结点度大于 2 的排序树,可以得到一种插入算法和删除算法都比二叉排序树简单的树结构,且这些算法的时间复杂性仍是 O(logn) 。这种树结构称为2-3树。2-3树的名字反映出 2-3树 具有如下性质: 一棵 2-3树 中的每个内部结点的度或者是2,或者是3。其中度为2的结点称为 2结点,度为3的结点称为 3结点。


Q:什么是2-3树


A:一棵 2-3树(2-3 tree) 是一棵查找树,该查找树或者为空,或者满足如下性质:


1)每个内部结点或者是一个 2结点,或者是一个 3结点。一个 2结点 存放一个元素,而一个 3结点 存放两个元素。

2)令左孩子和中孩子表示 2结点 的两个儿子。以左孩子为根的子树中所有结点的关键字值都小于该结点元素的关键字,而以中孩子为根的子树中所有结点的关键字值都大于该结点元素的关键字。

3)令左孩子,中孩子和右孩子表示 3结点 的三个儿子。且有左孩子结点元素的关键字小于右孩子结点元素的关键字;以左孩子为根的子树中所有结点的关键字值都小于右孩子结点元素的关键字;以中孩子为根的子树中所有结点的关键字值都大于左孩子结点元素的关键字而小于右孩子结点元素的关键字;而以右孩子为根的子树中所有结点的关键字值都大于右孩子结点元素的关键字。

4)所有叶子点都在树的同一层。

一棵2-3树

4fb0616a58bdeb80454faa64a8d60006_98256db8bf9b44afa7b36f5c8402f652.png

2-3树 的结点


2-3树的结点如下所示:


struct two_three {
    element data_l, data_r;
    two_three *left_child;
    two_three *middle_child;
    two_three *right_child;
};

假设任何元素的关键字值都不等于 INT_MAX ,并且 2结点 的 data_r.key=INT_MAX ,其唯一的元素存放在 data_l 域中,其 left_child 域和 middle_child 域分别指向它的两个儿子,将right_child 域置为 NULL。


2-3树 的查找


2-3 树 的查找类似二叉平衡树的查找过程。


假设我们在 2-3树 t 上查找元素的关键字值等于 x 的结点,假定关键字值为整数。在进行查找时,一共有四种情况:


1)x 小于第一个关键字。

2)x 位于第一个关键字和第二个关键字之间。

3)x 大于第二个关键字。

4)x 等于结点中某个关键字


参考代码


two_three *search23(two_three *t, element x)
{
    while(t) {
        if (x < t->data_l) {
            t = t->left_child;
        } else if (x > t->data_l && x < t->data_r) {
            t = t->middle_child;
        } else if (x > t->data_r) {
            t = t->right_child;
        } else {
            return t;
        }
    }
    return NULL;
}


通过分析,可以发现 while 循环的重复次数不会超过 2-3树的高度。因此,如果树 t 包含 n 个结点,则函数 search23 的时间复杂度为O(logn)。


2-3树 的插入


若原 2-3树 为空,则直接插入结点,若原 2-3树 非空,在插入之前需要对带插入的结点进行一次查找操作。若树中已经有此结点则不进行插入操作。若没有此结点,进行插入操作,此时插入有两种情况。


1)向 2 结点的树中插入新结点

2)向 3 结点的树中插入新结点


向 2 结点中插入新结点


此时叶子结点里面就只有一个元素,那么插入之后正好两个元素,符合 2-3 树 的要求,直接插入。


插入关键字值为 30 的元素。


898c2c0504a4c89b670f0a6bdf3eecf9_37df8692f4584601b8f033d6c9445f29.png


向 3 结点中插入新结点


此时这个叶子结点里面已经有两个元素了,插入后就有三个元素,那么就把叶子结点中间的元素传给父结点,自己分裂成两个结点。再看父结点,如果接收之后父结点也变成了 3结点,那么就一直重复这个过程,直到根结点。


插入关键字值为 35 的元素。


c53a3242a6007a059df19add457c9b3d_20131826bec5466bbbdcdf7edcc6dd8e.png


插入关键字值为 20 的元素。


091b1386d96b729c17c0d87c13ba2d55_2de192f4302f48589dd4fce68c7b9089.png


插入关键字值为 25 的元素。


3575325e89aa91aae503af9a49b3076d_2e431b397c334d33a0a24d466607757b.png


参考代码:

two_three_ptr search23(two_three_ptr t,element x)
{
  while(t){
  switch(compare(x,t)){// 比较大小,返回情况1,2,3,4
    case 1:t=t->left_child;
      break;
    case 2:t=t->middle_child;
      break;
    case 3:t=t->right_child;
      break;
    case 1:return t;          
  } 
  } 
  return NULL:
} 
void insert23 (two_three_ptr *t,element y)
{
  two_three_ptr q,p,temp;
  if(!(*t))//如果树为空
  new_root(t,y,NULL);
  else{
  p=find_node(*t,y);
  if(!p){
    cout<<"该结点在树中"<<endl;
    exit(1); 
  }
  q=NULL;
  for(;;){
    if(p->data_r.key==INT_MAX){//二结点 
    put_in(&p,y,q);
    break;
    }
    else{//三结点    
    split(p,&y,&q);
    if(p==*t){
      new_root(t,y,q);
      break;
    }
    else{
      p=s->pop();
    }
    }
  }
  }
}

该函数调用了以下几个函数:


1)new_root:输入新根的左子树,新根存放的单个元素和新根的中子树,返回指向新根的指针。

void new_root(two_three *&left, element y, two_three *middle)
{
    two_three *p = new two_three;
    p->left_child = left;
    p->middle_child = middle;
    p->right_child = NULL;
    p->data_l = y;
    p->data_r = MAX;
    left = p;
}

2)find_node:在一棵非空的 2-3树 t 中查找关键字值为 y. key 的元素。如果 t 中存在关键字值为 y. key 的元素,则返回 NULL,否则返回查找过程中遇到的叶结点 p。此外, find_node还会创建一个全局的栈结构,以便得到从叶结点 p 到根结点 t 的路径上所有 p 的祖先结点。这个栈结构依次保存从最近祖先结点到最远祖先结点的结点表。由于在拆分结点时需要访问拆分结点的父结点,所以要使用这个结点表。

two_three *find_node(two_three *t, element x, stack *&s)
{
    while(t) {
        s->push(t);
        if (x < t->data_l && t->left_child) {
            t = t->left_child;
        } else if (x > t->data_l && x < t->data_r && t->middle_child) {
            t = t->middle_child;
        } else if (x > t->data_r && t->right_child) {
            t = t->right_child;
        } else if (x == t->data_l || x == t->data_r) {
            return NULL;
        } else {
            return s->pop();
        }
    }
}


3)put_in:该函数将元素 y 插到只包含一个元素的结点 p 中,并将子树 q 直接放在 y 的右侧。因此,如果 y 变为 data_l,则 q 就变为 middle_child ,原来的 data_l 和 middle child 分别右移变成 data_r 和 right_child。如果 y 变为 data_r,则 q 就变成right_child。

void put_in(two_three *p, element x, two_three *q)
{
    if(p->data_l < x) {
        p->data_r = x;
        p->right_child = q;
    } else {
        p->right_child = p->middle_child;
        p->middle_child = q;
        p->data_r = p->data_l;
        p->data_l = x;
    }
}

4)split:该函数的输入为包含两个元素的结点 p,其功能是创建一个新结点 q。新结点 q 将存放 p 中原来的两个元素与元素 y 三者中关键字值最大的元素。三者中关键字值最小的元素将成为 p 中的唯一元素。p 中原有的三个子树指针和指针 q 分别存放在 p 结点和新建结点的四个儿子域中。函数返回时,y 为关键字值处于中间大小的元素,q 为指向新建结点的指针。

void split(two_three *p, element &x, two_three *&q)
{
    element min, max;
    if(x < p->data_l) {
        min = x;
        max = p->data_r;
        x = p->data_l;
    } else if (x > p->data_r) {
        min = p->data_l;
        max = x;
        x = p->data_r;
    } else {
        min = p->data_l;
        max = p->data_r;
    }
    two_three *n = new two_three;
    n->data_l = max;
    n->data_r = MAX;
    n->left_child = p->right_child;
    n->middle_child = q;
    n->right_child = NULL;
    p->data_l = min;
    p->data_r = MAX;
    p->right_child = NULL;
    q = n;
}


2-3树 的删除


在删除之前需要对要删除的结点进行一次查找操作。若树中没有此结点则不进行删除操作。


如果要删除的元素不在叶子结点中,可以用一个叶子结点中的适当元素与待删除元素进行交换,从而将该删除操作转换成在叶结点上的删除操作。一般情况下,可以用被删除元素左子树中的关键字值最大的元素或者右子树中关键字值最小的元素与该元素进行交换。


如果要删除的叶子结点是 3结点 ,直接把元素删除了就可以了,不会破坏2-3 B树的任何性质。如果要删除的叶子结点是 2结点 ,直接删除的话就不符合 2-3树 的性质了。类似于平衡二叉树(AVL),此时就需要进行旋转或合并操作了。


根据要删除结点 p 是父结点 f 的左孩子,中间孩子还是右孩子,旋转,合并可以分为六种情况。


1.p是r的左结点旋转


7edddc018b36309691bb5b03ae656041_361b01eefe5945bf954a3a45ab5e2909.png


2.p是r的中结点旋转


7a0bf0bf0c3f3e7c92a61f65ab35e384_b6eaf483ef3242b3a09604e1510b1448.png


3.p是r的右结点旋转


93e23a515711b58424fa443cc1dd44ee_dc5bf2e3fcf644f09990c8ea40667a42.png


4.p是r的左结点合并


65c78d53ae2bbf392ef1ca4fff58d4b1_8410ec0dbba54777991ad51ec7e749ae.png

中结点和右结点的合并也差不多,这里就不在详细描述了。


相关文章
|
2月前
|
算法
数据结构之博弈树搜索(深度优先搜索)
本文介绍了使用深度优先搜索(DFS)算法在二叉树中执行遍历及构建链表的过程。首先定义了二叉树节点`TreeNode`和链表节点`ListNode`的结构体。通过递归函数`dfs`实现了二叉树的深度优先遍历,按预序(根、左、右)输出节点值。接着,通过`buildLinkedList`函数根据DFS遍历的顺序构建了一个单链表,展示了如何将树结构转换为线性结构。最后,讨论了此算法的优点,如实现简单和内存效率高,同时也指出了潜在的内存管理问题,并分析了算法的时间复杂度。
65 0
|
5天前
|
存储 C++
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
【数据结构——树】哈夫曼树(头歌实践教学平台习题)【合集】目录 任务描述 相关知识 测试说明 我的通关代码: 测试结果:任务描述 本关任务:编写一个程序构建哈夫曼树和生成哈夫曼编码。 相关知识 为了完成本关任务,你需要掌握: 1.如何构建哈夫曼树, 2.如何生成哈夫曼编码。 测试说明 平台会对你编写的代码进行测试: 测试输入: 1192677541518462450242195190181174157138124123 (用户分别输入所列单词的频度) 预
37 14
【C++数据结构——树】哈夫曼树(头歌实践教学平台习题) 【合集】
|
5天前
|
Java C++
【C++数据结构——树】二叉树的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现二叉树的基本运算。​ 相关知识 创建二叉树 销毁二叉树 查找结点 求二叉树的高度 输出二叉树 //二叉树节点结构体定义 structTreeNode{ intval; TreeNode*left; TreeNode*right; TreeNode(intx):val(x),left(NULL),right(NULL){} }; 创建二叉树 //创建二叉树函数(简单示例,手动构建) TreeNode*create
31 12
|
5天前
|
C++
【C++数据结构——树】二叉树的性质(头歌实践教学平台习题)【合集】
本文档介绍了如何根据二叉树的括号表示串创建二叉树,并计算其结点个数、叶子结点个数、某结点的层次和二叉树的宽度。主要内容包括: 1. **定义二叉树节点结构体**:定义了包含节点值、左子节点指针和右子节点指针的结构体。 2. **实现构建二叉树的函数**:通过解析括号表示串,递归地构建二叉树的各个节点及其子树。 3. **使用示例**:展示了如何调用 `buildTree` 函数构建二叉树并进行简单验证。 4. **计算二叉树属性**: - 计算二叉树节点个数。 - 计算二叉树叶子节点个数。 - 计算某节点的层次。 - 计算二叉树的宽度。 最后,提供了测试说明及通关代
31 10
|
5天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
23 2
|
3月前
|
存储 算法 搜索推荐
探索常见数据结构:数组、链表、栈、队列、树和图
探索常见数据结构:数组、链表、栈、队列、树和图
131 64
|
2月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
80 5
|
2月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
117 16
|
2月前
|
算法
数据结构之文件系统模拟(树数据结构)
本文介绍了文件系统模拟及其核心概念,包括树状数据结构、节点结构、文件系统类和相关操作。通过构建虚拟环境,模拟文件的创建、删除、移动、搜索等操作,展示了文件系统的基本功能和性能。代码示例演示了这些操作的具体实现,包括文件和目录的创建、移动和删除。文章还讨论了该算法的优势和局限性,如灵活性高但节点移除效率低等问题。
72 0
|
3月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
37 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)