二叉排序树(二叉查找树)

一、引言

1、如果查找的数据集是普通的顺序存储,则插入操作可以将记录放在表的末端,给表记录数加一即可;对于删除操作可以是删除后,后面的记录向前移,也可以是把要删除的元素与最后一个元素互换位置,然后再删除最后一个元素,表记录数减一。因为整个数据集不要求有序,所以这样的数据集的存储结构对于插入和删除操作的效率还是不错的,但是正由于是无序的,所以会造成查找的效率很低。

2、如果查找的数据集是有序的线性表,并且是顺序存储的,则查找可以用折半、插值和斐波那契等查找算法来实现,但是因为有序,在插入和删除操作上,就需要耗费大量的时间了。

3、从以上两点可以看出分别采用无序的顺序存储结构和有序的顺序存储结构,它们本身对于删除、插入和查找操作是有利有弊的。那有没有一种可以使得插入和删除的效率不错,而且查找的效率还不错的存储结构和算法呢?答案是有的,把这种需要在查找时插入或删除的查找表称之为动态查找表,而一种称之为”二叉排序树”的数据结构就可以实现动态查找表的高效率。

二、二叉排序树

1、假如数据集是有序的且初始只有两个元素{62, 88},然后要插入元素58,如果是采用顺序存储结构,则元素62和88需要分别向后移动一个位置,然后在第一个元素位置插入元素58,保持有序。但是如果是采用二叉树结构,就可以避免移动元素,如下图:首先将第一个元素62定为二叉树的根结点(root),然后元素88因为比62大,所以让它成为根结点的右子树;接着元素58比62小,所以让它成为根结点的左子树。此时元素58的插入就并没有影响到原本是元素62和88。

《二叉排序树(二叉查找树)》

如果数据元素有{62, 88, 58, 47, 35, 73, 51, 99, 37, 93},那么根据以上的规则来创建这棵二叉树如下图所示,当对这棵二叉树进行中序遍历时,得到的元素序列为{35, 37, 47, 51, 58, 62, 73, 88, 93, 99},可以发现它正好是一个从小到大排列的有序的序列。这样的一颗二叉树就叫做”二叉排序树”。

《二叉排序树(二叉查找树)》

2、二叉排序树的定义

二叉排序树也叫做二叉查找树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有结点的值均小于它的根结点的值
  • 若它的右子树不为空,则右子树上所有结点的值均大于它的根结点的值
  • 它的左、右子树也分别为二叉排序树(递归的定义)

3、构造一棵二叉树的目的并不是为了排序,而是为了提高查找和插入、删除元素的速度,因为在一个有序的数据集上进行查找操作,速度肯定是快于无序的数据集的。同时,二叉排序树也有利于插入和删除的实现。
4、二叉排序树的删除操作
二叉排序树的插入和查找操作都比较容易,但是对于删除操作,可不是那么容易。因为删除了某个结点,整颗二叉排序树依然要保持二叉排序树的特性,但是删除时有多种可能的情况,如下:

  • 如果要删除的结点是二叉排序树中的叶子结点,可以直接删除,对整棵树来说不会影响到其它结点,如下图。

《二叉排序树(二叉查找树)》

  • 如果要删除的结点只有左子树或者只有右子树,则删除结点后,将它的左子树或者右子树整个移动到删除结点的位置即可,可以想象成独子继承父业,如下图。

《二叉排序树(二叉查找树)》

  • 如果要删除的结点既有左子树又有右子树,可以先把该结点当成只有左子树,那么做法就如同第二种情况一样;然后再对要删除结点右子树的所有结点重新进行插入,如下图所示,但是这样一来,如果右子树的结点个数很多,效率就很低,而且还会导致整个二叉排序树结构发生很大的变化,有可能会增加树的高度,

《二叉排序树(二叉查找树)》

        仔细观察这棵二叉排序树,可以发现在结点47的左右两棵子树中,结点37或者结点48都可以用来代替结点47,此时删除结点47后,整个二叉树并没有发生本身的变化。为什么 是37和48呢?因为它们正好是二叉排序树中比它待删除元素结点小或者大的最接近的两个结点,也就是说,如果对该二叉排序树进行中序遍历,得到的序列为{29, 35, 36, 37, 47, 48, 49, 50, 51, 56, 58, 62, 73, 88, 93, 99},其中37和48正好是47的直接前驱和直接后继。所以,更好的解决办法是找到需要删除的结点p的直接前驱(或直接后继)s,用s来替换结点p,然后再删除该结点s,如下图,47被直接前驱37替换,然后删除了结点37,将结点36移至原37的位置。
《二叉排序树(二叉查找树)》

        或者用直接后继结点来代替,如下图:
《二叉排序树(二叉查找树)》

5、二叉排序树算法实现

/**************************************************************/
/**                        二叉排序树                          **/
/**************************************************************/

#include <stdio.h>
#include <stdlib.h>

#define FALSE 0
#define TRUE 1

typedef int Boolean;
typedef int ElemType;

// 二叉树的二叉链表结点结构定义
typedef struct BiTNode
{
    ElemType data;
    struct BiTNode *lchild, *rchild;   // 左右孩子指针
}BiTNode, *BiTree;

/**
 * 递归查找二叉排序树tree中是否存在key
 * 指针front指向tree的双亲,其初始调用值为NULL
 * 若查找成功,则指针p指向该数据元素结点,并返回TRUE;否则指针p指向查找路径上访问的最后一个结点并返回FALSE
 */
