目录
前置说明:
在学习二叉树的基本操作前,我们一般是需要创建一棵二叉树的,但是由于我们目前对于链式二叉树的理解很浅,暂时先手动创建一棵链式二叉树,以便快速的进入到二叉树的学习之旅,我们会在本文后期再来讲解如何创建一棵二叉树。
一、二叉树的创建
//二叉树节点的结构体
typedef int Datatype;
typedef struct BTreeNode
{
Datatype data;
struct BTreeNode* left;
struct BTreeNode* right;
}BTreeNode;
//创建一个节点
BTreeNode* BuyNode(Datatype x)
{
BTreeNode* newnode = (BTreeNode*)malloc(sizeof(BTreeNode));
if (newnode==NULL)
{
perror("malloc");
return NULL;
}
newnode->data = x;
newnode->left = NULL;
newnode->right = NULL;
return newnode;
}
//创建一棵链式二叉树
BTreeNode* CreatBTree()
{
BTreeNode* node1 = BuyNode(1);
BTreeNode* node2 = BuyNode(2);
BTreeNode* node3 = BuyNode(3);
BTreeNode* node4 = BuyNode(4);
BTreeNode* node5 = BuyNode(5);
BTreeNode* node6 = BuyNode(6);
node1->left = node2;
node1->right = node4;
node2->left = node3;
node4->left = node5;
node4->right = node6;
return node1;
}
那么这样我们就手动的创建好了一棵二叉树(如下图),可以遨游在二叉树的海洋中了!
注意:上述操作并不是真正的二叉树创建的方法,我们会在后面讲解真正的创建方式
二、二叉树的基本概念的回顾
在我们学习二叉树的基本操作前,我们先来回顾一下二叉树的概念:
(1)空树
(2)非空树:由根节点,左子树,右子树这三个组成的。
三、二叉树的前中后序遍历
二叉树操作中最为简单的非遍历莫属了。所谓的二叉树的遍历其实就是以某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每一个节点只会操作一次,而具体是进行怎样的操作则取决于实际问题中的需要,比如我们可以打印每一个节点,可以遍历计数.....等等。
由此看来,二叉树的遍历虽然是最为简单的操作,却是后面学习的根基,可以说遍历二叉树是整篇文章的关键!
按照规则,二叉树的遍历有四种:前序/中序/后序/层序
关于层序我们放到后面一点讲解
(1)前序(Preorder Traversal ):
访问节点的顺序依次是:根,左子树,右子树
(2)中序(Inorder Traversal):
访问节点的顺序依次是:左子树,根,右子树
(3)后序(Postorder Traversal):
访问节点的顺序依次是:左子树,右子树,根
由于被访问的节点必定是子树的根,所以N(Node)、L(Left subtree)和R(Right subtree)又可解释为根、根的左子树和根的右子树。NLR、LNR和LRN分别又称为先根遍历、中根遍历和后根遍历。
(1)二叉树的前序遍历
如果没有遇到空节点,就打印该节点的值,如果遇到空节点就打印N
//前序遍历
void PrevOrder(BTreeNode* root)
{
if (root==NULL)
{
printf("N ");
return;
}
printf("%d ",root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
可以看到这个代码很好的实现了我们的前序遍历,下面我们再来分析一下为什么要这样写。
关于二叉树,正是因为二叉树是递归创建的,所以我们在访问其节点的时候也必须使用递归的方式来访问,而递归只需要思考两个问题:
(1)递归逻辑(递归条件)
(2)返回条件(结束条件)
只要我们能够想清楚这两个问题,那么递归对于我们就如同探囊取物,轻而易举了。
二叉树递归的核心逻辑1:看图说话
第一种逻辑就是看图说话类型,即要求怎么走,我就怎么写!
首先,对于前序遍历,我们知道他的访问顺序是:根,左子树,右子树,所以我们在PrevOrder函数中首先写到就是这样的:
void PrevOrder(BTreeNode* root)
{
printf("%d ",root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
这不是就是按照我们的逻辑分析一步步走的前序嘛?
遇到根打印根节点的值,(现在根访问完成了)然后去访问左子树,再去访问右子树
注意:这里访问左右子树一定不能写成直接打印,否则就不是递归了,而是普通的打印函数,这并不能完成整棵树的遍历
其次,我们注意到,在这三句代码中都用到了解引用操作,但如果是空呢,那不就对空指针进行了非法的解引用操作嘛?所以我们就知道需要在这三句代码前补上对空指针的判断条件,像这样:
void PrevOrder(BTreeNode* root)
{
if (root==NULL)
{
printf("N ");
return;
}
printf("%d ",root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
如果遇到了空节点,就打印N,然后返回,这样就不会对空指针进行非法操作了,于是我们的前序遍历就完成了。
其实这里对NULL节点的判断不仅仅是因为对空指针的解引用,更重要的原因是当我们走到叶子节点的时候,他就是根,如果再去访问他的左右子树会发现都是NULL节点,所以需要从NULL节点返回到叶子节点。不过我们在平常写代码的时候可以理解成对空指针的解引用,能更加容易的想到,但是值得注意的是,如果后续我们的代码逻辑变得更加复杂了,仅仅考虑空指针是不够的,还是得走到叶子节点去考虑他的返回条件。
我们再来看看前序遍历的递归展开图,能够让你对递归的理解更加深刻!
像这种直接在树的图形上进行递归展开图是我们后期必须要掌握的技能
而在刚开始学习二叉树递归的时候可以把代码贴出来,一步一步跟着代码的执行逻辑进行,可以让思维更加清晰,并且对return的理解更深刻
好了,相信到这里你对二叉树的前序遍历以及递归的理解都会更上一层楼了,下面我们快速的把中序/后序的代码写出来,你可以依照上面我们所说的流程来尝试着自己写代码,并递归展开图来辅助我们的理解
(2)二叉树的中序遍历
先访问左子树,再访问根节点,最后访问右子树,如果遇到空节点就打印N并且返回
//中序遍历
void InOrder(BTreeNode* root)
{
if (root==NULL)
{
printf("N ");
return;
}
InOrder(root->left);
printf("%d ",root->data);
InOrder(root->right);
}
(3)二叉树的后序遍历
//后序遍历
void PostOrder(BTreeNode* root)
{
if (root == NULL)
{
printf("N ");
return;
}
InOrder(root->left);
InOrder(root->right);
printf("%d ", root->data);
}
关于二叉树的前中后序遍历都几乎一样,只需要对一种理解深刻,其他的都是信手拈来,大家可以在看文章的同时动手尝试画一画递归展开图,并且按照我们的核心逻辑去写一写代码,相信你对递归的理解和代码水平会有明显的提高!
四、二叉树的常见基本操作
二叉树的核心逻辑2:分治思想
对于二叉树,我们做任何访问的操作都要有一套逻辑:分治思想!
分治思想是我们以后写二叉树的关键,他的理念就是:你让我有什么操作,那么好,我先让我的左右子树去做,等他们做完了把结果汇报给我我再上交给你。
就好比我们的学校,教育局下发了一个任务给校长,校长肯定不能自己一件件的去做,不然得累死。于是,校长说:那么好,我先让院长们去做,我只用等院长做完汇总结果就行了;而事情就到了院长的手中,院长说:那么好,辅导员先去做,我等辅导员的结果就好;辅导员当然也不会傻,于是辅导员把班长叫到了一起说:你们班长先去吧这件事做完,给我汇报。于是教育局下发的任务就不需要校长亲力亲为了,而是逐级让下面的人去做就好了。
而这正是我们分治思想和递归的最为重要的逻辑,他能帮助我们完成99.9%的递归问题。
(1)求二叉树节点个数
求二叉树的节点当然有两种方法:
1.遍历一遍,每一次遇到一个节点就count++
2.分治思想,让左右子树去求他们的节点个数,根节点负责合并数据
我们直接使用分治思想来解决问题,先来想想他的子问题是什么?返回条件是什么?
子问题:左右子树分别求个数,到我(根)这里求和再加上自己的1,最后返回给上一层
返回条件:遇到NULL返回0
int BTreeSize(BTreeNode* root)
{
if (root==NULL)
{
return 0;
}
return BTreeSize(root->left) + BTreeSize(root->right) + 1;
}
这样,求二叉树节点的个数就被我们轻而易举的完成了,我们仍然只用了两大逻辑:
分治思想+看图说话
分治思想让我们知道了代码的逻辑是什么,是怎么样进行下去的;
看图说话让我们跟着分治思想分析出来的逻辑写代码。
有没有感觉递归从来没有如此通透?
(2)求二叉树叶子节点个数
子问题:
让左右子树去求叶子节点个数,到根节点合并,最后返回到上一层调用处
返回条件:
如果遇到NULL节点,返回0
如果遇到他的左右子树都是NULL返回1
int BTreeLeafSize(BTreeNode* root)
{
if (root==NULL)
{
return 0;
}
if (root->left==NULL&&root->right==NULL)
{
return 1;
}
return BTreeLeafSize(root->left) + BTreeLeafSize(root->right);
}
(3)求二叉树第K层节点的个数
子问题:
让左右子树分别求K-1层节点的个数,到根节点合并,最后返回到上一层调用处
返回条件:
如果遇到NULL返回0
如果K==1且节点不为NULL,返回1
int BTreeKSize(BTreeNode* root,int k)
{
if (root==NULL)
{
return 0;
}
//注意走到这里的时候说明root不为NULL,可以解引用
if (k==1)
{
return 1;
}
return BTreeKSize(root->left,k-1) + BTreeKSize(root->right,k-1);
}
(4)二叉树查找值为x的节点
子问题:
让左右子树去查找有无该节点,如果有就返回该节点的地址,如果没有返回NULL,最后再返回到上一层调用处
返回条件:
如果遇到NULL,返回NULL
如果遇到该节点,返回该节点的地址
如果没有遇到该节点,就返回NULL
BTreeNode* BTreeFind(BTreeNode* root,int x)
{
if (root==NULL)
{
return NULL;
}
//走到这里说明root不为NULL,可以解引用
if (root->data==x)
{
return root;
}
//注意这里的return和之前不太一样,之前是一把子就返回了两个的和
//而这里不能一次性返回两个!必须要选择其中一个返回
//所以我们需要把它记录下来,然后判断到底返回哪一个
BTreeNode* ret1 = BTreeFind(root->left,x);
if (ret1!=NULL)
{
return ret1;
}
BTreeNode* ret2 = BTreeFind(root->right,x);
if (ret1 != NULL)
{
return ret1;
}
//如果到这里都没有找到,说明左右子树和根都没有,返回NULL
return NULL;
}
注意:
(1)这一道题目和我们之前的都不太一样,因为它具有了返回值,准确的来说,他的返回值不能像上面几道题一样合并到一个return中返回,而是需要在上一层调用处判断返回值是否有效,即他的返回值是给上一层函数确定下一步行为的!
(2)还有一点值得注意的是,return并不是直接就能够返回到最外层main中的,而是返回到调用它的上一层函数,所以我们需要保证该函数中的每一种情况都有一个返回值,否则的话就会出现没有返回值的情况!这个我们需要画递归展开图才能很好的分析!
(5)求二叉树的高度
子问题:
先求出左右子树分别的高度,到根节点来判断谁大,返回大的那一个
返回条件:
如果遇到NULL返回0
如果不是NULL,就先求出左右子树的高度,返回大的
//求二叉树的高度
int BTreeHeight(BTreeNode* root)
{
if (root==NULL)
{
return 0;
}
int hleft = BTreeHeight(root->left);
int hright = BTreeHeight(root->right);
return hleft > hright ? hleft+1 : hright+1;
}
注意:
这里万万不可在返回的时候,直接使用三目比较操作符,因为这样会导致它计算多次大小,在比较谁大的时候就已经计算了一次大小,在返回的时候又会计算一次大小。而且这里还不仅仅是计算两次大小的问题,因为树是递归定义的,我们在求高度的时候也是使用递归的思路,这样会导致递归求大小的次数极为庞大,每过一层,下一层的次数都会以2为公比递增,一旦树的节点,高度比较大,会让效率大大降低!
五、二叉树的oj题
在做oj题之前,得先说明一点很关键的逻辑,他能帮助我们省掉很多的代码逻辑,让我们的思路更加清晰。
二叉树的核心逻辑3:
我们尽量去判断可以直接返回值的条件,否则的话会有很多逻辑去补充,因为如果你判断的情况是不能够直接返回true或者false的(或者其他类型的返回值),那你需要再if中补充接下来的步骤(往往是递归),有人会说,那这样我就不需要在外面递归调用了哇,但是实际上做题时这个会比你想象的更加复杂一点,如果你没有绝对的自信,尽量像我一样去判断能够直接返回的情况,而不是给自己找麻烦。
(1)单值二叉树
子问题:
让他的左右子树去判断是否为单值二叉树,到根节点合并&&返回布尔值
返回条件:
如果遇到了NULL节点,返回true
如果左子树的值和根的值不相同,返回false
如果右子树的值和根的值不相同,返回false
递归返回左子树&&右子树的判断结果
到这里没有返回的话,返回true(这一步是多余的,因为根本就不会走到这里)
bool isUnivalTree(struct TreeNode* root)
{
if(root==NULL)
{
return true;
}
//注意判断左右子树不为空,否则会对空指针解引用
if(root->left!=NULL&&root->left->val!=root->val)
{
return false;
}
if(root->right!=NULL&&root->right->val!=root->val)
{
return false;
}
//如果左右子树为空,或者值相同就会走到这里进入下一层递归
return isUnivalTree(root->left)&&isUnivalTree(root->right);
}
注意:每一条路径都需要保证能够有一个返回值,不要漏掉其中的 任何一条!
(2)检查两棵树是否相同
思路:用两个指针分别去走两棵树的遍历逻辑,最好用前序,因为针对于这道题,前序的效率会高一点,就比如只有根节点不同,但是你却去比较完了他的左右子树
子问题:
去判断左右子树是否是相同的树,到根去&&,并且返回到上一层调用的地方
返回条件:
如果遇到了NULL,分为几种情况:
1.一个为空,另一个不为空,返回false
2.都为空,返回true
如果同样位置的节点的值不相同,返回false
返回左右子树的判断结果相&&
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
if(p==NULL&&q==NULL)
{
return true;
}
//走到这说明有一个不为空,所以用||
if(p==NULL||q==NULL)
{
return false;
}
//走到这说明都不为空
if(p->val!=q->val)
{
return false;
}
return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);
}
(3)对称二叉树
思路:
新增一个函数,去判断树是否相同
子问题:
去判断左子树是否和另外一棵的右子树相同&&右子树是否和另外一棵的左子树相同
到根节点相&&,并返回到上一层调用处
返回条件:
直接去调用相同的树的函数
//复用的相同的树的函数,稍加修改
bool isSameTree(struct TreeNode* tree1,struct TreeNode* tree2)
{
if(tree1==NULL&&tree2==NULL)
{
return true;
}
if(tree1==NULL||tree2==NULL)
{
return false;
}
if(tree1->val!=tree2->val)
{
return false;
}
//注意这里比较的是对称的树,所以传的是一个左一个右,不要弄错了
return isSameTree(tree1->left,tree2->right)
&& isSameTree(tree1->right,tree2->left);
}
bool isSymmetric(struct TreeNode* root)
{
//因为题目说了节点的范围在1--1000,所以不需要判断是否为空
return isSameTree(root->left,root->right);
}
(4)另一棵树的子树
思路:
从根节点开始往下走,每一个节点都判断一下是否和subRoot树相同,如果是相同的就直接返回true
子问题:
判断他的左右子树是不是和subRoot树相同,到根节点||之后返回到上一层调用的地方
返回条件:
如果是NULL,不能解引用了,而且一定不和subRoot相同,直接返回false
每一个节点都判断是否和subRoot树相同,如果是相同的就直接返回true
返回左子树||右子树
bool isSameTree(struct TreeNode* p, struct TreeNode* q)
{
if(p==NULL&&q==NULL)
{
return true;
}
//走到这说明有一个不为空,所以用||
if(p==NULL||q==NULL)
{
return false;
}
//走到这说明都不为空
if(p->val!=q->val)
{
return false;
}
return isSameTree(p->left,q->left)&&isSameTree(p->right,q->right);
}
bool isSubtree(struct TreeNode* root, struct TreeNode* subRoot)
{
if(root==NULL)
{
return false;
}
//每一个节点都判断一下是否是子树
bool ret=isSameTree(root,subRoot);
if(ret==true)
{
return true;
}
return isSubtree(root->left,subRoot)
|| isSubtree(root->right,subRoot);
}
注意:无论什么题目空树NULL都是第一个要想到去判断的
(5)二叉树的前序遍历,把节点的值放到一个数组中
思路:
先求出树的大小,然后开辟对应大小的空间的数组(因为你并不知道要树有多少个节点)
用前序遍历的同时,把节点的值插入到数组中
//为了能够在递归调用的过程中改变i的值,所以我们传递的是i的地址
void PrevOrder(struct TreeNode* root,int* pi,int* arr)
{
if(root==NULL)
{
return;
}
//放数据到数组中
arr[*pi]=root->val;
(*pi)++;
PrevOrder(root->left,pi,arr);
PrevOrder(root->right,pi,arr);
}
//求出树的大小,方便我们malloc开空间
int TreeSize(struct TreeNode* root)
{
if(root==NULL)
{
return 0;
}
return TreeSize(root->left)+TreeSize(root->right)+1;
}
//因为力扣是不会知道测试用例中数组到底有多大的,所以给我们一个returnSize作为输出型参数告诉他
int* preorderTraversal(struct TreeNode* root, int* returnSize)
{
int n=TreeSize(root);
int*arr=(int*)malloc(sizeof(int)*n);
*returnSize=n;
//走一遍前序,把树的每一个节点的值放到这个数组中
//先定义一个i,用来表示数组的下标走到哪里了
int i=0;
PrevOrder(root,&i,arr);
return arr;
}
注意:这道题目又和之前的略有不同,因为他是要我们把数据插入到一个数组中,而这就会产生很多的问题:比如我们要传下标,但是如果我们是值传递,我们都知道值传递在函数内部的修改不影响外面的值,而恰好递归就是不断的创建栈帧,所以我们这个时候万万不可使用值传递,必须要使用地址的传递,才能对下标 i进行修改。
六、二叉树的层序遍历
层序遍历和之前的三种遍历大不相同,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层
上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
那么如何做到这样一个遍历的方式呢?我们在前中后序都是使用了递归的思想,但只有层序恰好不是递归,而是非递归,话说非递归就是使用队列先进先出的特性,让二叉树的节点依次进队列,然后再一一出队列访问的操作。
具体的操作是:先让二叉树的第一个节点入队列,然后不断的让队列往外面出数据,每出一个节点就让这个节点的左右孩子入队列,如果中途遇到了NULL就不入队列,直到队列为空,就完成了一次层序遍历。
队列的代码
//注意这里的QDataType是struct BRreeNode*类型的,因为我们是让树的节点入队列
typedef struct BTreeNode* QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x);
void QueuePop(Queue* pq);
QDataType QueueFront(Queue* pq);
QDataType QueueBack(Queue* pq);
int QueueSize(Queue* pq);
bool QueueEmpty(Queue* pq);
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail\n");
return;
}
newnode->data = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
assert(pq->phead == NULL);
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
// 1、一个节点
// 2、多个节点
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
// 头删
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->phead->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->ptail->data;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
/*return pq->phead == NULL
&& pq->ptail == NULL;*/
return pq->size == 0;
}
层序遍历的代码
void BTreeLevelOrder(BTreeNode* root)
{
Queue q1;
QueueInit(&q1);
//先让根节点入队列
QueuePush(&q1,root);
//每出一个节点,都让它的左右孩子节点入队列
while (!QueueEmpty(&q1))
{
//出一个节点
BTreeNode* front = QueueFront(&q1);
QueuePop(&q1);
printf("%d ",front->data);
//左右孩子节点入队列
//一定要注意这里是front不是root,因为front才是队列的第一个节点,如果写成root会死循环
if (front->left!=NULL)
{
QueuePush(&q1, front->left);
}
if (front->right!=NULL)
{
QueuePush(&q1, front->right);
}
}
printf("\n");
QueueDestroy(&q1);
}
七、二叉树的创建和销毁
(1)二叉树的销毁
//思路:后序遍历,最后销毁root先去递归左右子树
void BTreeDestroy(BTreeNode* root)
{
//如果是空树,什么都不用做,直接返回
if (root==NULL)
{
return;
}
BTreeDestroy(root->left);
BTreeDestroy(root->right);
free(root);
}
注意:
这里最好后序遍历,因为如果是前序或者中序还需要记录一下root的左右孩子节点,否则一旦root被释放了,就再也找不到他的左右孩子了
(2)前序创建二叉树
子问题:
先去创建一个根节点,作为整棵树的根
让根节点的左右子树去创建二叉树,并且把创建的二叉树连接到根节点
返回条件:
如果遇到了NULL就返回NULL,让下标往后面走一步
#include <stdio.h>
#include <stdlib.h>
typedef char Datatype;
typedef struct BTreeNode
{
Datatype data;
struct BTreeNode* left;
struct BTreeNode* right;
}BTreeNode;
BTreeNode* BuyNode(Datatype x)
{
BTreeNode* newnode=(BTreeNode*)malloc(sizeof(BTreeNode));
if(newnode==NULL)
{
perror("malloc");
return NULL;
}
newnode->data=x;
newnode->left=NULL;
newnode->right=NULL;
return newnode;
}
//子问题:让他的左右子树去创建树,到根节点连接上左右子树
//pi用来表示数组的下标,传地址是因为形参的改变不影响实参
BTreeNode* PrevOrderCreatTree(char* a,int* pi)
{
//如果是空节点就返回NULL
if(a[*pi]=='#')
{
(*pi)++;
return NULL;
}
//先创建一个根节点
BTreeNode* root=BuyNode(a[*pi]);
(*pi)++;
root->left=PrevOrderCreatTree(a,pi);
root->right=PrevOrderCreatTree(a, pi);
//返回root给上一层调用处
return root;
}
//中序遍历
void InOrder(BTreeNode* root)
{
if(root==NULL)
{
return;
}
InOrder(root->left);
printf("%c ",root->data);
InOrder(root->right);
}
int main()
{
char a[100];
scanf("%s",a);
int i=0;
BTreeNode* root=PrevOrderCreatTree(a,&i);
InOrder(root);
printf("\n");
return 0;
}
(3)判断二叉树是否是完全二叉树
思路:这道题肯定不能去判断每一行节点的个数是不是符合完全二叉树的规律,因为有可能某一行的节点不是连续的,但是会被我们误判成完全二叉树。所以最正确的方法应该是层序遍历,把每一行的节点都插入到队列中,然后不断的出队列,如果遇到了NULL,就判断后面是不是都是空,(因为完全二叉树一旦遇到了空后面就一定没有节点了)如果都是NULL就是完全二叉树,一旦发现一个不是空就不是完全二叉树。
bool BinaryTreeComplete(BTreeNode* root)
{
//创建一个队列
Queue q1;
QueueInit(&q1);
//先插入根节点
if (root!=NULL)
{
QueuePush(&q1, root);
}
//开始出队列,出的时候把左右孩子都添加到队列中
while (!QueueEmpty(&q1))
{
BTreeNode* front = QueueFront(&q1);
QueuePop(&q1);
//遇到NULL就跳出循环,表示已经插入完成了
if (front==NULL)
{
break;
}
//走到这里说明front不为空
QueuePush(&q1, front->left);
QueuePush(&q1,front->right);
}
//检查后面的队列中是否都是NULL,如果不是则说明不是完全二叉树
while (!QueueEmpty(&q1))
{
BTreeNode* front = QueueFront(&q1);
QueuePop(&q1);
if (front!=NULL)
{
QueueDestroy(&q1);
return false;
}
}
QueueDestroy(&q1);
return true;
}
希望这篇文章能对大家有所帮助,大家下去也可以按照我说的三大核心逻辑去改进出属于自己的做题思维!