zoukankan      html  css  js  c++  java
  • PTA习题解析:二叉搜索树的最近公共祖先

    二叉搜索树

    这道题目使用二叉搜索树实现,并且都要用到插入结点和查找结点的基操。更多基础内容可以查看博客——树表查找

    结构体定义

    typedef struct TNode
    {
        int data;
        struct TNode* left, * right;
    } TNode, * BinTree;
    

    插入操作

    二叉搜索树的插入本质上是查找操作,时间复杂度在 O(㏒2n) ~ O(n) 之间,这要根据树的形态而定。

    void Insert(BinTree& BST, int num)
    {
        if (BST == NULL)      //找到插入位置,插入结点
        {
            BST = new TNode;
            BST->data = num;
            BST->left = NULL;
            BST->right = NULL;
        }
        else
        {
            if (num < BST->data)
            {
                Insert(BST->left, num);
            }
            else if (num > BST->data)      //注意不要漏条件
            {
                Insert(BST->right, num);
            }
        }
    }
    

    查找操作

    bool Find(BinTree BST, int num)
    {
        bool flag = true;
    
        while (BST != NULL && BST->data != num)      //查找直到成功或失败
        {
            if (num < BST->data)
            {
                BST = BST->left;
            }
            else
            {
                BST = BST->right;
            }
        }
        if (BST == NULL)
        {
            flag = false;
        }
        return flag;
    }
    

    二叉搜索树的最近公共祖先

    测试样例

    输入

    6 8
    6 3 1 2 5 4 8 7
    2 5
    8 7
    1 9
    12 -3
    0 8
    99 99
    

    输出

    LCA of 2 and 5 is 3.
    8 is an ancestor of 7.
    ERROR: 9 is not found.
    ERROR: 12 and -3 are not found.
    ERROR: 0 is not found.
    ERROR: 99 and 99 are not found.
    

    题目解析

    这道题目是分成 3 部分来解决,分别是建立二叉搜索树、判断结点是否存在于树和获取 2 个结点的公共祖先。首先是建立二叉搜索树,这个操作虽然不难,循环调用插入函数就行。但是在这个地方一定要保证建立的正确性,否则下面的操作都无法实现。
    接下来是判断结点是否存在于树,这个部分是二叉搜索树的基操,使用上面的函数就行。这里的想法是,若结点不存在于树中就可以忽略第 3 部分的操作。
    第 3 部分就是获取 2 个结点的公共祖先,概括一下就是找到第一个公共祖先结点,该结点满足从这个结点出发进行搜索,可以找到两个要求的结点。关于这个操作可以用很多方式实现,这里讲解一个较为简单的手法,就是落脚到性质上去分析。对于二叉搜索树来说,满足条件的结点的数据域,值是 2 个给定数据的中间值,我们来观察一个例子。

    对于结点 2 和 5,他们的公共祖先是结点 3,如图所示是如此,从数值上看 3 是 2 和 5 的中间值。不过为什么不能是结点 4 呢?因为结点 4 虽然是中间值,但是却不是第一个遇到的中间值,也就是结点 3 所在的层次比其更上层。进行搜索操作时,结点 3 就会被先遍历到。下面再看一个例子,对于结点 8 和 7,其公共祖先就是结点 8,因为结点 8 本身就可以访问它本身。

    查找公共祖先函数 findLCA(BinTree BST, int num1, int num2)

    伪代码

    本质上其实也是查找操作,时间复杂度 O(㏒2n)。

    代码实现

    int findLCA(BinTree BST, int num1, int num2)
    {
        while (1)
        {
            if (BST->data > num1&& BST->data > num2)      //LCA 在根结点左树中
            {
                BST = BST->left;
            }
            else if (BST->data < num1 && BST->data < num2)      //LCA 在根结点左树中
            {
                BST = BST->right;
            }
            else      //当前根结点是 LCA
            {
                break;
            }
        }
        return BST->data;
    }
    

    主函数 main()

    伪代码

    代码实现

    int main()
    {
        BinTree BST = NULL;
        int fre, count;
        int num1, num2;      //待查找的 LCA 的 2 个结点
        bool flag1, flag2;      //结点是否存在于树结构中的 flag
        int lca;
    
        cin >> fre >> count;
        for (int i = 0; i < count; i++)     //建树
        {
            cin >> num1;
            Insert(BST, num1); 
        }
        for (int i = 0; i < fre; i++)
        {
            cin >> num1 >> num2;
            flag1 = Find(BST, num1);      //查找结点是否在树中
            flag2 = Find(BST, num2);
            if (flag1 == false && flag2 == false)      //2 个都不在
            {
                printf("ERROR: %d and %d are not found.
    ", num1, num2);
            }
            else if (flag1 == false)      //只有 1 个在
            {
                printf("ERROR: %d is not found.
    ", num1);
            }
            else if (flag2 == false)
            {
                printf("ERROR: %d is not found.
    ", num2);
            }
            else      //2 个结点都在
            {
                lca = findLCA(BST, num1, num2);      //找公共祖先
                if (lca == num1)
                {
                    printf("%d is an ancestor of %d.
    ", num1, num2);
                }
                else if (lca == num2)
                {
                    printf("%d is an ancestor of %d.
    ", num2, num1);
                }
                else
                {
                    printf("LCA of %d and %d is %d.
    ", num1, num2, lca);
                }
            }
        }
    }
    

    调试遇到的问题



    虽然提交列表很长,但是只解决了 2 个问题。
    Q1:找到的中间值并不是公共祖先,例如 2 和 5 找到的是 4。
    A1:在我使用上述找公共祖先的方法之前,我还写了另一种算法。思想概括一下,就是另外定义一个结构体,我称之为报文结构体,里面有 1 个 int 类型变量表示状态码,用于判断需要输出什么信息,用 2 个 bool 类型变量标记 2 个数据分别有没有被找到,1 个 int 类型变量存储公共祖先的值。想法就是只搜索 1 次,若找到需要的数据就修改对应的 flag,若 2 个数据都找到了就利用递归回溯来确定公共祖先。这种做法我觉得绝对是可行的,但是我还没有试出比较好的方法来优化状态码变量,因为我传递这个结构体是使用引用来传递,但是当我找到了 2 个数据,并修改了 flag 类型,递归并没有结束,当下一层递归时 2 个 flag 都被改变了,就会去修改状态码。对于公共祖先较为密集的部分,这个状态码就很容易被提前修改。因此在我找到更好的办法来控制这个状态码之前,使用了上文提及的方法。
    上文方法的好处是效率很高,这充分利用了二叉搜索树的性质,而我一开始并没有很好地理解所以没想到。
    Q2:运行超时。
    A2:我都不知道我中间做了什么,反正就是搞搞搞终于最后一次碰巧就不超时了,直接看截图吧。

    插入函数这么写超时。

    这么写不超时,看来确实是细节决定成败。由于我一开始漏了一个判断条件,使得建树函数并不能建立正确,在一些数据较为刁钻的测试点中就会有查找操作停不下来的情况。看来基础操作一定要烂熟于心,不能够出问题。

    知识总结

    1. 二叉搜索树的基操,这道题的前提条件就是建出二叉搜索树,没有这一步后面的所有都免谈。这就需要熟悉二叉搜索树的建立方式,二叉搜索树的建立基础是插入数据,而插入数据的本质是查找,虽然是基础操作,但是也可以加深对二叉搜索树的理解。尤其需要注意一定不能漏条件,这里的数据还是比较有规律的,实际应用时如果没有对每一种条件都进行判断,就会出大问题。
    2. 二叉搜索树的性质,对于一个结点,比这个结点数值大的结点都在右子树,反之都在左子树。如果灵活运用这个性质,操作就会变得简捷而高效。
  • 相关阅读:
    linux内核的若干问题
    shell(四)--turboastat
    Mac系统维护
    花卉养殖(1) 黄叶
    你就是佛(1)- 本体、开悟与思想
    linux 工具(2)----- crontab定时任务管理
    vim (四) 使用技巧
    linux kernel __init和__exit宏的作用
    优秀的网站
    Mac OSX 快捷键&命令行
  • 原文地址:https://www.cnblogs.com/linfangnan/p/14695377.html
Copyright © 2011-2022 走看看