深入理解二叉树:遍历、存储与算法实现
在之前的博客系列中,我们系统地探讨了多种线性表数据结构,包括顺序表、栈和队列等经典结构,并通过代码实现了它们的核心功能。从今天开始,我们将开启一个全新的数据结构篇章——树结构。与之前讨论的线性结构不同,树形结构以其独特的层次性和分支特性,为我们打开了算法世界的新大门。
1.树
1.1树的概念与结构
树是⼀种非线性的数据结构,它是由 n(n>=0) 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树,也就是说它是根朝上,而叶朝下的。
• 有⼀个特殊的结点,称为根结点,根结点没有前驱结点。
• 除根结点外,其余结点被分成 M(M>0) 个互不相交的集合 T1、T2、……、Tm ,其中每⼀个集合Ti(1 <= i <= m) ⼜是⼀棵结构与树类似的⼦树。每棵⼦树的根结点有且只有⼀个前驱,可以有 0 个或多个后继。
因此,树是递归定义的。
示例:
树形结构中,子树之间不能有交集,否则就不是树形结构。
• 子树是不相交的(如果存在相交就是图了)
•除了根结点外,每个结点有且仅有⼀个父结点
• ⼀棵N个结点的树有N-1条边
我们来看一些示例,它们都是一些非树型结构:
1.2树相关术语
与线性结构的直观简洁不同,树形结构展现出了更为丰富的层次关系和分支特性。为了准确描述和分析这种非线性的数据结构,我们引入了一系列专业术语:根节点、父节点、子节点、叶子节点、度、深度等。这些术语不仅帮助我们精确描述树的组成,也为后续讨论树的遍历和操作奠定了重要基础。下面我们来一一介绍,看下图:
父结点/双亲结点
:若一个结点含有子结点,则这个结点称为其子结点的父结点; 如上图:A是B的父结点
子结点/孩子点
:⼀个结点含有的子树的根结点称为该结点的子结点; 如上图:B是A的孩子结点
结点的度
:⼀个结点有几个孩⼦,他的度就是多少;比如A的度为6,F的度为2,K的度为0
树的度
:⼀棵树中,最大的结点的度称为树的度; 如上图:树的度为 6
叶子结点/终端结点
:度为 0 的结点称为叶结点; 如上图: B、C、H、I… 等结点为叶结点
分支结点/非终端结点
:度不为 0 的结点; 如上图: D、E、F、G… 等结点为分支结点
兄弟结点
:具有相同父结点的结点互称为兄弟结点(亲兄弟); 如上图: B、C 是兄弟结点
结点的层次
:从根开始定义起,根为第 1 层,根的⼦结点为第 2 层,以此类推;
树的高度或深度
:树中结点的最大层次; 如上图:树的⾼度为 4
结点的祖先
:从根到该结点所经分支上的所有结点;如上图: A 是所有结点的祖先
路径
:⼀条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列;比如A到Q的路径为:A-E-J-Q;H到Q的路径H-D-A-E-J-Q
子孙
:以某结点为根的子树中任⼀结点都称为该结点的子孙。如上图:所有结点都是A的⼦孙
森林
:由 m(m>0) 棵互不相交的树的集合称为森林
理解这些树结构的基本概念不在于死记硬背,而在于培养实际应用能力。我们需要能够具备在树型结构中准确识别各个节点的父子关系,计算节点的度(子节点数量),区分内部节点和叶子节点等等能力,这些实操能力将为后续的代码实现打下坚实的基础。
1.3树的表示
掌握了树结构的基本概念和术语后,我们现在需要解决一个关键问题:如何在程序中有效地表示和存储树形结构?在计算机科学中,树的表示方法多种多样,每种方法都有其适用的场景和优缺点。常见的表示方式包括:双亲表示法(父指针表示法),孩子表示法,孩子兄弟表示法(二叉链表表示法)
我们这里简单介绍一下孩子兄弟表示法:
struct TreeNode
{
struct Node* child; // 左边开始的第⼀个孩⼦结点
struct Node* brother; // 指向其右边的下⼀个兄弟结点
int data; // 结点中的数据域
};
1.4树形结构实际运用场景
文件系统是计算机存储和管理文件的一种⽅式,它利用树形结构来组织和管理文件和文件夹。在文件系统中,树结构被广泛应用,它通过父结点和子结点之间的关系来表示不同层级的文件和文件夹之间的关联。
2.二叉树
2.1概念与结构
在树形结构中,我们最常用的就是⼆叉树,⼀棵二叉树是结点的⼀个有限集合,该集合由⼀个根结点加上两棵别称为左子树和右子树的⼆叉树组成或者为空。
从上图可以看出二叉树具备以下特点:
- 二叉树不存在度大于 2 的结点
- 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树
注意:对于任意的二叉树都是由以下几种情况复合而成的:
2.2特殊的二叉树
2.2.1满二叉树
⼀个二叉树,如果每⼀个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果⼀个二叉树的层数为 K ,且结点总数是 2k − 1 ,则它就是满二叉树。
2.2.2完全二叉树!
完全二叉树(Complete Binary Tree)是一种特殊的二叉树,它的定义是基于满二叉树的概念。在完全二叉树中,除了最后一层,其他各层的节点数都达到了最大个数,而最后一层的节点则都连续集中在最左边。这种结构的二叉树,其深度为h时,除了第h层外,其余各层(1~h-1)的节点数都是满的,第h层的节点从左到右连续排列,但不一定是满的。
⼆叉树性质
根据满⼆叉树的特点可知:
1)若规定根结点的层数为 1 ,则⼀棵非空⼆叉树的第i层上最多有 2i−1 个结点
2)若规定根结点的层数为 1 ,则深度为 h 的⼆叉树的最⼤结点数是 2h − 1
3)若规定根结点的层数为 1 ,具有 n 个结点的满⼆叉树的深度 h = log2 (n + 1)( log以2为底, n+1 为对数)
2.3二叉树存储结构
二叉树⼀般使用两种结构存储,⼀种顺序结构,⼀种链式结构。
2.3.1顺序存储
顺序结构存储就是使用数组来存储,⼀般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。
现实中我们通常把堆(⼀种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,⼀个是数据结构,⼀个是操作系统中管理内存的⼀块区域分段。
2.3.2链式存储
二叉树的链式存储结构通过链表形式动态表示结点间的逻辑关系,其核心设计思想如下:
-
结点结构设计
- 每个链式结点由三个域构成:
- 数据域:存储当前结点的元素值
- 左指针域:指向左子结点的内存地址
- 右指针域:指向右子结点的内存地址
- 通过指针的动态链接实现树形拓扑结构,形成"结点-子树"的递归关系
- 每个链式结点由三个域构成:
-
链式类型划分
- 二叉链:
- 仅含左右子结点指针
- 空间效率高,满足基础算法实现需求
- 三叉链:
- 增加父结点指针域
- 便于逆向溯源,应用于红黑树等复杂数据结构
- 二叉链:
3.实现顺序结构二叉树
3.1堆的概念与结构
堆通常采用顺序存储结构(如数组)实现,其在逻辑上表现为一棵完全二叉树,同时满足关键有序性:每个结点的值总保持大于等于(大顶堆)或小于等于(小顶堆)其子节点的值。这种双重要求既继承了完全二叉树的层级结构特性,又通过父子结点间的有序性约束,赋予堆快速获取极值、动态调整高效的核心优势,成为优先队列等场景的理想数据结构载体。
对于具有 n 个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有结点从0 开始编号,则对于序号为 i 的结点有:
- 若 i>0 , i 位置结点的双亲序号: (i-1)/2 ; i=0 , i 为根结点编号,无双亲结点
- 若 2i+1<n ,左孩⼦序号: 2i+1 , 2i+1>=n 否则无左孩子
- 若 2i+2<n ,右孩⼦序号: 2i+2 , 2i+2>=n 否则无右孩子
3.2堆的实现及堆排序算法
该代码实现了小根堆
(Min-Heap)的所有基本操作,具备插入、删除、调整等核心功能。
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int HPDataType;// 堆结构定义(数组存储)
typedef struct Heap
{HPDataType* arr; // 动态数组指针int size; // 当前元素个数int capacity; // 数组容量
}HP;/* 堆基础操作 */
void HPInit(HP* php) // 初始化空堆
{assert(php);php->arr = NULL;php->capacity = php->size = 0;
}// 元素交换函数
void Swap(int* a, int* b)
{int temp = *a;*a = *b;*b = temp;
}/* 核心调整算法 */
// 向上调整(插入时使用)时间复杂度O(logN)
void AdjustUp(HPDataType* a, int child)
{assert(a);int parent = (child - 1) / 2; // 计算父节点索引// 小堆:子节点 < 父节点时交换(大堆改为>)while (child > 0) // 循环直到到达根节点{if (a[child] < a[parent]) // 小堆条件判断{Swap(&a[child], &a[parent]);child = parent; // 向上移动指针parent = (child - 1) / 2; // 更新父节点}elsebreak; // 满足堆条件时退出}
}// 插入元素(自动扩容)
void HPPush(HP* php, HPDataType x)
{assert(php);// 容量检查(2倍扩容策略)if (php->capacity == php->size){int newcapacity = php->capacity == 0 ? 4 : 2 * php->capacity;HPDataType* temp = (HPDataType*)realloc(php->arr, newcapacity * sizeof(HPDataType));if (temp == NULL){perror("realloc fail");exit(1);}php->arr = temp;php->capacity = newcapacity;}// 插入尾部并调整php->arr[php->size++] = x;AdjustUp(php->arr, php->size-1); // 从新插入位置开始调整
}// 判断堆是否为空
bool HPEmpty(HP* php)
{assert(php);return php->size == 0;
}// 向下调整(删除堆顶时使用)时间复杂度O(logN)
void AdjustDown(HPDataType* a, int n, int parent)
{assert(a);int child = parent * 2 + 1; // 左孩子索引while (child < n) // 在有效范围内调整{// 选择较小的子节点(小堆特性,大堆则选较大值)if (child + 1 < n && a[child] > a[child + 1])child++;// 小堆条件判断(父节点 > 子节点时交换)if (a[child] < a[parent]) {Swap(&a[child], &a[parent]);parent = child; // 向下移动指针child = parent * 2 + 1; // 更新左孩子索引}elsebreak; // 满足堆条件时退出}
}// 删除堆顶元素
void HPPop(HP* php)
{assert(php);assert(!HPEmpty(php));// 交换首尾元素并删除尾部Swap(&php->arr[0], &php->arr[php->size - 1]);php->size--;// 从根节点开始向下调整AdjustDown(php->arr, php->size, 0);
}/* 辅助功能 */
int HPSize(HP* php) // 获取当前元素数量
{assert(php);return php->size;
}void HPDestroy(HP* php) // 销毁堆释放内存
{assert(php);if (php->arr)free(php->arr);php->arr = NULL;php->capacity = php->size = 0;
}HPDataType HPTop(HP* php) // 获取堆顶元素
{assert(php && php->size > 0);return php->arr[0];
}/* 建堆方法*/
void HPInitArray(HP* php, HPDataType* a, int n)
{for (int i = 0; i < n; i++){HPPush(php, a[i]); // 逐个插入方式建堆}
}
在这里我们着重介绍一下向上调整算法(AdjustUp)
和向下调整算法(AdjustDown)
这两个核心函数。
3.2.1向上调整算法
向上调整算法用于在插入新元素后,维护堆的有序性。
当向堆的尾部插入一个新元素时,可能破坏堆的父子节点大小关系(如小根堆中父节点应 ≤ 子节点),该算法通过逐层向上比较与交换,确保堆结构恢复有效状态。
示例:
如果我们要用向上调整算法来建立一个堆并将它排序,代码非常简单,如下:
//向上调整算法建堆时间复杂度为: O(n ∗ log2 n)
void HeapSort(int* a, int n)
{// a数组直接建堆 O(N)for (int i = 0; i < n; i++){AdjustUp(a, n, i);}int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}
注意:数据降序需用小堆,升序则用大堆
究其原因是小堆每次调整完将最小值放在数组最后,大堆调整完将最大值放在最后。
3.2.2向下调整算法
删除堆是删除堆顶的数据,将堆顶的数据根最后⼀个数据⼀换,然后删除数组最后⼀个数据,再进行向下调整算法。
向下调整算法有⼀个前提:左右子树必须是⼀个堆,才能调整。
• 将堆顶元素与堆中最后⼀个元素进行交换
• 删除堆中最后⼀个元素
• 将堆顶元素向下调整到满足堆特性为止
同样的,用向下调整算法来建立一个堆并将它排序,代码也非常简单,如下:
//堆排序整体算法复杂度为O(N*logN)
//向下调整算法建堆时间复杂度为: O(n)
void HeapSort(int* a, int n)
{// a数组直接建堆 O(N)for (int i = (n - 1 - 1) / 2; i >= 0; --i){AdjustDown(a, n, i);}int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown(a, end, 0);--end;}
}
因为向下调整算法建堆的时间复杂度要小于向上调整算法,所以在堆排序实际应用中我们大多采用向下调整算法建堆,并且向上调整算法并不适合后续的排序算法,所以整体来说向下调整算法更加优秀。
3.3.TOK-K问题
TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,⼀般情况下数据量都比较大。
比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。
对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了。(数据不能⼀下子全部加载到内存中)。
最佳的方式就是用堆来解决,基本思路如下:
1)用数据集合中前K个元素来建堆,求前k个最大的元素,则建小堆,求前k个最小的元素,则建大堆
2)用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素
将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素
代码实现:
void CreateNDate()
{// 造数据int n = 100000;srand(time(0));const char* file = "data.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("fopen error");return;}for (int i = 0; i < n; ++i){int x = (rand() + i) % 1000000;fprintf(fin, "%d\n", x);}fclose(fin);
}void TOPk()
{int k = 0;printf("请输入k:");scanf("%d", &k);const char* file = "data.txt";FILE* fout = fopen(file, "r");if (fout == NULL){perror("fopen fail!");exit(1);}int* minHeap = (int*)malloc(k * sizeof(int));if (minHeap == NULL){perror("malloc fail!");exit(2);}//从文件中读取前K个数据for (int i = 0; i < k; i++){fscanf(fout, "%d", &minHeap[i]);}//建堆---小堆for (int i = (k - 1 - 1) / 2; i >= 0; i--){AdjustDown(minHeap, k, i);}int x = 0;while (fscanf(fout, "%d", &x) == 1){//读取到的数据跟堆顶的数据进行比较//比堆顶值大,交换入堆if (x > minHeap[0]){minHeap[0] = x;AdjustDown(minHeap, k, 0);}}for (int i = 0; i < k; i++){printf("%d ", minHeap[i]);}fclose(fout);
}
执行过程中
CreateData
函数创建一份写有十万个数据的文件,用到了创建随机数的三个函数rand
,srand
,time
,这些知识都在前面的博客内容中有所提到,不了解的读者可以去看看这两篇博客。
猜数字小游戏
文件操作手册
然后我们动态创建一个数组,将文件中前k个数据存入数组中,并建立小堆,然后将文件中其他数据与小堆堆顶进行比较,如果大于堆顶数据,则替换,并重新向下调整,保证堆顶是堆内最小数据,直至遍历完所有数据,最后打印堆内数据为最后结果。
4.实现链式结构二叉树
4.1链式二叉树的基本结构
用链表来表示⼀棵二叉树,即用链式结构表示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 ,其结构如下:
typedef int BTDataType;
// ⼆叉链
typedef struct BinaryTreeNode
{struct BinTreeNode* left; // 指向当前结点左孩⼦struct BinTreeNode* right; // 指向当前结点右孩⼦BTDataType val; // 当前结点值域
}BTNode;
⼆叉树的创建方式比较复杂,为了更好的步入到⼆叉树内容中,我们手动先创建⼀棵链式⼆叉树:
BTNode* CreateNode(BTDataType x)
{BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));if (newnode == NULL){perror("malloc fail!");exit(1);}newnode->data = x;newnode->left = newnode->right = NULL;return newnode;
}
int main()
{BTNode* node1 = CreateNode(1);BTNode* node2 = CreateNode(2);BTNode* node3 = CreateNode(3);BTNode* node4 = CreateNode(4);BTNode* node5 = CreateNode(5);BTNode* node6 = CreateNode(6);node1->left = node2;node1->right = node3;node2->left = node4;node2->right = node5;node3->left = node6;return 0;
}
这是上面代码创建的二叉树示意图。
回顾二叉树的概念,二叉树分为空树和非空二叉树,非空二叉树由根结点、根结点的左子树、根结点的右子树组成的。
根结点的左子树和右子树分别又是由子树结点、子树结点的左子树、子树结点的右子树组成的,因此二叉树定义是递归式的,后面链式二叉树的许多操作中基本都是借助该概念帮助实现的
4.2二叉树的四种遍历方式
还是上面我们自己创建的二叉树,我们分别用不同的遍历方式来遍历,看看都是怎么个顺序。
首先我们来介绍三种遍历方式,它们是一体的,分别是前序遍历,中序遍历,后序遍历。遍历规则为:
- 前序遍历(亦称先序遍历):访问根结点的操作发生在遍历其左右⼦树之前,访问顺序为:根结点、左子树、右子树
- 中序遍历:访问根结点的操作发生在遍历其左右子树中间,访问顺序为:左子树、根结点、右子树
- 后序遍历:访问根结点的操作发生在遍历其左右子树之后,访问顺序为:左子树、右子树、根结点
//前序遍历
void PreOrder(BTNode* root)
{if (root == NULL){return;}printf("%d ", root->data);PreOrder(root->left);PreOrder(root->right);
}//中序遍历
void InOrder(BTNode* root)
{if (root == NULL){return;}InOrder(root->left);printf("%d ", root->data);InOrder(root->right);
}//后序遍历
void PostOrder(BTNode* root)
{if (root == NULL){return;}PostOrder(root->left);PostOrder(root->right);printf("%d ", root->data);
}
前序遍历遵循"根节点-左子树-右子树"的顺序访问二叉树。以上文创建二叉树为例,遍历从根节点1开始:首先访问并打印根节点1,接着遍历左子树,访问节点2并打印,继续深入节点2的左子树,访问节点4并打印(节点4没有子节点,该分支遍历结束),返回处理节点2的右子树,访问节点5并打印(节点5没有子节点),最后处理根节点的右子树,访问节点3并打印,继续访问节点3的左子树节点6并打印(节点6没有子节点,且节点3没有右子树),程序结束。
最终前序遍历结果为:1 2 4 5 3 6
中序遍历与后续遍历一样按照自身规则一步一步递归实现,这里不再重复赘述,最后中序遍历结果为:4 2 5 1 6 3,后序遍历结果为:4 5 2 6 3 1,大家可以自己试着遍历。
还有一种遍历方式是层序遍历,我们设二叉树的根结点所在层数为1,从所在二叉树的根结点出发,首先访问第一层的树根结点,然后从左到右访问第二层上的结点,接着是第三层的结点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。
实现层序遍历我们需要用到之前学过的一种数据结构:队列。在实现层序遍历代码中用到了之前队列博客里的封装函数,大家可以去这篇博客里看一下:
栈与队列,要注意:typedef struct BinaryTreeNode* QDataType;
void LevelOrder(BTNode* root)
{Queue pq;QueueInit(&pq);if(root)QueuePush(&pq, root);while (!QueueEmpty(&pq)){BTNode* front = QueueFront(&pq);printf("%d ", front->data);QueuePop(&pq);if(front->left)QueuePush(&pq, front->left);if(front->right)QueuePush(&pq, front->right);}QueueDestroy(&pq);
}
实现过程:
初始化队列:首先创建一个先进先出(FIFO)的队列结构,用于临时存储待访问的节点指针。根节点入队:若二叉树非空,将根节点指针存入队列,作为遍历的起始点。
循环处理队列:
取出队首节点:通过队列接口获取当前队首元素(即最早入队的节点)
访问节点数据:输出该节点的数值信息(或其他处理操作)
节点出队:将该节点从队列中移除
子节点入队:若当前节点存在左孩子,将其左孩子指针入队;同理处理右孩子
终止条件:当队列为空时,表明所有节点均已访问完毕,遍历结束。
资源释放:最终销毁队列结构,释放系统资源。
该算法通过队列的先进先出特性,确保节点总是按照层级顺序被处理:首先访问根节点,接着是第二层的左右子节点,然后是第三层的节点,以此类推。
4.3链式二叉树其他函数封装
/* 计算二叉树节点总数 */
int BinaryTreeSize(BTNode* root)
{// 递归终止条件:空树节点数为0if (root == NULL) return 0;// 当前节点(1) + 左子树节点数 + 右子树节点数return 1 + BinaryTreeSize(root->left) + BinaryTreeSize(root->right);
}/* 计算二叉树叶子节点数 */
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层节点数 */
int BinaryTreeLevelKSize(BTNode* root, int k)
{// 递归终止条件1:空树if (root == NULL) return 0;// 递归终止条件2:到达目标层级if (k == 1) return 1;// 左子树第k-1层节点数 + 右子树第k-1层节点数return BinaryTreeLevelKSize(root->left, k-1) + BinaryTreeLevelKSize(root->right, k-1);
}/* 计算二叉树深度/高度 */
int BinaryTreeDepth(BTNode* root)
{// 递归终止条件:空树深度为0if (root == NULL) return 0;// 分别计算左右子树深度int leftDepth = BinaryTreeDepth(root->left);int rightDepth = BinaryTreeDepth(root->right);// 取较大值 + 当前层(1)return (leftDepth > rightDepth ? leftDepth : rightDepth) + 1;
}/* 查找值为x的节点 */
BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{// 递归终止条件1:空树或找到目标节点if (root == NULL) return NULL;if (root->data == x) return root;// 先在左子树查找BTNode* leftResult = BinaryTreeFind(root->left, x);if (leftResult) return leftResult;// 左子树未找到则在右子树查找BTNode* rightResult = BinaryTreeFind(root->right, x);if (rightResult) return rightResult;// 整棵树未找到返回NULLreturn NULL;
}/* 销毁二叉树(二级指针版) */
void BinaryTreeDestory(BTNode** root)
{// 递归终止条件:空树if (*root == NULL) return;// 后序方式递归释放BinaryTreeDestory(&(*root)->left);BinaryTreeDestory(&(*root)->right);// 释放当前节点并将指针置NULLfree(*root);*root = NULL;
}/* 判断是否完全二叉树 */
bool BinaryTreeComplete(BTNode* root)
{Queue pq;QueueInit(&pq);// 根节点入队开始层序遍历if (root) QueuePush(&pq, root);// 第一阶段:遇到第一个NULL节点时停止while (!QueueEmpty(&pq)){BTNode* front = QueueFront(&pq);QueuePop(&pq);// 遇到空节点则跳出循环if (front == NULL) break;// 无论子节点是否NULL都入队QueuePush(&pq, front->left);QueuePush(&pq, front->right);}// 第二阶段:检查队列剩余节点while (!QueueEmpty(&pq)){BTNode* front = QueueFront(&pq);QueuePop(&pq);// 如果还有非NULL节点,则不是完全二叉树if (front != NULL) {QueueDestroy(&pq);return false;}}QueueDestroy(&pq);return true;
}
关键点与注意点:
-
递归实现:
多数函数(节点计数、深度计算、查找等)采用递归实现,需确保:
-
明确的递归终止条件(如root == NULL)
-
正确的递归分解(如左子树+右子树+当前节点)
注意:递归深度可能导致栈溢出(极端深度的树)
-
-
遍历方式选择:
-
销毁二叉树必须使用后序遍历(先释放子树再释放根)
-
层级相关操作(如K层节点数)适合前序/深度优先
-
完全二叉树判断必须用层序遍历
-
-
完全二叉树检测:
核心逻辑:层序遍历中遇到NULL后,后续不得出现非NULL节点
需注意:
-
所有子节点(包括NULL)都要入队
-
队列中存储的是节点指针,需处理NULL值
-
-
内存管理:
-
销毁函数使用二级指针(BTNode**),确保外部指针置NULL
-
队列操作后必须调用Destroy,避免内存泄漏
-
查找函数返回节点指针,注意不要误修改原树结构
-
这一系列函数完整实现了二叉树的核心操作,涵盖了节点统计、结构分析和内存管理。递归思想贯穿多数实现,需特别注意终止条件和资源释放。
相关文章:

