堆排序与链式二叉树:数据结构与排序算法的双重探索

d8796d6112a840a89812634c4f357860.gif

大家好,我是小卡皮巴拉

文章目录

目录

引言

一.堆排序

1.1 版本一

核心概念

堆排序过程

1.2 版本二

堆排序函数 HeapSort

向下调整算法 AdjustDown

向上调整算法 AdjustUp

二.链式二叉树

2.1 前中后序遍历

链式二叉树的结构

创建链式二叉树

前序遍历

中序遍历

后序遍历

2.2 链式二叉树的常见操作 

二叉树的结点个数

二叉树叶子结点的个数

二叉树第k层结点的个数

求二叉树的高度/深度

二叉树查找值为x的结点

2.3 层序遍历

使用层序遍历检验是否为完全二叉树

兄弟们共勉 !!! 


 

每篇前言

博客主页:小卡皮巴拉

咱的口号:🌹小比特,大梦想🌹

作者请求:由于博主水平有限,难免会有错误和不准之处,我也非常渴望知道这些错误,恳请大佬们批评斧正。

d89d34950d1342d5a1e07d45c08f238a.png

引言

在数据结构与算法的广阔天地中,堆排序与链式二叉树无疑是两颗璀璨的明珠。它们各自以其独特的魅力和广泛的应用场景,在数据处理和算法优化中发挥着举足轻重的作用。

堆排序,作为一种基于堆数据结构的比较排序算法,以其高效的排序速度和稳定的性能表现,成为了众多排序算法中的佼佼者。它利用堆的性质,通过一系列精心设计的操作,将无序的数据逐步转化为有序,从而实现了数据的快速排序。

而链式二叉树,则以其灵活的节点连接方式和高效的查找性能,成为了数据结构中不可或缺的一部分。它通过将数据元素组织成二叉树的形式,利用节点的指针域实现数据的动态存储和访问,为数据的查找、插入和删除等操作提供了极大的便利。

今天,我们将一起踏上一段探索之旅,深入剖析堆排序的算法实现与链式二叉树的构建细节。从堆的构建、调整与排序过程,到链式二叉树的节点定义、插入与遍历操作,我们将一步步揭开这两大数据结构的神秘面纱,带你领略它们背后的智慧与魅力。

一.堆排序

1.1 版本一

版本一:基于已有数组建堆、取堆顶元素完成排序版本

void HeapSort(int* arr,int n)
{HP hp;HPInit(&hp);//调用push将数组中的数据建堆for (int i = 0; i < n; i++){HPPush(&hp, arr[i]);}int i = 0;while (!HPEmpty(&hp)){arr[i++] = HPTop(&hp);HPPop(&hp);}HPDesTroy(&hp);
}

核心概念

  • :一种特殊的完全二叉树,用于实现堆排序。这里我们假设使用最大堆。

  • 空间复杂度:O(N),因为使用了额外的堆数据结构。

堆排序过程

  1. 构建堆

    • 将数组a中的元素逐个添加到堆hp中。

    • 注意:传统堆排序直接在输入数组上构建堆,这里为了教学目的使用了额外的堆结构。

  2. 排序

    • 当堆不为空时,重复以下步骤:

      • 取出堆顶元素(最大值),将其放到数组a的当前位置。

      • 从堆中移除堆顶元素。

    • 这个过程会持续到堆为空,此时数组a将按降序排列。

  3. 清理

    • 销毁堆hp,释放任何动态分配的内存。

1.2 版本二

数组建堆,首尾交换,交换后的堆尾数据从堆中删掉,将堆顶数据向下调整选出次大的数据

堆排序函数 HeapSort

ab5aea7c4de34a94b3ee05e112f9fa02.png

  1. 函数定义void HeapSort(int* arr, int n)

    • arr:指向待排序数组的指针。

    • n:数组的长度。

  2. 建堆过程

    • 循环从最后一个非叶子节点开始((n - 1 - 1) / 2),逐步向上至根节点(i >= 0),对每个节点调用AdjustDown以确保以该节点为根的子树满足堆性质。

    • 注释中提到的向上调整算法AdjustUp被注释掉了,但其实用向上调整算法也是能够建堆的(但向下调整更为高效)。

  3. 排序过程

    • 如果要进行升序排序,需要构建最大堆(大堆);如果要进行降序排序,则需要构建最小堆(小堆)。

    • 通过交换堆顶(当前最大值)与数组末尾元素,并减小堆的大小(end--),然后调用AdjustDown来维护堆的性质,从而实现排序。

    • 这个过程重复进行,直到堆的大小减为1,此时数组已排序完成。

