数据结构:树表查找


禁止码迷,布布扣,豌豆代理,码农教程,爱码网等第三方爬虫网站爬取!

二叉排序树

什么是二叉排序树

对于线性查找而言,当查找的数据发生了变化,进行修改的开销是很大的,因此线性查找适合静态查找。对于动态查找而言,用树结构来制作查找表会更为合适,因为动态地操作树结构的结点,开销远比顺序表小得多。
所谓二叉搜索树,也可以称之为二叉排序树,就是基于树结构建立的查找表。对于二叉排序树需要满足一下 3 个条件:

  1. 左子树为空树或所有的结点值均小于其根结点的值;
  2. 右子树为空树或所有的结点值均大于其根结点的值;
  3. 左右子树也统统都是二叉排序树。

同时由于本质上是二叉树,且满足以上性质,因此对一个二叉排序树进行中序遍历,将会得到一个有序序列。例如如图所示二叉排序树:

进行中序遍历,不难得到有序序列:1、2、3、4、5、6、7。
接下来我们也用 ASL 度量一下这个二叉排序树:

ASL(成功):(1 + 2 × 2 + 3 × 3 + 4) ÷ 6 = 3
ASL(失败):(3 + 4 × 5 + 5 × 2) ÷ 8 = 33/8

二叉排序树结构体定义

二叉排序树本身就是二叉树,因此和普通的二叉树的结构体定义相同。

typedef struct TNode
{
    ElementType Data;
    TNode *Left;
    TNode *Right;
}*BinTree;

二叉排序树的查找

查找指定数据

算法流程

二叉搜索树本身就是一个有序的结构,因此可以通过控制访问的子树来缩小查找范围。二叉树最好的处理方式就是递归,查找流程为:

  1. 若二叉排序树为空,则返回空指针;
  2. 若二叉排序树非空,则对于给出的值 key 与根结点的数据 data 进行对比,这将产生 3 个分支:
  • 若 key 等于 data,查找成功,返回结点的地址;
  • 若 key 小于 data,则递归进入左子树查找;
  • 若 key 大于 data,则递归进入右子树查找。

代码实现

BinTree SearchBST(BinTree T, KeyType key)
{
      if(T == NULL || key == T->Data)
            return T;
      else if(key < T->data)      //递归进入左子树查找
            return SearchBST(T->Left, key);
      else                        //递归进入右子树查找
            return SearchBST(T->Right, key);
}

查找最小值

由于对于一个结点而言,左子树的数据都小于根结点数据,因此可以直接沿着左子树向下挖掘,直到找到最左侧的叶子结点。

BinTree FindMin(BinTree BST)
{
    if (BST != NULL)
    {
        while (BST->Left)      //沿着左子树向下挖掘
        {
            BST = BST->Left;
        }
    }
    return BST;
}

查找最大值

由于对于一个结点而言,右子树的数据都大于根结点数据,因此可以直接沿着右子树向下挖掘,直到找到最右侧的叶子结点。

BinTree FindMax(BinTree BST)
{
    if (BST)
    {
        while (BST->Right)      //沿着右子树向下挖掘
        {
            BST = BST->Right;

        }
    }
    return BST;
}

插入与删除操作

插入数据

算法流程

  1. 若二叉排序树为空,则直接插入;
  2. 若二叉排序树非空,则对于给出的值 key 与根结点的数据 data 进行对比,这将产生 2 个分支:
  • 若 key 小于 data,则递归进入左子树插入;
  • 若 key 大于 data,则递归进入右子树插入。

代码实现

插入数据的算法的整体框架和查找操作的相同,需要通过递归找到插入位置。

bool InsertBST(BinTree BST, ElementType X)
{
    bool flag = true;

    if (BST == NULL)      //找到插入位置
    {
        BST = new TNode;
        BST->Data = X;
        BST->Left = NULL;
        BST->Right = NULL;
    }
    else
    {
        if (X < BST->Data)      //递归进入左子树插入
        {
            InsertBST(BST->Left, X);
        }
        else if(X > BST->Data)      //递归进入右子树插入
        {
            InsertBST(BST->Right, X);
        }
        else      //若结点已存在,无需插入
        {
            flag = false;
        }
    }
    return flag;
}

删除数据

算法流程

首先基于查找的框架,找到需要删除的结点。二叉搜索树的删除需要考虑更多的可能性,有以下 3 种分支:

  1. 结点的左右子树都为 NULL,直接修改其双亲结点;
  2. 结点仅有左子树或右子树,令子树的第一个结点来替代删除结点即可;
  3. 结点同时拥有左子树和右子树,则需要进行选择,一种做法就是选择右子树的最小结点或左子树的最大结点来取代被删除结点。

代码实现

BinTree DeleteBST(BinTree BST, ElementType X)
{
    BinTree ptr;

    if (BST == NULL)      //要删除的结点不存在
    {
        cout << "Not Found\n" << endl;
    }
    else       //要删除的结点存在
    {
        if (X == BST->Data)      //找到了要删除的结点
        {
            if (BST->Left && BST->Right)      //左右子树都存在
            {
                ptr = FindMin(BST->Right);      //选择右子树的最小结点
                BST->Data = ptr->Data;      //用右子树的最小结点取代被删除结点
                BST->Right = DeleteBST(BST->Right, BST->Data);      //删除原来的右子树的最小结点
            }
            else      //结点的子树存在空树
            {
                if (BST->Left == NULL)      //结点的左子树为空树(此时可能右结点也是空树)
                {
                    BST = BST->Right;
                }
                else                        //结点的右子树为空树
                {
                    BST = BST->Left;
                }
            }
        }
        else if (X < BST->Data)      //递归进入左子树查找删除结点
        {
            BST->Left = DeleteBST(BST->Left, X);
        }
        else                         //递归进入右子树查找删除结点
        {
            BST->Right = DeleteBST(BST->Right, X);
        }
    }
    return BST;
}