深入理解二叉树:遍历、存储与算法实现
在之前的博客系列中,我们系统地探讨了多种线性表数据结构,包括顺序表、栈和队列等经典结构,并通过代码实现了它们的核心功能。从今天开始,我们将开启一个全新的数据结构篇章——树结构。与之前讨论的线性结构不同,树形…...
Python3 简易DNS服务器实现
使用Python3开发一个简单的DNS服务器,支持配置资源记录(RR),并能通过dig命令进行查询。 让自己理解DNS原理 实现方案 我们将使用socketserver和dnslib库来构建这个DNS服务器。dnslib库能帮助我们处理DNS协议的复杂细节。 1. 安装依赖 首先确保安装了d…...

【Win32 API】 lstrcmpA()
作用 比较两个字符字符串(比较区分大小写)。 lstrcmp 函数通过从第一个字符开始检查,若相等,则检查下一个,直到找到不相等或到达字符串的末尾。 函数 int lstrcmpA(LPCSTR lpString1, LPCSTR lpString2); 参数 lpStr…...

(C语言)超市管理系统 (正式版)(指针)(数据结构)(清屏操作)(文件读写)
目录 前言: 源代码: product.h product.c fileio.h fileio.c main.c 代码解析: 一、程序结构概述 二、product.c 函数详解 1. 初始化商品列表 Init_products 2. 添加商品 add_product 3. 显示商品 display_products 4. 修改商品 mo…...