//堆排序
void HeapSort(int* arr, int n)
{//根据给定的arr来进行建堆//child;n-1 parent:(n-1-1)/2//向下调整算法建堆for (int i = (n - 1 - 1) / 2; i >= 0; i--){AdjustDown(arr, i, n);}//向上调整算法建堆/*for (int i = 0; i < n; i++){AdjustUp(arr, i);}*///堆排序//排升序——建大堆//排降序——建小堆//建小堆大堆时,主要影响因素在向下调整算法中://1.左孩子和右孩子的比较//2.孩子和父亲的比较//如果要从构造小堆改为构造大堆,两个判断条件的><都要取反int end = n - 1;while (end > 0){Swap(&arr[0], &arr[end]);AdjustDown(arr, 0, end);end--;}
}

向下调整算法 AdjustDown

6f573b42e20245b8b8c4b5e0b4d24154.png

83208cf55c2146a58cdf4e9f39707f65.png

a1e0441fce334ab9bda3f1c09fce63e7.png

  1. 函数定义void AdjustDown(HPDataType* arr, int parent, int n)

    • arr:指向堆数组的指针(这里HPDataTypeint)。

    • parent:当前需要调整的父节点索引。

    • n:堆的大小(即当前堆中元素的数量)。

  2. 调整过程

    • 计算左孩子节点的索引(child = parent * 2 + 1)。

    • while循环中,首先检查是否存在右孩子,并且右孩子的值是否小于左孩子的值。如果是,则更新child为右孩子的索引。

    • 接着,比较父节点与孩子节点的值。如果父节点的值大于孩子节点的值(对于最大堆而言),则交换它们,并继续向下调整(更新parentchild)。

    • 如果父节点的值不大于孩子节点的值,则跳出循环,因为当前子树已经满足堆性质。

//向下调整算法
void AdjustDown(HPDataType* arr, int parent,int n)
{int child = parent * 2 + 1;while (child < n){//先找最小的孩子if (child + 1 < n && arr[child] > arr[child + 1]){child++;}//parent和child比较if (arr[child] < arr[parent]){Swap(&arr[child], &arr[parent]);parent = child;child = parent * 2 + 1;}else{break;}}
}

向上调整算法 AdjustUp

c0b378a102d248a1844142cd1d38518c.png

1ace71712b5c41a6a4825fb41b9648f3.png

eed10ac99fef44ee9f7b1aede13c873b.png

函数定义void AdjustUp(HPDataType* arr, int child)

  • arr:指向堆数组的指针(这里HPDataTypeint类型,表示堆中存储的是整数)。

  • child:当前需要向上调整的节点的索引(也称为“孩子”节点)。

调整过程

  1. 初始化父节点索引

    • 根据孩子节点索引child,计算出父节点索引parent = (child - 1) / 2

  2. 循环调整

    • child大于0,并且孩子节点的值大于父节点的值时(对于最大堆):

      • 交换孩子节点和父节点的值。

      • 更新child为当前父节点的索引。

      • 重新计算新的父节点索引。

  3. 终止条件

    • child不大于0,或者孩子节点的值不大于新的父节点的值时,停止调整。

//向上调整算法
void AdjustUp(HPDataType* arr, int child)
{int parent = (child - 1) / 2;while (child > 0){// > 大堆// < 小堆 if (arr[child] > arr[parent]){Swap(&arr[child], &arr[parent]);child = parent;parent = (child - 1) / 2;}else {break;}}
}

注意事项

  • 注释中提到的向上调整算法AdjustUp函数没有使用,在堆排序中,它通常用于在插入新元素后维护堆的性质,但是此处也是可以使用向上调整算法来建堆的,在这个特定的实现中,由于是从一个无序数组开始构建堆,所以使用向下调整算法AdjustDown更为高效

  • 堆排序是一种原地排序算法,因为它只需要一个额外的常数空间来存储临时变量(如childparent等),而不需要额外的数组来存储数据。

  • 建小堆大堆时,主要影响因素在向下调整算法中:
    1.左孩子和右孩子的比较
    2.孩子和父亲的比较
    如果要从构造小堆改为构造大堆,两个判断条件的><都要取反

二.链式二叉树

2.1 前中后序遍历

二叉树的操作离不开树的遍历,我们先来看看二叉树的遍历有哪些方式

遍历规则

按照规则,二叉树的遍历有:前序/中序/后序的递归结构遍历:

1)前序遍历(PreorderTraversal亦称先序遍历):访问根结点的操作发生在遍历其左右子树之前 访 问顺序为:根结点、左子树、右子树

2)中序遍历(InorderTraversal):访问根结点的操作发生在遍历其左右子树之中(间) 访问顺序为:左子树、根结点、右子树

3)后序遍历(PostorderTraversal):访问根结点的操作发生在遍历其左右子树之后 访问顺序为:左子树、右子树、根结点

图解遍历:

以前序遍历为例:370aa0512cd641a398431efdb8837472.png

函数递归栈帧图:

9d4995e328c741a4b2542dfb79cd9abd.png前序遍历结果:1 2 3 4 5 6 

中序遍历结果:3 2 1 5 4 6

后序遍历结果:3 1 5 6 4 1

后面会进行代码的实现

链式二叉树的结构

结构体名称BTNode

目的:定义二叉树节点。

字段

  1. BTDataType data;
    • 类型:字符(char的别名BTDataType
    • 描述:节点值。
  2. BTNode* left;
    • 类型:指向BTNode的指针
    • 描述:左孩子节点。
  3. BTNode* right;
    • 类型:指向BTNode的指针
    • 描述:右孩子节点。

实现代码:

typedef char BTDataType;
//二叉树结点结构的定义
typedef struct BinaryTreeNode
{BTDataType data;//当前结点值域struct BinaryTreeNode* left;//指向当前结点左孩⼦struct BinaryTreeNode* right;//指向当前结点右孩⼦
}BTNode;

创建链式二叉树

函数一:申请新节点

函数名buyNode

参数

  • BTDataType x:要存储在新节点中的值。

返回值

  • 指向新创建的BTNode结构体的指针。

功能

  • 分配内存以创建一个新的BTNode节点。
  • 检查内存分配是否成功,如果失败则打印错误信息并退出程序。
  • 初始化新节点的值域为参数x
  • 将新节点的左右孩子指针初始化为NULL
  • 返回指向新节点的指针。

函数二:手动构造二叉树

函数名CreateTree

参数:无

返回值

  • 指向二叉树根节点的指针。

功能

  • 调用buyNode函数创建六个新节点,分别存储字符'A'到'F'。
  • 设置这些节点之间的父子关系,以构造一个特定的二叉树结构:
    • 'A'是根节点。
    • 'B'和'C'是'A'的左右孩子。
    • 'D'和'E'是'B'的左右孩子。
    • 'F'是'C'的左孩子。
  • 返回指向根节点'A'的指针。

实现代码:

//申请新结点
BTNode* buyNode(BTDataType x)
{BTNode* node = (BTNode*)malloc(sizeof(BTNode));if (node == NULL){perror("malloc fail!");exit(1);}node->data = x;node->left = node->right = NULL;return node;
}
//手动构造一颗二叉树
BTNode* CreateTree()
{BTNode* nodeA = buyNode('A');BTNode* nodeB = buyNode('B');BTNode* nodeC = buyNode('C');BTNode* nodeD = buyNode('D');BTNode* nodeE = buyNode('E');BTNode* nodeF = buyNode('F');nodeA->left = nodeB;nodeA->right = nodeC;nodeB->left = nodeD;nodeB->right = nodeE;nodeC->left = nodeF;return nodeA;
}

前序遍历

函数名PreOrder

参数

  • BTNode* root:指向二叉树根节点的指针。

返回值:无

功能

  • 实现二叉树的前序遍历算法。
  • 前序遍历的顺序是:首先访问根节点,然后递归地前序遍历左子树,最后递归地前序遍历右子树。

实现细节

  1. 递归终止条件:如果当前节点(root)为空,则打印"NULL ",并返回。这个条件确保了递归能够正确终止。然而,在标准的前序遍历实现中,通常不会打印"NULL",因为空节点不产生输出。这里的打印是为了调试或满足特定要求。

  2. 访问根节点:打印当前节点的值(root->data)。

  3. 递归遍历左子树:调用PreOrder函数,传入当前节点的左孩子(root->left)作为参数。

  4. 递归遍历右子树:调用PreOrder函数,传入当前节点的右孩子(root->right)作为参数。

实现代码:

//前序遍历——根左右
void PreOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}printf("%c ", root->data);PreOrder(root->left);PreOrder(root->right);
}

结合上述创建的链式二叉树,其运行结果为:

1a9c0af73274488caedea0087ba7b84e.png

中序遍历

函数名InOrder

参数

  • BTNode* root:指向二叉树根节点的指针。

返回值:无

功能

  • 实现二叉树的中序遍历算法。
  • 中序遍历的顺序是:首先递归地中序遍历左子树,然后访问根节点,最后递归地中序遍历右子树。

实现细节

  1. 递归终止条件:如果当前节点(root)为空,则打印"NULL "(这里的打印是为了便于调试)。

  2. 递归遍历左子树:调用InOrder函数,传入当前节点的左孩子(root->left)作为参数。

  3. 访问根节点:在左子树遍历完成后,打印当前节点的值(root->data)。

  4. 递归遍历右子树:调用InOrder函数,传入当前节点的右孩子(root->right)作为参数。

实现代码:

//中序遍历——左根右
void InOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}InOrder(root->left);printf("%c ", root->data);InOrder(root->right);
}

