数据结构与算法:链式二叉树

上一篇文章我们结束了二叉树的顺序存储,本届内容我们来到二叉树的链式存储!

链式二叉树

  • 1.链式二叉树的遍历
    • 1.1二叉树的前序,中序,后序遍历
    • 1.2 三种遍历方法代码实现
  • 2. 获取相关个数
    • 2.1获取节点个数
    • 2.2获取叶节点个数
    • 2.3 获取树的高度
    • 2.4 计算第K层节点个数
  • 3.寻找某个节点
  • 4.用前序遍历建造一个二叉树
  • 5.二叉树的销毁
  • 6.链式二叉树的层序遍历
  • 7.判断是否为完全二叉树

1.链式二叉树的遍历

首先我们定义二叉树节点内容:

typedef int BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinartTreeNode* left;
	struct BinartTreeNode* right;
}TreeNode;

每个节点包含两个指针,指向左子树和右子树
在这里插入图片描述
左右子树的节点又可以细分为:根,左子树,右子树

1.1二叉树的前序,中序,后序遍历

前序遍历、中序遍历和后序遍历。这些遍历方式指的是节点访问的顺序

  • 前序遍历:在前序遍历中,我们首先访问根节点,然后递归地进行左子树的前序遍历,接着递归地进行右子树的前序遍历
  • 中序遍历:中序遍历中,我们首先递归地进行左子树的中序遍历,然后访问根节点,最后递归地进行右子树的中序遍历
  • 后序遍历:后序遍历中,我们首先递归地进行左子树的后序遍历,然后递归地进行右子树的后序遍历,最后访问根节点。

我们以一颗树为例来展开讨论

在这里插入图片描述
首先讨论前序遍历

在前序遍历中,我们首先访问根节点,然后是左子树,最后是右子树
对于上述树的前序遍历,遍历顺序将是:

  • 访问根节点 1
  • 访问左子节点 2
  • 访问左子节点 2 的左子节点 3
  • 左子节点3的左子树为空,返回到3
  • 访问完3的左子树访问3的右子树,为空,返回到3
  • 2的左子树访问完,访问2的右子树,为空,返回到2
  • 1的左子树访问完成,回到1访问右子树4
  • 访问4的左子树5,再访问5的左,5的左为空,则返回,访问5的右,再返回到5
  • 4的左子树遍历完成,访问右子树6,6的左右都为空,访问结束

如果用N来代表空,我们可以表示出访问顺序:

1 2 3 N N N 4 5 N N 6 N N

接下来讨论中序遍历

在中序遍历中,我们首先遍历左子树,然后访问根节点,最后遍历右子树

遍历顺序

  • 首先访问左子树,1的左子树2,再到2的左子树3,3的左子树为NULL,访问空,返回到三,再访问节点3,根访问后到右子树NULL,所以开始访问顺序为:
N 3 N
  • 访问完2的左子树,再按照顺序访问根节点2,再访问2的右子树N
N 3 N 2 N
  • 访问完1的左子树,访问节点1,再访问1的右子树,而右子树4部分优先访问左子树,所以先访问到5的左子树NULL,再访问5节点和5的右子树NULL
N 3 N 2 N 1 N 5 N
  • 访问完4的左子树,访问根节点4,再访问4的右子树,右子树优先访问左子树部分,即6的左子树NULL,再到6节点,最后访问6的右子树NULL
N 3 N 2 N 1 N 5 N 4 N 6 N

最后我们来看后序遍历

  • 首先访问1的左子树,在子树中,首先访问 2 的左子树,3的左子树为空NULL,访问后访问3的右子树NULL,再访问根节点3
N N 3
  • 访问完2的左子树后,访问2的右子树NULL,再访问节点2
N N 3 N 2
  • 访问完1的左子树,再访问1的右子树,子树中,先访问4的左子树,5组成的子树部分,先访问5的左子树NULL,再访问5的右子树NULL,最后访问根节点5
N N 3 N 2 N N 5
  • 4的左子树访问完成后,访问4的右子树,子树部分6,先访问6的左右NULL,再访问根节点6