Boolean SearchBinarySortTree(BiTree tree, ElemType key, BiTree front, BiTree *p)
{
    if (!tree)  // 查找失败
    {
        *p = front;
        return FALSE;
    }
    else if (tree->data == key)   // 查找成功
    {
        *p = tree;
        return TRUE;
    }
    else if (tree->data < key)
    {
        return SearchBinarySortTree(tree->rchild, key, tree, p);  // 在该结点的右子树继续查找
    }
    else
    {
        return SearchBinarySortTree(tree->lchild, key, tree, p);  // 在该结点的左子树继续查找
    }
}

/**
 * 当二叉树tree中不存在关键字等于key的数据元素时,插入key并返回TRUE,否则返回FALSE
 */
Boolean InsertBinarySortTree(BiTree *tree, ElemType key)
{
    BiTree p, s;

    if (!SearchBinarySortTree(*tree, key, NULL, &p)) // 不存在关键字为key的数据元素
    {
        s = (BiTree)malloc(sizeof(BiTNode));
        if (!s)
        {
            return FALSE;
        }
        s->data = key;
        s->lchild = s->rchild = NULL;

        if (!p)   // p为空则说明二叉查找树为空
        {
            *tree = s; // 作为二叉排序树的根结点
        }
        else if (p->data > key)
        {
            p->lchild = s;   // s结点作为左孩子
        }
        else
        {
            p->rchild = s;   // s结点作为右孩子
        }
        return TRUE;
    }
    return FALSE;            // 已经存在关键字为key的数据元素
}

/**
 * 从二叉排序树中删除结点p,并重接它的左或右子树
 */
 Boolean DoDelete(BiTree *p)
 {
     BiTree q, s;

     // 右子树为空,则只需要重接它的左子树
     if ((*p)->rchild == NULL)
     {
         q = *p;
         *p = (*p)->lchild;
         free(q);
     }
     else if ((*p)->lchild == NULL)     // 左子树为空,则只需要重接它的右子树
     {
         q = *p;
         *p = (*p)->rchild;
         free(q);
     }
     else     // 左右子树均不为空
     {
         q = *p;
         s = (*p)->lchild;
         while (s->rchild)   // 转左,然后向右到尽头(找到待删除结点的前驱)
         {
             q = s;
             s = s->rchild;
         }
         (*p)->data = s->data;   // s指向被删除结点的直接前驱
         if(q != *p)    // 重接q的右子树
         {
             q->rchild = s->lchild;  // 重接q的右子树
         }
         else
         {
             q->lchild = s->lchild;  // 重接q的左子树
         }
         free(s);
     }
     return TRUE;
 }

/**
 * 若二叉排序树tree中存在关键字等于key的数据元素时,则删除该元素结点,并返回TRUE,否则返回FALSE
 */
Boolean DeleteBinarySortTree(BiTree *tree, ElemType key)
{
    if (!tree)    // 不存在关键字等于key的数据元素
    {
        return FALSE;
    }
    else
    {
        if (key == (*tree)->data)    // 找到关键字等于key的数据元素
        {
            return DoDelete(tree);
        }
        else if (key < (*tree)->data)
        {
            return DeleteBinarySortTree(&(*tree)->lchild, key);
        }
        else
        {
            return DeleteBinarySortTree(&(*tree)->rchild, key);
        }
    }
}

int main()
{
    int i;
    Boolean bole;
    ElemType data[10] = {62, 88, 58, 47, 35, 73, 51, 99, 37, 93};

    BiTree tree = NULL, p;

    for (i = 0; i < 10; i++)
    {
        InsertBinarySortTree(&tree, data[i]);
    }

    bole = SearchBinarySortTree(tree, 99, NULL, &p);
    if (bole == TRUE)
    {
        printf("查找成功\n");
    }
    else
    {
        printf("查找失败\n");
    }

    bole = DeleteBinarySortTree(&tree, 99);
    if (bole == TRUE)
    {
        printf("删除成功\n");
    }
    else
    {
        printf("删除失败\n");
    }

    return 0;
}

三、二叉排序树总结
1、二叉排序树是以链接的方式存储,保持了链接存储结构在执行插入和删除操作时不用移动元素的优点,只要找到合适的插入和删除位置后,仅需修改链接指针即可,所以其插入和删除的时间性能较好。而对于二叉排序树的查找操作,走的就是从根结点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数,最小为1次,即根结点就是要找的结点,最多也不会操作树的深度。也就是说二叉排序树的查找性能取决于二叉排序树的形状,可是二叉排序树的形状是不确定的,比如数列{62, 88, 58, 47, 35, 73, 51, 99, 37, 93},可以构建成如下图左边所示的二叉排序树,但是如果数列元素本身就是从小到大有序,比如{35, 37, 47, 51, 58, 62, 73, 88, 93, 99},则构造成的二叉排序树就成了如下图左边所示的极端的右斜树(也是一棵二叉排序树),此时同样是查找结点99,左图只需要比较两次,而右图需要比较10次,差异很大。
《二叉排序树(二叉查找树)》

所以,希望二叉排序树是比较平衡的,即其深度与完全二叉树相同,均为⌊log2N⌋+1,那么查找的时间复杂度也就是O(logn),近似于折半查找。而像上图的右图那样就是不平衡的最坏情况,查找时间复杂度为O(n),等同于顺序查找。

所以:如果希望对一个集合按照二叉排序树来查找,最好是把它构建成一棵平衡的二叉排序树。

    原文作者:二叉查找树
    原文地址: https://blog.csdn.net/cdu09/article/details/23543285
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