建立二叉排序树

模拟建立

按照整数序列 {4,5,7,2,1,3,6} 依次插入的顺序构造相应二叉排序树。
首先加入结点 4 作为根结点,接着加入结点 5。

加入结点 7。

加入结点 2。

加入结点 1。

加入结点 3。

加入结点 6,构造完毕。

代码实现

操作本质上还是查找操作,代码其实很简单,就是重复调用插入结点的函数。

void CreatBST(BinTree &T)
{      
      int count;      //插入的结点数
      int a_num;

      T = NULL;
      for(int i = 0, i < count, i++)
      {
            cin >> a_num;
            T = InsertBST(T, a_num)      //循环调用插入函数
      }
}

AVL 树

如果我们建立的二叉排序树是一个斜树,那么查找的过程与线性查找没有太大区别,效率依旧很恐怖。

我们所希望的是,二叉搜索树的每一个子树中,结点的分布都是呈现左右子树的结点分布尽量深度较小且个数相等,最好是优雅的“平衡状态”。二叉排序树的形状取决于数据集,当二叉树的高度越小、结构越合理,搜索的性能就越好,时间复杂度 O(log2n)。G. M. Adelson-Velsky 和 E. M. Landis 在1962年的论文《An algorithm for the organization of information》中发表了一种名为 AVL 树的数据结构,它就能很好地解决这个问题。
左转博客AVL 树

例题解析

左转博客——PTA习题解析——是否完全二叉搜索树、二叉搜索树的最近公共祖先

B-树

什么是 B-树

定义

B-树是一种针对外存进行查找的查找结构,可以用于磁盘管理系统中的目录管理,以及数据库系统中的索引组织等。因为如果说使用前面讲过的的查找方法,都以结点为单位进行查找,这样需要反复地进行内、外存的交换,开销很大,统称为内查找法。B-树的查找机理也是快速缩小查找范围,这与二叉搜索树类似。
m 阶 B-树在非空的情况下,需要满足以下性质:

  1. 树中每个结点至多有 m 棵子树;
  2. 若根结点不是叶子结点,则至少有 2 棵子树;
  3. 除根之外的所有非终端结点至少有 m/2 (向上取证)棵子树;
  4. 所有的叶子结点都出现在同一层次上,并且不带信息,通常称为失败结点失败结点;
  5. 所有的非终端结点最多有 m一1 个关键字。

特点

  • 平衡:所有叶子结点均在同一层次;
  • 有序:树中每个结点中的关键字都是宥序的,且某个关键字左子树中的关键字均小于它,其右子树中的关键字均大于它;
  • 多路:结点中出现多个关键字多个子树的特性。

插入

由于B-树中除根之外,所有非终端结点中的关键字个数必须大于等于 m / 2-l,因此每次插入一个关键宇不是在树中添加一个叶子结点,而是首先在最低层的某个非终端结点中添加一个关键字,若该结点的关键字个数不超过 m-1,则插人完成。否则表明结点已满,要产生结点的“分裂”,将此结点在同一层分成两个结点。
一般情况下结点分裂方法是:以中间关键字为界把结点一分为二,成为两个结点,并把中间关键字向上插入到双亲结点上,若双亲结点已满,则采用同样的方法继续分解。最坏的情况下,一直分解到树根结点,这时 B- 树高度增加。看例子吧,首先有如图 B- 树。

插入 30。

插入 26。

插入 85。

插入 7。

删除

m 阶B-树的删除操作是在树的某个结点中删除指定的关键字及其邻近的一个指针,删除后调整使得每个结点的关键字数目范围为 m/2-1 到 m 之间。删除记录后如果结点的关键字个数如果小于 m/2-l 则进行“合并结点的操作。除了删除记录,还删除该记录邻近的指针。
若该结点为下层的非终端结点,由于其指针域均为空,删除后不会影响其他结点,那就直接删除。若该结点不是最下层的非终端结点,邻近的指针则指向一棵子树,就不可直接删除,那就需要将要删除记录用其右左边邻近指针指向的子树中关键字圾小大的记录该记录必定在下层的非终端结点中替换。采取这种方法进行处理,无论删除的记录所在的结点是否为下层的非终端结点,都可以归结为在最下层的非终端结点的删除操作。看例子吧,首先有如图B-树。

删除 50。

删除 53。

删除 37。

B+树

B+树支持 2 种查找,一种是从最小关键字其的顺序查找,一种是从根结点开始的随机查找。它和B-树类似,具体的差别在于以下 3 个方面。

  1. 有 n 棵子树的结点中含有 n 个关键字;
  2. 所有的叶子结点中包含了全部关键字的信息,以及指向含这些关键字记录的指针,叶子结点本身依关键字的大小自小而则顶序链接;
  3. 所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中的最大或最小关键字。

参考资料

《大话数据结构》—— 程杰 著,清华大学出版社
《数据结构(C语言版|第二版)》—— 严蔚敏 李冬梅 吴伟民 编著,人民邮电出版社
AVL 树

posted @ 2020-05-27 03:32  乌漆WhiteMoon  阅读(1245)  评论(0编辑  收藏  举报