N N 3 N 2 N N 5 N N 6
  • 4的左右子树访问完成,访问根节点4
  • 1的左右子树访问完成,访问根节点1
N N 3 N 2 N N 5 N N 6 4 1

1.2 三种遍历方法代码实现

接下来我们硬代码构造一个上图的树,来完成我们三种遍历方式的代码

TreeNode* CreatTreeNode(int x)
{
	TreeNode* node = (TreeNode*)malloc(sizeof(TreeNode));
	node->data = x;
	node->left = NULL;
	node->right = NULL;
	return node;
}
TreeNode* FCreatTree()
{
	TreeNode* node1 = CreatTreeNode(1);
	TreeNode* node2 = CreatTreeNode(2);
	TreeNode* node3 = CreatTreeNode(3);
	TreeNode* node4 = CreatTreeNode(4);
	TreeNode* node5 = CreatTreeNode(5);
	TreeNode* node6 = CreatTreeNode(6);

	node1->left = node2;
	node1->right = node4;
	node2->left = node3;
	node4->left = node5;
	node4->right = node6;
	return node1;
}

在上述三种方法的讨论中我们可以知道,三种遍历是用递归方法来完成的

前序遍历,首先访问当前节点(根节点),然后递归地进行左子树的前序遍历,最后递归地进行右子树的前序遍历

代码如下:

void PrevOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	printf("%d ", root->data);
	PrevOrder(root->left);
	PrevOrder(root->right);
}
  • 如果遇到节点为空,则打印N 并返回
  • 注意:这里是递归进行的,返回是指返回到上一层函数
  • printf("%d ", root->data);首先访问根节点
  • PrevOrder(root->left);访问左子树,进入左子树里面又分为先访问根节点,再访问左子树
  • PrevOrder(root->right);访问完左子树后,访问右子树

我们可以画出递归展开图来加深理解

在这里插入图片描述
由1先访问根节点1,再到左子树2,2访问完根节点,到左子树3

在这里插入图片描述
3访问左子树为空,返回到上一层函数,接着访问3的右子树
在这里插入图片描述
3函数结束,返回到上一层函数,来对2的右子树进行访问,以此类推
在这里插入图片描述
递归展开图可以更好的帮我们理解递归的过程

有了上述的讲解,中序遍历和后续遍历则变得很简单了

中序遍历代码:

void InOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}
  • InOrder(root->left);先访问左子树
  • printf("%d ", root->data);左子树访问完后访问根节点
  • InOrder(root->right);最后访问右子树

后序遍历代码:

void PostOrder(TreeNode* root)
{
	if (root == NULL)
	{
		printf("N ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

代码测试如下:
在这里插入图片描述
与刚刚讨论结果一致!

2. 获取相关个数

2.1获取节点个数

获取二叉树节点个数的核心思想是基于递归遍历整个树并计数。基于二叉树的性质,我们可以采用分治法二叉树的节点总数是其左子树的节点数加上右子树的节点数,再加上根节点本身

基本步骤:

  1. 递归的基准情况:如果当前节点为NULL,意味着到达了叶子节点的外部(空树),返回0。
  2. 递归分解问题:递归地计算左子树的节点个数和右子树的节点个数。
  3. 合并结果:将左子树的节点数和右子树的节点数相加,然后加1(代表当前节点),得到的总和就是整个二叉树的节点个数。
int TreeSize(TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
    return 1 + TreeSize(root->left) + TreeSize(root->right);
}

在上面的代码中,TreeSize函数会递归地遍历整棵树每遇到一个非空节点,就通过递归计算它的左右子树的节点数,然后将这两个数加起来,并加上1(当前节点)。通过这种方式,当递归遍历完整棵树后,我们就可以得到二叉树的节点总数。

我们还可以用三目运算符简化上述代码:

int TreeSize(TreeNode* root)
{
return root==NULL?0:TreeSize(root->left)+1+TreeSize(root->right);
}

测试代码,以上树为例:

    1
   / \
  2   4
 /   / \
3   5   6

在这里插入图片描述

2.2获取叶节点个数

获取二叉树中叶节点(叶子节点)个数的思路与获取节点总数相似,但关注的是那些没有子节点的节点。叶子节点定义为没有左右子树的节点。基于递归的方法,我们可以遍历整棵树,并在遇到叶子节点时对计数器进行增加。

  1. 递归的基准情况:如果当前节点为NULL,意味着到达叶子节点的外部(空树),直接返回0。
  2. 识别叶子节点:如果当前节点既没有左子树也没有右子树,意味着它是一个叶子节点,因此返回1
  3. 递归分解问题:如果当前节点不是叶子节点,递归地计算左子树的叶子节点个数和右子树的叶子节点个数。
  4. 合并结果:将左子树的叶子节点数和右子树的叶子节点数相加,得到的和就是整个二叉树的叶子节点个数
int TreeleafSize(TreeNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	if (root->left == NULL && root->right == NULL)
	{
		return 1;
	}
	else
	{
		return TreeleafSize(root->left) + TreeleafSize(root->right);
	}
}

对于每一个节点,算法检查是否它是一个叶子节点:如果是,返回1;否则,继续在它的左右子树中寻找叶子节点并计数。最终,通过对所有找到的叶子节点进行计数,我们可以得到整个二叉树的叶子节点数。

测试:
在这里插入图片描述

2.3 获取树的高度

获取二叉树的高度(或深度)的基本思路是遵循分治法原则,即递归地计算二叉树每个节点的左右子树的高度,并从中选择最大的一个,然后加1(当前节点在路径上增加的高度)来得到以该节点为根的子树的总高度。树的总高度即是从根节点到最远叶子节点的最长路径上的节点数。

  1. 递归基准:如果当前的节点为NULL,意味着到达了叶子节点的外部(空树),其高度为0。
  2. 递归地检查:递归地计算左子树的高度和右子树的高度。
  3. 选择最大的子树高度并加上当前节点:从左子树和右子树的高度中选择更大的一个,然后加1(当前节点本身)来得到整个子树的高度。

首先我们来看这串代码

int TreeHeight(TreeNode* root) {
	if (root == NULL)
	{
		return 0;
	}
	TreeHeight(root->left);
	TreeHeight(root->right);

	return (TreeHeight(root->left) > TreeHeight(root->right) ? TreeHeight(root->left) : TreeHeight(root->right)) + 1;
}

这串代码有很大的缺陷

三目运算符对 TreeHeight(root->left)TreeHeight(root->right) 各调用了两次。对于每一个非叶子节点,它的左右子树高度都被计算了两遍。这种重复计算对于简单的树结构可能不明显,但对于较大的二叉树来说,就会导致大量不必要的计算,进而降低程序的运行效率。

改进:

int TreeHeight(TreeNode* root) {
	if (root == NULL)
	{
		return 0;
	}
	int leftHeight = TreeHeight(root->left);
	int rightHeight = TreeHeight(root->right);

	return (leftHeight > rightHeight ? leftHeight : rightHeight) + 1;
}

2.4 计算第K层节点个数

计算二叉树第 (k) 层的节点个数同样可以采用递归的方法来解决。这个问题可以拆分为:计算某节点左子树的第 (k-1) 层节点个数和右子树的第 (k-1) 层节点个数的和,因为从当前节点的视角看,它的子树的层数都少了一层。

int TreeKcount(TreeNode* root, int k)
{
	if (root == NULL||k<1)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}

	return TreeKcount(root->left, k - 1) + TreeKcount(root->right, k - 1);
}

当达到第 1 层时,若二叉树非空,则第1层的节点数肯定是1(即当前节点),因为每次递归减少1层,直到递归到目标层级

  • 当我们要求的是第1层的节点数量时(即树的根节点),且树非空,那么节点数量为1。
  • 当 k 值大于1时,我们通过递归调用来求解左右子树在第 (k-1) 层的节点数量,然后将它们相加得到答案。
  • 通过递归减少 k 的值,我们逐渐深入到树中更深的层次,直到 k 减至1,这时我们就到达了目标层,并可以直接计算节点数量

测试代码:

    1
   / \
  2   4
 /   / \
3   5   6

在这里插入图片描述

3.寻找某个节点

在二叉树中寻找值为 x 的节点可以通过递归方法来实现。本质上,这是一种深度优先搜索(DFS)策略。思路是从根节点开始,先检查根节点的值是否等于 x,如果是,就返回当前节点。如果不是,就先在左子树中递归查找,如果左子树中找到了,就直接返回结果;如果左子树中没有找到,再在右子树中查找。如果整棵树都没找到,最终返回 NULL 表示不存在值为 x 的节点。

TreeNode* TreeFind(TreeNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	TreeNode* tmp1 = TreeFind(root->left, x);
	TreeNode* tmp2 = TreeFind(root->right, x);

	if (tmp1)
	{
		return tmp1;
	}
	if (tmp2)
	{
		return tmp2;
	}
	return	NULL;
}

假设我们寻找3

        1
       / \
      2   4
     /   / \
    3   5   6
  1. 开始于根节点(值为1):首先检查根节点的值,它是1,不等于我们要找的值3。

  2. 递归搜索左子树(值为2的节点):进入下一层,移至节点值2,检查这个节点,它也不是我们要找的值。

  3. 递归搜索左子树的左子树(值为3的节点):继续递归进入下一层,移至节点值3,这个节点是我们要找的,因为它的值等于3。函数返回这个节点的地址

这里简单解释一下这段代码的含义:

if (tmp1) {
    return tmp1;
}
if (tmp2) {
    return tmp2;
}
return NULL;
  • tmp1tmp2分别是对当前节点的左子树和右子树递归执行TreeFind函数后得到的结果。tmp1是左子树中查找的结果,而tmp2是右子树中查找的结果。

  • if (tmp1)检查左子树递归查找的结果。如果tmp1不为NULL,这意味着在左子树中找到了值为x的节点,因此直接返回该节点。这里的if (tmp1)、

  • 如果tmp1为NULL,表明左子树中没有找到目标节点,程序将继续检查tmp2,即右子树递归查找的结果。同理,如果tmp2不为NULL(if (tmp2)),意味着在右子树中找到了值为x的节点,函数则返回该节点。

  • 如果两个条件判断if (tmp1)和if (tmp2)都不成立,即tmp1和tmp2都为NULL,表示在当前节点的左右子树中都没有找到值为x的节点,那么函数最终返回NULL,表示查找失败。

4.用前序遍历建造一个二叉树

在上述铺垫后,我们来做一个题:

通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树

这里的#可以当做前面讲的NULL

首先我们定义函数;

TreeNode* CreatTree(BTDataType* a, int *pi);
  • 字符串"ABD##E#H##CF##G##",按照前序遍历构建二叉树,其中’A’是根节点,继续按照根-左-右的顺序解析字符串。
  • 'A'的左子节点是'B''B'的左子节点是'D''D'的左右子节点都是空("##"), 这表示’D’是叶子节点
  • 回到'B’,'B'的右子节点是'E''E'左子节点为空(由"#"表示),右子节点是’H’'H’的左右子节点也都是空
  • 回到根节点'A',它的右子节点是'C''C'的左子节点为空,右子节点是'F'。'F’的左右子节点都是空。
  • 最后,'C'的右子节点是'G',同样,'G’的左右子节点都是空

得到的树结构如下:

    A
   / \
  B   C
 / \   \
D   E   F
     \   \
      H   G

代码如下:

TreeNode* CreatTree(char* a, int* pi)
{
	if (a[*pi] == '#')
	{
		*pi++;
		return NULL;
	}
	TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
	if (root == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	root->data = a[(*pi)++];
	root->left = CreatTree(a, pi);
	root->right = CreatTree(a, pi);
	return root;
}

这里TreeNode的数据类型需要从int 改为char,我们将typedef后面的int修改即可

typedef char BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinartTreeNode* left;
	struct BinartTreeNode* right;
}TreeNode;

5.二叉树的销毁

void DestroyTree(TreeNode* root) {
    if (root == NULL) {
        return;
    }
    DestroyTree(root->left);  // 销毁左子树
    DestroyTree(root->right); // 销毁右子树
    free(root);               // 释放当前节点
}

当你调用DestroyTree并传入树的根节点时,函数首先检查是否给定的节点(在最初的调用中,这是根节点)为NULL。如果不是,它会递归地调用自身来销毁左子树和右子树。完成这些递归调用之后,返回到最初的调用层次时,它会释放根节点本身占用的内存。

  1. 检查root是否为NULL。如果是,说明已经处理到了空子树,函数返回。
  2. 递归调用DestroyTree(root->left);销毁左子树。这将遍历到最左侧的叶子节点,沿途经过的每个节点的左子树都将被先递归调用DestroyTree处理。
  3. 递归调用DestroyTree(root->right);销毁右子树。每个节点的右子树同样会经历相同的递归处理。
  4. 最后,经过左子树和右子树的递归处理后,回到当前的节点,并使用free(root);释放掉该节点占用的内存。

尽管根节点被销毁,但原始root变量本身在函数返回后仍然存在,只是它现在成为了一个悬空指针。为了安全起见,调用完DestroyTree之后,应该手动将root设置为NULL

例如:

int main() {
    TreeNode* root = FCreatTree()
    DestroyTree(root);
    root = NULL; 
    return 0;
}

如果不想手动设置为NULL,只用一个DestroyTree函数来解决,这里意味着我们需要在函数里面实现置空过程,意味着我们要修改指针,则需要传参二级指针

void DestroyTree(TreeNode** root) {
    if (*root == NULL) {
        return;
    }
    DestroyTree(&((*root)->left));
    DestroyTree(&((*root)->right));
    free(*root);
    *root = NULL; // 将调用方的根节点指针置为NULL
}

6.链式二叉树的层序遍历

层序遍历(Level Order Traversal)是指按照从上到下、从左到右的顺序访问二叉树的每个节点。这种遍历方式可以保证二叉树中每个节点的访问顺序正好对应其在树中的层次和位置。层序遍历通常借助于队列这一数据结构来实现。

所以要想实现层序遍历,首先得有队列的函数,在前几节内容中我们已经对其讲解,这里直接引用其代码:

void QueueInit(Queue* pq)
{
	assert(pq);
	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");
		return;
	}
	newnode->val = x;
	newnode->next = NULL;

	if (pq->ptail == NULL)
	{
		pq->ptail = pq->phead = newnode;
	}
	else
	{
		pq->ptail->next = newnode;
		pq->ptail = newnode;
	}
	pq->size++;
}
void QueuePop(Queue* pq) {
	assert(pq);                     // 确保 pq 不是 NULL
	if (pq->phead == NULL) return;  // 如果队列为空,则没有元素可以弹出

	QNode* temp = pq->phead;        // 临时保存队列头部节点的地址,以便后续释放内存
	pq->phead = pq->phead->next;    // 更新头指针指向下一个节点
	free(temp);                     // 释放原头节点占据的内存

	temp = NULL;
	if (pq->phead == NULL) {        // 如果弹出之后队列变为空,则尾指针也要更新为 NULL
		pq->ptail = NULL;
	}

	pq->size--;  // 更新队列大小
}
QDataType QueueFront(Queue* pq)
{
	assert(pq);                     // 确保 pq 不是 NULL
	assert(pq->phead != NULL);
	return pq->phead->val; 

}
QDataType QueueBack(Queue* pq) {
	assert(pq != NULL); // 确保队列指针不为NULL
	assert(pq->ptail != NULL); // 确保队列不为空,即队尾指针不为NULL

	// 返回队列尾部元素的值
	return pq->ptail->val;
}
bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->phead == NULL;
}
int QueueSize(Queue* pq)
{
	assert(pq);
	return pq->size;
}
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;
}

