树表查找与二叉排序树


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

二叉排序树

什么是二叉排序树

对于线性查找而言,当查找的数据发生了变化,进行修改的开销是很大的,因此线性查找适合静态查找。对于动态查找而言,用树结构来制作查找表会更为合适,因为动态地操作树结构的结点,开销远比顺序表小得多。
所谓二叉搜索树,也可以称之为二叉排序树,就是基于树结构建立的查找表。对于二叉排序树需要满足一下 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,则递归进入右子树插入。

代码实现

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

BinTree InsertBST(BinTree BST, ElementType X)
{
    if (BST == NULL)      //找到插入位置
    {
        BST = new TNode;
        BST->Data = X;
        BST->Left = NULL;
        BST->Right = NULL;
    }
    else
    {
        if (X < BST->Data)      //递归进入左子树插入
            BST->Left = InsertBST(BST->Left, X);
        else                    //递归进入右子树插入
            BST->Right = InsertBST(BST->Right, X);
    }
    return BST;
}

删除数据

算法流程

首先基于查找的框架,找到需要删除的结点。二叉搜索树的删除需要考虑更多的可能性,有以下 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 树

实例:是否完全二叉搜索树

题干

题目分析

这道题目可以被分为 2 部分,分别是建立二叉搜索树和判断是否是完全二叉树。首先是建立二叉搜索树,这个操作并不难,只需要使用上文给出的建树函数,循环调用插入函数就行。值得注意的是这道题左子树是较大的关键字,右子树是较小的关键字。
接下来就是判断是否是完全二叉树,首先我们先回忆一下什么是完全二叉树。我使用通俗的话来说,所谓完全二叉树就是生成结点的顺序是严格按照从上到下,从左往右的顺序来构建的二叉树。例如对于题设测试样例 1 所建立的二叉搜索树,我把它展开为拓展二叉树的形式:

若按照“从上到下,从左到右”的顺序去读这个二叉树,会发现空结点只会集中出现在末尾部分。下面再看测试样例 2:

从定义上讲,这不是个完全二叉树,若展开成拓展二叉树的形式,按照“从上到下,从左到右”的顺序去读这个二叉树,会发现有个空结点穿插在了结点之间。
也就是说要判断一个二叉树是否是完全二叉树,可以先展开为拓展二叉树,然后按照“从上到下,从左到右”的顺序遍历这个二叉树,若在所有实际存在的结点遍历完毕之前遇到了空结点,就说明这不是完全二叉树。如何实现“从上到下,从左到右”的顺序遍历?这就是所谓的层序遍历法,需要通过一个队列结构来辅助实现。对于二叉树的相关概念和操作,可以前往博客——二叉树结构详解进行回顾。

总体的思路已经很明确了,接下来就是如何体现中间遇到了空结点?在层序遍历中我们可以直接忽略空结点,不让空结点入队列,但是这里必须用拓展二叉树的思想让空结点入队列,这样我们才能确定是否有空结点的出现。但是如果是这样的话,可以在空结点入队列时判断不是完全二叉树吗?也不行,因为这么操作在最后会有一系列空结点入队列。
再观察一下完全二叉树的特点,我们就会明白了,若二叉树是完全二叉树,那么遇到空结点之前入队列的结点数就会和二叉搜索树中的结点数相等。此时我们可以另外定义一个变量来统计结点数,当遇到空结点入队列时就停止统计,在层序遍历结束后返回这个变量。若返回的结点数和实际结点数相同,说明是完全二叉树,否则就不是,这样就能同时实现层序遍历和完全二叉树的判定了。

伪代码

代码实现

int levelOrder(BinTree t)   //层序遍历并判断完全二叉树
{
    BinTree ptr;
    queue<BinTree> que_level;    //层序结点队列
    int flag = 0;      //是否有 NULL 入队列的 flag
    int count = 0;      //统计遇到 NULL 之前的结点数

    if (t == NULL)    //空树处理
    {
        cout << "NULL";
    }
    que_level.push(t);    //根结点入队列
    while (!que_level.empty())    //直至空队列,结束循环
    {
        if (que_level.front() == NULL)      //队列读取到空结点
        {
            flag = 1;      //修改 flag 表示接下来不再统计结点数
        }
        else       //队列头结点非空
        {
            if (count == 0)
            {
                cout << que_level.front()->data;
            }
            else
            {
                cout << " " << que_level.front()->data;
            }
            if (flag == 0)
            {
                count++;      //统计结点数
            }
            que_level.push(que_level.front()->left);    //左结点入队列
            que_level.push(que_level.front()->right);    //右结点入队列
        }
        que_level.pop();    //队列头出队列
    }
    return count;      //若返回的结点数和实际结点数相同,说明是完全二叉树
}

调试遇到的问题

这道题虽然是一次过了,但是调试时遇到的问题很多。
Q1:层序遍历操作得出的结点序列,与测试样例差别很大,顺序混乱。
A1:按照层次分开,发现每一层的结点都是逆序的,重新读题发现题目要求左子树是较大的关键字,右子树是较小的关键字。因此通过修改结点插入函数的判断条件,就能得到正确的序列。
Q2:判定完全二叉树时,发现无论什么情况判断为是。
A2:因为没有按照拓展二叉树去写,空结点并不会入队列,而我的判断语句是在出队列时发挥作用的,这就导致了我无法进行任何判断。修改方式为,遍历到了空结点也入队列。
Q3:修改好 Q3 后,发现无论什么情况判断为否。
A3:我的判断语句是根据是否是空结点来判断的,但是用拓展二叉树的思想让空结点入队列,操作在最后会有一系列空结点入队列,这就导致了无论如何都有空结点的出现。这就说明我的判断条件写错了,或者判断机制得重新设计。最后的解法是另外定义一个变量来统计结点数,当遇到空结点入队列时就停止统计,在层序遍历结束后返回这个变量。若返回的结点数和实际结点数相同,说明是完全二叉树,否则就不是。

知识总结

  1. 二叉搜索树的基操,这道题的前提条件就是建出二叉搜索树,没有这一步后面的所有都免谈。这就需要熟悉二叉搜索树的建立方式,二叉搜索树的建立基础是插入数据,而插入数据的本质是查找,虽然是基础操作,但是也可以加深对二叉搜索树的理解。
  2. 层序遍历法,这个操作是属于二叉树遍历法之一。层序遍历就好像从根结点开始,一层一层向下扩散搜索,这就跟我们队列实现迷宫算法非常类似,因为迷宫算法的不同路径也是无关联的,但是我们是用广度优先搜索的思想可以找到最短路径。层序遍历需要结合队列结构协同操作,在这里有熟悉了这个遍历手法。
  3. 完全二叉树的性质,完全二叉树的概念不好理解,但是用“从上到下,从左到右”这个顺序就会变得形象。在这里对完全二叉树的判断提出要求,这就需要理解其特点和性质,同时这也是堆结构的基础,在这里加深理解是很必要的。
  4. 辅助变量的使用,在这里我使用了 count 变量顺手判断了是否是完全二叉树。这个变量的设计,不仅是从需求和问题出发,更是结合了细化的知识点,可见细致的分析对问题的解决而言极为重要。

参考资料

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

猜你喜欢

转载自www.cnblogs.com/linfangnan/p/12958068.html