PTA习题解析:是否完全二叉搜索树

二叉搜索树#

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

结构体定义#

Copy Highlighter-hljs
typedef struct TNode { int data; struct TNode* left, * right; } TNode, * BinTree;

插入操作#

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

Copy Highlighter-hljs
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); } } }

查找操作#

Copy Highlighter-hljs
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; }

是否完全二叉搜索树#

测试样例 1#

输入样例#

Copy Highlighter-hljs
9 38 45 42 24 58 30 67 12 51

输出样例#

Copy Highlighter-hljs
38 45 24 58 42 30 12 67 51 YES

测试样例 2#

输入样例#

Copy Highlighter-hljs
8 38 24 12 45 58 67 42 51

输出样例#

Copy Highlighter-hljs
38 45 24 58 42 12 67 51 NO

题目分析#

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

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

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

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

主函数 main()#

Copy Highlighter-hljs
int main() { BinTree T = NULL; int fre; //查找次数 int num; int count; //遇到 NULL 前遍历的结点数 cin >> fre; for (int i = 0; i < fre; i++) //建树 { cin >> num; Insert(T, num); } //PreOrderTraverse(T); //前序遍历检查建树是否正确 count = levelOrder(T); if (count == fre) //若返回的结点数和实际结点数相同,说明是完全二叉树 { cout << "\nYES"; } else //否则不是 { cout << "\nNO"; } return 0; }

层序遍历函数 levelOrder(BinTree t)#

伪代码#

由于需要把所有结点都过一遍,因此时间复杂度 O(n)。

代码实现#

Copy Highlighter-hljs
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 变量顺手判断了是否是完全二叉树。这个变量的设计,不仅是从需求和问题出发,更是结合了细化的知识点,可见细致的分析对问题的解决而言极为重要。
posted @   乌漆WhiteMoon  阅读(1251)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示
CONTENTS