队列在层序遍历中的使用是由于其先进先出(First In First Out, FIFO)的特性,这保证了树的节点能够按照从上到下、从左到右的顺序进行访问,这里解释为什么要用队列进行层序遍历:

  • 在层序遍历过程中,我们首先访问根节点然后是根节点的子节点(从左到右),接着是子节点的子节点,以此类推。队列能够保证节点的访问顺序正好符合这一层级结构,因为我们总是先将左子节点加入队列,然后是右子节点。当从队列中取出一个节点时,它的子节点已经按照正确的顺序排在后面

实现步骤

  • 初始化:创建一个队列,将根节点入队
  • 循环执行以下操作,直到队列为空:
  • 将队头的节点出队,并访问该节点
    1. 如果该节点有左子节点,则将左子节点入队
    2. 如果该节点有右子节点,则将右子节点入队

我们思考一下,这里队列里面放的是什么,节点还是节点的值?

这里放入的是节点,如果只放入节点的值,则找不到下一个节点,所以这里我们需要修改队列的typedef类型!

typedef struct BinaryTreeNode* QDataType;
typedef struct QueueNode
{
	QDataType val;
	struct QueueNode* next;
}QNode;
typedef struct Queue 
{
	QNode* phead;
	QNode* ptail;
	int size;
}Queue;