打印结果:

c06af63010154135948fb8a8d757537d.png

后序遍历

函数名PostOrder

参数

  • BTNode* root:指向二叉树根节点的指针。

返回值:无

功能

  • 实现二叉树的后序遍历算法。
  • 按照“左子树-右子树-根节点”的顺序访问节点。

实现细节

  1. 递归终止条件:如果当前节点(root)为空,则按照代码逻辑会打印"NULL "。

  2. 递归遍历左子树:调用PostOrder函数,传入当前节点的左孩子(root->left)作为参数,进行递归后序遍历。

  3. 递归遍历右子树:同样地,调用PostOrder函数,传入当前节点的右孩子(root->right)作为参数,进行递归后序遍历。

  4. 访问根节点:在左右子树都被遍历之后,打印当前节点的值(root->data)。

实现代码:

//后序遍历——左右根
void PostOrder(BTNode* root)
{if (root == NULL){printf("NULL ");return;}PostOrder(root->left);PostOrder(root->right);printf("%c", root->data);
}

打印结果:

2096c8829dd24a1e89532795a75120b3.png

2.2 链式二叉树的常见操作 

二叉树的结点个数

函数名:BinaryTreeSize

参数

  • BTNode* root:指向二叉树根节点的指针。

返回值

  • int:返回二叉树的节点总数。

功能

  • 实现计算二叉树节点总数的算法。
  • 递归地计算左子树和右子树的节点数,并将它们与根节点相加得到总节点数。

实现细节

  • 递归终止条件:如果当前节点(root)为空,则根据二叉树的定义,空树没有节点,返回0。

  • 递归计算左子树节点数:调用BinaryTreeSize函数,传入当前节点的左孩子(root->left)作为参数,递归地计算左子树的节点总数。

  • 递归计算右子树节点数:同样地,调用BinaryTreeSize函数,传入当前节点的右孩子(root->right)作为参数,递归地计算右子树的节点总数。

  • 计算总节点数:将左子树的节点数、右子树的节点数与根节点(1个)相加,得到当前二叉树的总节点数。

实现代码