NAT转换和ICMP
NAT nat原理示意 nat实现 ICMP ICMP支持主机或路由器: 差错或异常报告网络探寻 2类icmp报文: 差错报告报文(5种) 目的不可达源抑制--拥塞控制超时&超期--TTL超时参数问题--问题报文丢弃重定向--不应该由这个路由器转发&a…...
Executors类详解
Executors类详解 Executors 是Java中用于快速创建线程池的工具类,提供了一系列工厂方法,简化了 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 的配置。以下是其核心方法、实现原理及使用注意事项: 1. 常用线程池工厂方法 (1) newFixedThreadPool 作用:创建固定大小…...

【专利信息服务平台-注册/登录安全分析报告】
前言 由于网站注册入口容易被黑客攻击,存在如下安全问题: 暴力破解密码,造成用户信息泄露短信盗刷的安全问题,影响业务及导致用户投诉带来经济损失,尤其是后付费客户,风险巨大,造成亏损无底洞…...

BUUCTF——web刷题第一页题解
共31题,admin那题没有,因为环境问题,我做的非常卡 目录 极客大挑战 2019]Havefun [HCTF 2018]WarmU [ACTF2020 新生赛]Include [ACTF2020 新生赛]Exec [GXYCTF2019]Ping Ping Ping [SUCTF 2019]EasySQL [极客大挑战 2019]LoveSQL [极…...

哪个品牌的智能对讲机好用?推荐1款,能扛事更智能
在专业通信领域,智能对讲机早已突破传统设备的局限,成为集通信、调度、数据传输于一体的智能化终端。面对复杂多变的作业环境,用户对设备的稳定性、通信效率和智能化水平提出了更高要求。但是,市面上产品同质化严重,部…...

【Win32 API】 lstrcpyA()
作用 将字符串复制到指定的字符串缓冲区。 函数 LPSTR lstrcpyA(LPSTR lpString1, LPCSTR lpString2); 参数 lpString1 类型:LPTSTR 一个缓冲区,用于接收由 lpString2 参数指向的字符串的内容。 缓冲区必须足够大才能包含字符串,包括终止…...

Vue3——Watch侦听器
目录 手动指定监听对象 侦听ref对象 侦听ref对象中的某个属性 reactive写法 watchEffect 自动侦听 多源侦听 一次性侦听器 watch 是⼀个⽤于观察和响应Vue响应式系统中数据变化的⽅法。它允许你指定⼀个数据源(可以是 响应式引⽤、计算属性、组件的属性等…...

Go的单测gomock及覆盖率命令
安装gomock: go get github.com/golang/mock/gomockgo get github.com/golang/mock/mockgen 使用 mockgen 生成 mock 代码: 参考 mockgen -sourceservice/user.go -destinationservice/mocks/mock_user_service.go -packagemocks go test -coverprofilecoverage.out…...

Leetcode209做题笔记
力扣209 题目分析:想象一个窗口遍历着这个数组,不断扩大右边界,让r。往窗口中添加数字: 此时我们找到了这个窗口,它的和满足了大于等于target的条件,题目让我求最短的,那么我们就尝试来缩短它&…...

Suna: 开源多面手 AI 代理
GitHub:GitHub - kortix-ai/suna: Suna - Open Source Generalist AI Agent 更多AI开源软件:发现分享好用的AI工具、AI开源软件、AI模型、AI变现 - 小众AI Suna 是一个完全开源的 AI 助手,可帮助您轻松完成实际任务。通过自然对话,…...

25-05-16计算机网络学习笔记Day1
深入剖析计算机网络:今日学习笔记总结 本系列博客源自作者在大二期末复习计算机网络时所记录笔记,看的视频资料是B站湖科大教书匠的计算机网络微课堂,每篇博客结尾附书写笔记(字丑见谅哈哈) 视频链接地址 一、计算机网络基础概念 …...

12 web 自动化之基于关键字+数据驱动-反射自动化框架搭建
文章目录 一、如何实现一条用例,实现覆盖所有用例的测试1、结合数据驱动:编辑一条用例,外部导入数据实现循环测试2、用例体:实现不同用例的操作步骤对应的断言 二、实战1、项目路径总览2、common 文件夹下的代码文件3、keywords 文…...

动态IP赋能业务增效:技术解构与实战应用指南
在数字化转型加速的今天,IP地址作为网络通信的基础设施,其技术特性正深刻影响着企业业务架构的效率与安全性。动态IP(Dynamic IP)作为互联网资源分配的核心机制,早已突破传统认知中的"临时地址"定位…...

【Java ee初阶】http(1)
HTTP 全称为“超文本传输协议”,由名字可知,这是一个基于文本格式的协议,而TCP,UDP,以太网,IP...都是基于二进制格式的协议。 如何区别该协议是基于哪种格式的协议? 形如这种协议格式…...
OkHttp用法-Java调用http服务
特点:高性能,支持异步请求,连接池优化 官方文档:提供快速入门指南和高级功能(如拦截器、连接池)的详细说明,GitHub仓库包含丰富示例。 社区资源:中文教程丰富,GitHub高…...

day18-数据结构引言
一、 概述 数据结构:相互之间存在一种或多种特定关系的数据元素的集合。 1.1 特定关系: 1. 逻辑结构 2.物理结构(在内存当中的存储关系) 逻辑结构物理结构集合,所有数据在同一个集合中,关系平等顺…...

我开源了一个免费在线工具!UIED Tools
UIED Tools - 免费在线工具集合 最近更新:修改了文档说明,优化了项目结构介绍 这是设计师转开发的第一个开源项目,bug和代码规范可能有些欠缺。 这是一个功能丰富的免费在线工具集合网站,集成了多种实用工具,包括 AI …...

什么时候可以开始学习深度学习?
咱们先来聊聊机器学习和深度学习的关系~ 这个问题其实挺常见的,之前我也跟不少同事、同学聊过。最近有好几个同学也聊过。 简单说,深度学习是机器学习的一个子集,两者不是并列关系,而是“包含”关系。 你可以这么理解ÿ…...

初学python的我开始Leetcode题8-5
提示:100道LeetCode热题-8-5主要是二叉树相关,包括三题:路径总和 III、二叉树的最近公共祖先、二叉树中的最大路径和。由于初学,所以我的代码部分仅供参考。 前言 二叉树完结撒花~ 下一次的图论会是一些有趣的应用案例~ 提示&am…...

构建RAG混合开发---PythonAI+JavaEE+Vue.js前端的实践
写在前文:之所以设计这一套流程,是因为 Python在前沿的科技前沿的生态要比Java好,而Java在企业级应用层开发比较活跃; 毕竟许多企业的后端服务、应用程序均采用Java开发,涵盖权限管理、后台应用、缓存机制、中间件集成…...

08.webgl_buffergeometry_attributes_none ,three官方示例+编辑器+AI快速学习
本实例主要讲解内容 这个Three.js示例展示了无属性几何体渲染技术,通过WebGL 2的gl_VertexID特性和伪随机数生成算法,在着色器中动态计算顶点位置和颜色,而不需要在CPU端预先定义几何体数据。 核心技术包括: WebGL 2的顶点ID特…...

26考研 | 王道 | 计算机组成原理 | 一、计算机系统概述
26考研 | 王道 | 计算机组成原理 | 一、计算机系统概述 文章目录 26考研 | 王道 | 计算机组成原理 | 一、计算机系统概述1.1 计算机的发展1.2 计算机硬件和软件1.2.1 计算机硬件的基本组成1.2.2 各个硬件的工作原理1.2.3 计算机软件1.2.4 计算机系统的层次结构1.2.5 计算机系统…...
转换算子介绍
### 转换算子的定义与用法 #### 定义 转换算子(Transformation Operators)是指用于处理分布式数据集的操作符,在大数据框架中广泛使用,例如Apache Flink和Apache Spark。这些操作符允许开发者对数据集执行各种变换操作࿰…...
Android学习总结之Glide自定义三级缓存(实战篇)
一、为什么需要三级缓存 内存缓存(Memory Cache) 内存缓存旨在快速显示刚浏览过的图片,例如在滑动列表时来回切换的图片。在 Glide 中,内存缓存使用 LruCache 算法(最近最少使用),能自动清理长…...
单片机开发软件
目录 纯编码 vscode Ardunio Keil 1. 集成化开发环境(IDE) 2. 多架构芯片支持 3. 高效的代码生成与优化 4. 强大的调试与仿真功能 5. 丰富的库函数与生态系统 6. 教育与企业级适用性 典型应用场景 半编码半图形化 STM32CUBEIED 1. 图形化配置…...

LeetCode100.2 字母异位词分组
观察题目,需要把strs中的元素按照字母进行归类,一个朴素的思路是:遍历strs,对每个元素排序后插入哈希表中,随后再遍历一遍表将其转化为vector<vector<string>>。 class Solution { public:vector<vect…...