接下来实现代码:

void LevelOrder(TreeNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root != NULL)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		TreeNode* front = QueueFront(&q);
		QueuePop(&q);

		printf("%d ", front->data);
		if (front->left != NULL)
			QueuePush(&q, front->left);
		if (front->right != NULL)
			QueuePush(&q, front->right);

	}
	QueueDestroy(&q);
}

我们来进行分析:
以最开始的那棵树为例子:

       1
      / \
     2   4
    /   / \
   3   5   6

让我们展开遍历的过程:

  1. 开始时,队列状态:1
  2. 取出1,打印1,加入1的左子节点2和右子节点4,队列状态:2, 4
  3. 取出2,打印2,加入2的左子节点3,队列状态:4, 3
  4. 取出4,打印4,加入4的左子节点5和右子节点6,队列状态:3, 5, 6
  5. 取出3,打印3,队列状态:5, 6
  6. 取出5,打印5,队列状态:6
  7. 取出6,打印6,队列空

打印顺序:1, 2, 4, 3, 5, 6。

7.判断是否为完全二叉树

判断一棵树是否为完全二叉树,可以利用层序遍历的思想。完全二叉树的特点是,除了最后一层外,每一层都是完全填满的,且最后一层的所有节点都集中在左侧。在层序遍历过程中,如果遇到了一个节点,其有右子节点而无左子节点,那么这棵树肯定不是完全二叉树;另外,如果遇到了一个节点并非左右子节点都有,那么所有接下来遍历到的节点都必须是叶子节点。