//二叉树的结点个数
int BinaryTreeSize(BTNode* root)
{if (root == NULL){return 0;}//递归//每一颗二叉树的结点个数都可以表示为//根结点(1)+左子树结点个数+右子树结点个数return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}

二叉树叶子结点的个数

函数名:BinaryTreeLeafSize

参数

  • BTNode* root:指向二叉树根节点的指针。

返回值

  • int:返回二叉树中叶子节点的总数。

功能

  • 实现计算二叉树叶子节点总数的算法。
  • 递归地检查每个节点是否为叶子节点,并累加叶子节点的数量。

实现细节

  • 递归终止条件:如果当前节点(root)为空,则根据二叉树的定义,空树没有叶子节点,返回0。

  • 叶子节点判断:如果当前节点的左孩子(root->left)和右孩子(root->right)都为空,则当前节点是叶子节点,返回1。

  • 递归计算叶子节点数:如果当前节点不是叶子节点,则递归地调用BinaryTreeLeafSize函数,分别传入当前节点的左孩子和右孩子作为参数,计算左子树和右子树的叶子节点总数,并将这两个数相加得到当前二叉树的叶子节点总数。

实现代码

 

//二叉树叶子结点的个数
int BinaryTreeLeafSize(BTNode* root)
{if (root == NULL){return 0;}//递归//叶子结点:左孩子和右孩子都为空if (root->left == NULL && root->right == NULL){return 1;}//总数:左子树叶子结点个数+右子树叶子结点个数return BinaryTreeLeafSize(root->left) + BinaryTreeLeafSize(root->right);
}

二叉树第k层结点的个数

函数名:BinaryTreeLevelKSize

参数

  • BTNode* root:指向二叉树根节点的指针。
  • int k:指定的层数,从1开始计数。

返回值

  • int:返回二叉树第k层节点的总数。

功能

  • 实现计算二叉树第k层节点总数的算法。
  • 递归地遍历二叉树,直到达到指定的层数,然后累加该层的节点数。

实现细节

  • 递归终止条件
    • 如果当前节点(root)为空,则根据二叉树的定义,该层没有节点,返回0。
    • 如果当前层数k等于1,说明已经到达了根节点所在的层(根节点位于第1层),因此该层有1个节点,返回1。
  • 递归计算第k层节点数
    • 如果当前节点不是空节点且当前层数k大于1,则递归地调用BinaryTreeLevelKSize函数,分别传入当前节点的左孩子和右孩子作为新的根节点,并将层数k减1(因为向下递归一层),计算左子树和右子树在第k-1层的节点总数。
    • 将左子树和右子树在第k-1层的节点数相加,得到当前二叉树在第k层的节点总数。

注意

  • 这里的递归逻辑有一点需要注意,即当我们想要计算第k层的节点数时,实际上是递归地去计算左子树和右子树在第k-1层的节点数。这是因为当我们从根节点开始递归时,根节点是第1层,它的子节点(左孩子和右孩子)位于第2层,依此类推。

实现代码

//二叉树第k层结点的个数
int BinaryTreeLevelKSize(BTNode* root, int k)
{if (root == NULL){return 0;}if (k == 1){return 1; // 根节点所在层,只有1个节点}//递归//总数:左子树第k-1层结点的个数+右子树第k-1层结点的个数return BinaryTreeLevelKSize(root->left, k - 1) + BinaryTreeLevelKSize(root->right, k - 1);
}

求二叉树的高度/深度

函数名:BinaryTreeDepth

参数

  • BTNode* root:指向二叉树根节点的指针。

返回值

  • int:返回二叉树的高度(或深度),即从根节点到最远叶子节点的最长路径上的节点数。

功能

  • 实现计算二叉树高度的算法。
  • 递归地计算左子树和右子树的高度,并返回其中较大的高度值加上1(根节点的高度)。

实现细节

  • 递归终止条件:如果当前节点(root)为空,则根据二叉树的定义,空树的高度为0。

  • 递归计算子树高度:分别调用BinaryTreeDepth函数,传入当前节点的左孩子(root->left)和右孩子(root->right)作为参数,递归地计算左子树和右子树的高度。

  • 计算总高度:将左子树的高度(leftDep)和右子树的高度(rightDep)进行比较,取其中较大的值,然后加上1(根节点的高度),得到当前二叉树的总高度。

实现代码

//求二叉树的高度/深度
int BinaryTreeDepth(BTNode* root)
{if (root == NULL){return 0;}//递归计算左子树和右子树的高度int leftDep = BinaryTreeDepth(root->left);int rightDep = BinaryTreeDepth(root->right);//返回当前树的高度:根节点的高度(1)加上左右子树中较大的高度return 1 + (leftDep > rightDep ? leftDep : rightDep);
}

二叉树查找值为x的结点

函数名:BinaryTreeFind

参数

  • BTNode* root:指向二叉树根节点的指针。
  • BTDataType x:要查找的目标值,其类型应与二叉树节点的数据类型相匹配。

返回值

  • BTNode*:返回指向找到的值为x的节点的指针;如果未找到,则返回NULL

功能

  • 实现在二叉树中查找值为x的节点的算法。
  • 递归地遍历二叉树,直到找到值为x的节点或遍历完整个树。

实现细节

  • 递归终止条件:如果当前节点(root)为空,则根据二叉树的定义,该节点下不存在任何子节点,因此返回NULL表示未找到。

  • 查找目标值:如果当前节点的值(root->data)等于目标值x,则找到了目标节点,返回当前节点的指针。

  • 递归查找

    • 首先,递归地调用BinaryTreeFind函数,传入当前节点的左孩子(root->left)作为新的根节点和相同的目标值x,尝试在左子树中查找目标节点。
    • 如果左子树中找到了目标节点(即leftFind不为NULL),则返回该节点的指针。
    • 如果左子树中没有找到目标节点,则继续递归地调用BinaryTreeFind函数,传入当前节点的右孩子(root->right)作为新的根节点和相同的目标值x,尝试在右子树中查找目标节点。
    • 如果右子树中找到了目标节点(即rightFind不为NULL),则返回该节点的指针。
  • 未找到目标值:如果左子树和右子树中都没有找到目标节点,则返回NULL表示在整个二叉树中未找到值为x的节点。

实现代码

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{//先遍历左子树//找到了,直接返回//没有找到,再去遍历右子树if (root == NULL){return NULL;}if (root->data == x){return root;}BTNode* leftFind = BinaryTreeFind(root->left, x);if (leftFind){return leftFind;}BTNode* rightFind = BinaryTreeFind(root->right, x);if (rightFind){return rightFind;}return NULL;
}

2.3 层序遍历

除了先序遍历、中序遍历、后序遍历外,还可以对二叉树进行层序遍历。设二叉树的根结点所在层数为1,层序遍历就是从所在二叉树的根结点出发,首先访问第1层的树根结点,然后从左到右访问第2层上的结点,接着是第3层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历,实现层序遍历需要额外借助数据结构:队列

2eef706bacf64ed09b23c32e2604cf30.png

函数名LevelOrder

参数

  • BTNode* root:指向二叉树根节点的指针。

功能

  • 实现二叉树的层序遍历(广度优先遍历),通过队列数据结构辅助完成。

实现细节

  1. 初始化队列
    • 创建一个Queue类型的变量q
    • 调用QueueInit(&q)函数初始化队列q,使其处于可用状态。
  2. 根节点入队
    • 调用QueuePush(&q, root)函数,将二叉树的根节点root添加到队列q的末尾。
  3. 遍历队列
    • 使用while循环遍历队列q,条件是队列不为空(!QueueEmpty(&q))。
    • 在循环体内,首先调用QueueFront(&q)函数获取队列头部的节点指针,并将其赋值给BTNode* top。这里需要注意的是,QueueFront只是获取队列头部的节点指针,并不会将该节点从队列中移除。
    • 紧接着,调用QueuePop(&q)函数从队列中移除刚才通过QueueFront获取的节点(即top指向的节点)。
    • 使用printf("%c", top->data);语句打印当前节点(即top指向的节点)的值。这里假设节点值的数据类型为字符,因此使用%c格式说明符。
  4. 孩子节点入队
    • 检查当前节点(top)的左孩子是否存在(top->left)。如果存在,则调用QueuePush(&q, top->left);将其添加到队列q的末尾。
    • 同样地,检查当前节点的右孩子是否存在(top->right)。如果存在,则调用QueuePush(&q, top->right);将其添加到队列q的末尾。
  5. 销毁队列
    • 遍历完成后,调用QueueDestroy(&q)函数销毁队列q,并释放其占用的资源。

代码实现:

//层序遍历
void LevelOrder(BTNode* root)
{//借助数据结构——队列Queue q;QueueInit(&q);QueuePush(&q, root);while (!QueueEmpty(&q)){//取队头,出队头BTNode* top = QueueFront(&q);QueuePop(&q);printf("%c", top->data);//将队头左右孩子入队列(不为空)if (top->left){QueuePush(&q, top->left);}if (top->right){QueuePush(&q, top->right);}}QueueDestroy(&q);
}

上述是完整的解析,其中用队列实现层序遍历的核心思路如下: 

  1. 初始化队列
    • 创建一个空队列,用于存储待访问的节点。
  2. 根节点入队
    • 将二叉树的根节点加入队列。
  3. 循环遍历
    • 当队列不为空时,执行以下步骤:
      a. 从队列头部取出一个节点(访问该节点,例如打印其值)。
      b. 如果该节点有左孩子,将左孩子加入队列。
      c. 如果该节点有右孩子,将右孩子加入队列。
  4. 结束
    • 当队列为空时,遍历结束,所有节点都已按层次顺序访问过。

学习了层序遍历,下面我们用层序遍历来检验二叉树是否为完全二叉树

使用层序遍历检验是否为完全二叉树

函数名BinaryTreeComplete

参数

  • BTNode* root:指向二叉树根节点的指针。

返回值

  • bool:如果二叉树是完全二叉树,则返回true;否则返回false

功能

  • 该函数通过层序遍历的方式验证给定的二叉树是否为完全二叉树。

实现细节

  1. 空树处理
    • 首先检查根节点root是否为NULL。如果是,根据完全二叉树的定义(有时空树也被认为是完全二叉树),函数返回true
  2. 队列初始化
    • 创建一个名为q的队列,并调用QueueInit(&q)函数对其进行初始化。
  3. 根节点入队
    • 将二叉树的根节点root通过QueuePush(&q, root)函数加入队列q
  4. 遍历与检查
    • 使用while循环遍历队列q,条件是队列不为空(!QueueEmpty(&q))。
    • 在循环内部,首先通过QueueFront(&q)函数获取队列头部的节点指针,并将其赋值给BTNode* node
    • 随后,调用QueuePop(&q)函数将node节点从队列中移除。
    • 使用一个布尔变量met_null来跟踪是否遇到了空节点。如果在遍历过程中遇到了空节点,并且之后还遇到了非空节点,则树不是完全二叉树。
  5. 空节点与非空节点处理
    • 如果node为空,则将met_null设置为true,表示之后应该只遇到空节点。
    • 如果node不为空,则检查met_null是否为true。如果是,说明在之前已经遇到了空节点,但当前节点却是非空的,因此树不是完全二叉树。在这种情况下,函数会销毁队列并返回false
    • 如果node不为空且met_nullfalse,则将node的左右孩子(如果存在)加入队列。
  6. 队列销毁与结果返回
    • 如果遍历完成后没有违反完全二叉树的性质,则函数销毁队列q,并返回true,表示树是完全二叉树。

 代码实现:

#include <stdbool.h>// 假设已经定义了BTNode结构体、Queue结构体以及相关的队列操作函数bool BinaryTreeComplete(BTNode* root) {if (root == NULL) {// 空树被认为是完全二叉树(根据定义可有所不同,这里假设是)return true;}Queue q;QueueInit(&q);QueuePush(&q, root);bool met_null = false; // 标记是否遇到了空节点while (!QueueEmpty(&q)) {BTNode* node = QueueFront(&q);QueuePop(&q);// 如果已经遇到了空节点,但当前节点不是空,则不是完全二叉树if (met_null && node != NULL) {QueueDestroy(&q);return false;}// 如果当前节点是空,则设置标记为true,表示之后应该只遇到空节点if (node == NULL) {met_null = true;} else {// 否则,将当前节点的左右孩子加入队列QueuePush(&q, node->left);QueuePush(&q, node->right);}}QueueDestroy(&q);return true;
}

简化提炼思路如下:

思路

  1. 初始化
    • 使用一个队列来进行层序遍历。
    • 将根节点加入队列。
    • 初始化一个布尔变量 met_null 为 false,用于标记是否遇到了空节点。
  2. 层序遍历
    • 遍历队列,直到队列为空。
    • 在每次循环中,从队列头部取出一个节点。
    • 如果 met_null 为 true(意味着之前已经遇到了空节点),但当前节点不是空节点,则树不是完全二叉树,返回 false
    • 如果当前节点是空节点,则将 met_null 设置为 true
    • 如果当前节点不是空节点,则将其左右孩子(如果存在)加入队列。
  3. 结束条件
    • 如果遍历结束后没有违反完全二叉树的性质(即没有在遇到空节点后再遇到非空节点),则树是完全二叉树,返回 true

关键点

  • 一旦在层序遍历中遇到了空节点,之后的所有节点(如果存在)都应该是空节点,否则树就不是完全二叉树。
  • 使用队列来保持层序遍历的顺序。
  • 使用布尔变量 met_null 来跟踪是否遇到了空节点。

兄弟们共勉 !!! 

码字不易,求个三连

抱拳了兄弟们!

cad4eb7aa9fb4624aa20a08056a4030c.gif

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Rust语言在系统编程中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 Rust语言在系统编程中的应用 Rust语言在系统编程中的应用 Rust语言在系统编程中的应用 引言 Rust 概述 定义与原理 发展历程 Ru…

车-路-站-网”信息耦合的汽车有序充电

电动汽车作为一种环保、的交通工具&#xff0c;正逐渐成为未来交通的发展趋势。然而&#xff0c;大规模电动汽车的无序充电可能导致电网负荷波动、电压下降等问题&#xff0c;影响电网的安全稳定运行。为了解决这些问题&#xff0c;需要制定有效的电动汽车有序充电策略&#xf…

42个接受医工交叉领域投稿的二三四区期刊汇总|个人观点·24-11-14

小罗碎碎念 昨天晚上的推文总结了94个接受医工交叉研究投稿的一区期刊&#xff0c;今天这期推文把剩下的二三四区期刊补上。我会按照二三四区的顺序介绍&#xff0c;其中每个区会按照大类进行区分。 同样&#xff0c;由于公众号的排版限制&#xff0c;如下图所示的表格&#…

【更新中】《硬件架构的艺术》笔记(三):处理多个时钟

介绍 单时钟设计更易于实现&#xff0c;也更少出现亚稳态、建立和保持时间违例方面的问题。但在实践中&#xff0c;很少有设计只在一个时钟下运行。 多时钟域 多个始终可以有以下一种或多种时钟关系&#xff1a; 1、时钟频率不同。 2、时钟频率相同&#xff0c;但相位不同…

【vue2.0入门】vue基本语法

目录 引言一、页面动态插值1. 一般用法 二、计算属性computed三、动态class、style绑定四、条件渲染与列表渲染五、事件处理六、表单输入绑定七、总结 引言 本系列教程旨在帮助一些零基础的玩家快速上手前端开发。基于我自学的经验会删减部分使用频率不高的内容&#xff0c;并不…

vuepress+vdoing主题搭建自己的知识管理库

在github上建2个仓库 仓库1&#xff0c;文档源文件&#xff08;markdown&#xff09;&#xff0c;可以是私有或公有仓库。 仓库2&#xff0c;build生成的文件&#xff08;发布为github pages&#xff09;&#xff0c;公有仓库。 克隆模板 https://github.com/xugaoyi/vdoing…

开源项目推荐——OpenDroneMap无人机影像数据处理

实景三维作为GIS最火的课题&#xff0c;最近在想做一套自己的三维构建工具&#xff0c;考察了几个开源项目&#xff0c;把自己的搜索过程用csdn记录下来&#xff0c;希望也能帮助到各位同仁。 OpenDroneMap&#xff08;ODM&#xff09;是一个开源项目&#xff0c;旨在处理无人…

深入浅出《钉钉AI》产品体验报告

1. 引言 随着人工智能技术的迅猛发展&#xff0c;企业协同办公领域迎来了新的变革。钉钉作为阿里巴巴集团旗下的企业级通讯与协同办公平台&#xff0c;推出了钉钉AI助理&#xff0c;旨在提高工作效率&#xff0c;优化用户体验。本报告将对钉钉AI助理进行全面的产品体验分析&am…

夹耳开放式耳机好用吗?夹耳开放式耳机推荐

夹耳式耳机作为开放式耳机的一种&#xff0c;在最近几年里深受大家欢迎。它能够受到大家欢迎的原因主要是其不入耳的特性&#xff0c;既有助于保护听力健康&#xff0c;又能让人尽情享受极致的音乐体验。不过&#xff0c;很多小伙伴不知道夹耳式耳机到底好不好用&#xff1f;夹…

opencv(c++)----图像的读取以及显示

opencv(c)----图像的读取以及显示 imread: 作用&#xff1a;读取图像文件并将其加载到 Mat 对象中。参数&#xff1a; 第一个参数是文件路径&#xff0c;可以是相对路径或绝对路径。第二个参数是读取标志&#xff0c;比如 IMREAD_COLOR 表示以彩色模式读取图像。 返回值&#x…

SwiftUI-基础入门

开发OS X 图形应用界面时有三种实现方式&#xff1a;XIB、Storyboard、SwiftUI。Storyboard基于XIB做了优化&#xff0c;但XIB基本被放弃了&#xff0c;而SwiftUI是苹果公司后来开发的一套编程语言&#xff0c;用来平替Objective-C。虽然现在Swift 6 还是有些不完善的地方&…

string------1

文章目录 一. STL1.概念2.版本 二. string类2.1 为什么学习string类2. 标准库中的string类2.2.1 构造&#xff08;7个&#xff09;2.2.2 对string类对象进行“访问和修改”&#xff08;1&#xff09;operator[]&#xff08;2&#xff09;迭代器1.迭代器的使用2.迭代器的价值&am…

css-50 Projects in 50 Days(3)

html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>旋转页面</title><link rel"sty…

【线性代数\矩阵论】矩阵逆引理证明、应用

矩阵逆引理证明、应用 矩阵求逆引理要解决的问题是&#xff1a;减少矩阵求逆的计算量。已知一个矩阵 A A A及其逆矩阵 A − 1 A^{-1} A−1&#xff0c;当矩阵产生了变化时&#xff0c;例如增加一个扰动 P P P&#xff0c;能不能根据已知的逆矩阵 A − 1 A^{-1} A−1&#xff0c…

1111111111待修改--大流量分析(三)-BUUCTF

总结摘要 题目来来源URL https://buuoj.cn/challenges#%E5%A4%A7%E6%B5%81%E9%87%8F%E5%88%86%E6%9E%90%EF%BC%88%E4%B8%89%EF%BC%89 答题过程 这道题是看大佬写着说查找phpinfo&#xff0c;我现在也不知道为什么能够一下子就定位到这里了 这里先按照phpinfo进行&#xff…

OCRSpace申请free api流程

0.OCRSpace概述 OCR.Space是一款功能强大的在线光学字符识别&#xff08;OCR&#xff09;工具。 格式与语言支持广泛&#xff1a;支持多种图片格式&#xff0c;如 JPG、PNG、GIF、PDF 等作为输入。在语言方面&#xff0c;它支持英语、中文、法语、德语等20多种语言的文字识别…

UVC 输出视频格式修改和windows下数据分析

文章目录 前言一、UVC MJPEG视频帧描述符1.MJPG视频帧格式示例 二、UVC YUV2、NV12、M420、I420无压缩视频帧描述符GUID1.如YUV2数据参数初始为: 三、UVC Windows下UVC摄像头数据分析总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 项目需要&#…

C语言 | Leetcode C语言题解之第552题学生出勤记录II

题目&#xff1a; 题解&#xff1a; const int MOD 1000000007;struct Matrix {long mat[6][6];int row, col; };struct Matrix multiply(struct Matrix a, struct Matrix b) {int rows a.row, columns b.col, temp b.row;struct Matrix c;memset(c.mat, 0, sizeof(c.mat)…

【Linux系统编程】第四十六弹---线程同步与生产消费模型深度解析

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、Linux线程同步 1.1、同步概念与竞态条件 1.2、条件变量 1.2.1、认识条件变量接口 1.2.2、举例子认识条件变量 1.2.3、…

ML 系列:第 21 节 — 离散概率分布(二项分布)

一、说明 二项分布描述了在固定数量的独立伯努利试验中一定数量的成功的概率&#xff0c;其中每个试验只有两种可能的结果&#xff08;通常标记为成功和失败&#xff09;。 二、探讨伯努利模型 例如&#xff0c;假设您正在抛一枚公平的硬币 &#xff08;其中正面成功&#xff…