我们再原有层序遍历代码上修改即可:

bool BinaryTreeComplete(TreeNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root != NULL)
	{
		QueuePush(&q, root);
	}
	while (!QueueEmpty(&q))
	{
		TreeNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
			break;

		QueuePush(&q, front->left);
		QueuePush(&q, front->right);

	}
	while (!QueueEmpty(&q))
	{
		TreeNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front)
		{
			QueuePop(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

第一部分:将所有节点(包括空节点)按层序添加到队列中

  1. 首先,初始化一个队列 q。
  2. 如果根节点不为空,则将根节点添加到队列中。
  3. 接下来进入一个循环,直到队列为空。在这个循环中:
    • 从队列中弹出一个节点(记为 front)。
    • 如果 front 是 NULL,即遇到了第一个空节点,则退出循环。这是因为在完全二叉树中,一旦按层序遍历遇到空节点,其后的所有节点也应该是空的
    • 如果 front 非空,将其左右子节点(即使是空的)添加到队列中。这样做是为了保证即使是空节点也按照层序顺序排列。

第二部分:检查队列中剩余的所有节点是否都是空的

退出第一部分的循环后,理论上队列中剩下的所有节点应该都是空节点(如果是完全二叉树的话)。因此,接下来的循环会遍历队列中剩余的所有节点:

从队列中弹出一个节点(记为 front)。
如果 front 是非空的,说明在遇到第一个空节点之后还有非空节点,这违反了完全二叉树的性质。因此,函数返回 false。
如果循环顺利结束,没有遇到任何非空节点,说明这棵树是一棵完全二叉树,函数返回 true。

本节内容到此结束!!感谢大家阅读!!!
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/445655.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

前端请求到 SpringMVC 的处理流程

1. 发起请求 客户端通过 HTTP 协议向服务器发起请求。 2. 前端控制器&#xff08;DispatcherServlet&#xff09; 这个请求会先到前端控制器 DispatcherServlet&#xff0c;它是整个流程的入口点&#xff0c;负责接收请求并将其分发给相应的处理器。 3. 处理器映射&#xf…

数据库-多表查询

外连接与内连接 -- 查询部门及所属部门名称&#xff0c;隐式内连接 select tb_emp.name,tb_dept.name from tb_emp,tb_dept where tb_emp.dept_idtb_dept.id;-- 起别名 select e.name,q.name from tb_emp e,tb_dept q where e.dept_idq.id;-- 外连接 select tb_emp.name,tb_dep…

GEE图像可视化常用函数

目录 图层操作Map.addLayer&#xff08;&#xff09;Map.centerObject&#xff08;&#xff09; 直方图ui.Chart.image.histogram&#xff08;&#xff09; 趋势线ui.Chart.image.series&#xff08;&#xff09; 图层操作 Map.addLayer&#xff08;&#xff09; Map.addLaye…

python并发编程:异步IO(Asynchronous I/O)

异步IO(Asynchronous I/O) Linux下的asynchronous IO其实用得不多&#xff0c;从内核2.6版本才开始引入。先看一下它的流程&#xff1a; 用户进程发起read操作之后&#xff0c;立刻就可以开始去做其它的事。而另一方面&#xff0c;从kernel的角度&#xff0c;当它受到一个asyn…

RocketMQ、Kafka、RabbitMQ 消费原理,顺序消费问题【图文理解】

B站视频地址 文章目录 一、开始二、结果1、RocketMQ 消费关系图1-1、queue和consumer的关系1-2、consumer 和线程的关系 2、Kafka 消费关系图1-1、partitions和consumer的关系1-2、consumer 和线程的关系 3、RabbitMQ 消费关系图1-1、queue和consumer的关系1-2、consumer 和线程…

爬虫练习:获取某招聘网站Python岗位信息

一、相关网站 二、相关代码 import requests from lxml import etree import csv with open(拉钩Python岗位数据.csv, w, newline, encodingutf-8) as csvfile:fieldnames [公司, 规模,岗位,地区,薪资,经验要求]writer csv.DictWriter(csvfile, fieldnamesfieldnames)writer…

每日OJ题_牛客WY28 跳石板(动态规划)

目录 牛客WY28 跳石板 解析代码 牛客WY28 跳石板 跳石板_牛客题霸_牛客网 解析代码 #include <iostream> #include <vector> #include <climits> #include <cmath> using namespace std;void get_div_num(int n, vector<int>& arr) {for…

selenium元素定位问题

具体网页信息如下&#xff1a; 定位的时候driver.find_element(By.CLASS_NAME, 方法搞不定。 定位方法&#xff1a; 方法一&#xff1a;通过文本定位 driver.find_element(By.XPATH, "//*[text()高分一号]").click() time.sleep(3) 如果是部分文字 #部分文字py…

怎么写品牌方流量打造抖音运营规划方案

【干货资料持续更新&#xff0c;以防走丢】 怎么写品牌方流量打造抖音运营规划方案 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 抖音运营资料合集&#xff08;完整资料包含以下内容&#xff09; 目录 Step 1: 人货沟通策略 人群定位与细分 1. 从品牌及产品…

【备战蓝桥杯系列】蓝桥杯国二选手笔记二:算法模版笔记(Java)

感谢大家的点赞&#xff0c;关注&#xff0c;评论。准备蓝桥杯的同学可以关注一下本专栏哦&#xff0c;不定期更新蓝桥杯笔记以及经验分享。本人多次参加过蓝桥杯&#xff0c;并获得过蓝桥杯国二的成绩。 算法模版笔记&#xff08;Java&#xff09; 这篇文章给大家分享我的蓝桥…

寒假作业Day 10

寒假作业Day 10 一、选择题 1、下列数据结构中&#xff0c;不属于线性表的是( ) A.队列 B.顺序表 C.二叉树 D.链表 A. 队列&#xff1a;队列是一种特殊的线性表&#xff0c;它只允许在表的前端&#xff08;front&#xff09;进行删除操作&#xff0c;而在表的后端&#xff08…

【经管数据-更新】华证ESG评级得分数据(2009-2023年)

一、数据说明 参考《经济研究》中方先明&#xff08;2023&#xff09;的做法&#xff0c;将华证ESG评级进行赋值&#xff0c;指标包含C、CC、CCC、B、BB、BBB、A、AA、AAA共9个等级&#xff0c;将上市公司ESG 等级从低到高分别赋值为1至9 二、数据来源&#xff1a;世界银行&am…

Springboot进行web开发

创建springboot工程&#xff0c;基于2022版idea pom.xml文件中的插件爆红&#xff1a; 解决方法&#xff1a;给插件加<version>版本号</version> 版本号和<parent></parent>中的版本号一样。 另外有人说重启也可以解决爆红&#xff0c;可以试一下&a…

Stable diffusion(一)

Stable diffusion 原理解读 名词解释 正向扩散&#xff08;Fixed Forward Diffusion Process&#xff09;&#xff1a;反向扩散&#xff08;Generative Reverse Denoising Process&#xff09; VAE&#xff08;Variational AutoEncoder&#xff09;&#xff1a;一个用于压缩图…

【动态规划】【前缀和】【和式变换】100216. K 个不相交子数组的最大能量值

本文涉及知识点 动态规划汇总 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 LeetCode 100216. K 个不相交子数组的最大能量值 给你一个长度为 n 下标从 0 开始的整数数组 nums 和一个 正奇数 整数 k 。 x 个子数组的能量值定义为 stren…

Swagger修改Api文档中的数据类型

swagger不陌生,API接口利器,本次要解决的问题是:我们知道前端在接收Long类型的属性时会出现精度问题,一般我们会在序列化的时候将Long类型的数字转换成String但是swagger的API文档中的类型还是Long,我们要解决的就是这个问题 不知道swagger怎么配置得可以看之前的文章:springb…

空间复杂度的OJ练习——轮转数组

旋转数组OJ链接&#xff1a;https://leetcode-cn.com/problems/rotate-array/ 题目&#xff1a; 思路&#xff1a; 通过题目我们可以知道这是一个无序数组&#xff0c;只需要将数组中的数按给定条件重新排列&#xff0c;因此我们可以想到以下几种方法&#xff1a; 1.暴力求解法…

【Tauri】(5):本地运行candle和 qwen 大模型,并测试速度

1&#xff0c;本地运行candle 关于candle项目 https://github.com/huggingface/candle Hugging Face 使用rust开发的高性能推理框架。 语法简单&#xff0c; 风格与 PyTorch 相似。 CPU 和 Cuda Backend&#xff1a;m1、f16、bf16。 支持 Serverless&#xff08;CPU&#xff…

React 从0到1构建企业级框架基于Antd Designer

一、 create-react-app 创建 cms-front 二、 删除不必须要的文件形成如下结构 1. React版本为17版本 public 文件夹下保留 favicon.ico 偏爱图标index.html资源文件 2.src 保留 index.js 入口文件和app.js(基于spa原则)单文件即可 三、配置eslint 1. 安装 eslint. npm inst…

章六、集合(1)—— Set 接口及实现类、集合迭代、Map 接口、Collections类

一、 Set 接口及实现类 Set接口不能存储重复元素 Set接口继承了Collection接口。Set中所存储的元素是不重复的,但是是无序的, Set中的元素是没有索引的 Set接口有两个实现类&#xff1a; ● HashSet &#xff1a;HashSet类中的元素不能重复 ● TreeSet &#xff1a;可以给Set集…