自学内容网 自学内容网

链式二叉树的实现

目录

链式二叉树的结点设置

创建二叉树结点函数BuyBTNode

链式二叉树各个功能函数实现的核心思路总结

链式二叉树的功能函数实现主要依赖于递归方法和分治思想:

链式二叉树的4种遍历方式

前序遍历函数PrevOrder

1.前序遍历(也叫先根遍历)思路:

2.前序遍历代码:根->左子树->右子树 

3.代码解析

前序遍历步骤

4.递归展开图

中序遍历函数InOrder

1.中序遍历思路:

2.中序遍历代码:左子树->根结点->右子树

3.代码解析

后序遍历函数PostOrder

1.后序遍历的思路:

2.后序遍历代码:左子树->根结点->右子树 

3.代码解析

层序遍历函数LevelOrder

1.用队列实现层序遍历思路

2.层序遍历代码

2.1.用队列q实现层序遍历时队列存储的数据类型一定是链式二叉树结点地址的原因

2.2.写法1:一行打印完二叉树存放的所有数据

2.2.1.代码解析 

2.3.写法2: 一行一行打印二叉树的每一层数据(即严格控制二叉树每一层数据的输出)

2.3.1.代码解析 

2.3.2.测试代码

4种遍历方式的时间复杂度和空间复杂度的计算过程

1.前序、中序、后序遍历的时间复杂度和空间复杂度

1.1.时间复杂度:O(n)

1.2.空间复杂度 :O(n)

1.3.总结

2.层序遍历的时间复杂度和空间复杂度

2.1.时间复杂度O(n)

2.2.空间复杂度O(n)

2.3.总结

求二叉树的结点总数函数TreeSize

1.在用递归求二叉树结点总数时需要注意以下几点:

1.1.不能用局部变量的返回值来求二叉树结点总数的原因分析

(1)错误代码

(2)原因

1.2.不能用静态变量size的返回值求二叉树结点总数的原因分析

(1)错误代码

(2)原因

1.3.不能用全局变量size求二叉树结点总数的原因分析

2.递归求二叉树结点总数的思路及代码

2.1.思路

2.2.代码

(1)代码思路步骤

(2)代码

2.3.递归展开图

2.4.TreeSize函数的时间复杂度和空间复杂度 

2.4.1.时间复杂度O(n)

2.4.2.空间复杂度O(n)

求二叉树所有叶子结点总数函数TreeLeafSize

1.思路

2.代码

(1)思路步骤

(2)代码

3.递归展开图

4.测试代码

求二叉树高度函数TreeHeight

1.思路

1.1.求二叉树高度思路:

1.2.递归求二叉树高度的具体过程:

2.代码

3.递归展开图

4. 注意事项

求二叉树第k层结点个数(k > 1)函数TreeKLevelSize

1.思路

1.1.二叉树第k层结点个数思路:

1.2.递归求二叉树第k层结点个数的具体过程:

2.代码

3.递归展开图

4.测试代码

查找函数TreeFind

1.思路

2.代码

3.递归展开图

4.查找函数TreeFind错误写法(注意事项)

4.1.错误写法1

错误原因分析过程:

4.2.错误写法2 

错误原因分析过程:

原因1:递归调用未处理返回值

原因2:递归调用的结果未被正确利用(这是真正的问题所在)

小结

4.3.错误写法3

错误原因分析过程:

错误原因1:多次递归调用

错误原因2:不必要的重复计算

二叉树销毁函数TreeDestroy

1.思路

递归销毁二叉树的思路步骤总结如下:

2.代码

 判断二叉树是否是完全二叉树函数TreeComplete

1.利用层序遍历判断二叉树是否是完全二叉树的分析过程

1.1.利用上面层序遍历二叉树有如下特性:

1.2.利用层序遍历判断二叉树是完全二叉树的思路

2.代码

2.1.代码思路步骤

2.2.代码

3.注意事项

链式二叉树整个工程代码

1.BinaryTree.h

2.BinaryTree.c

3.test.c测试代码 

4.Queue.h

5.Queue.c


链式二叉树的结点设置

既然是链式二叉树,那必须得有自己的结点类型,以下是链式二叉树结点类型的定义,为了避免过多重复的代码,下面的问题都统一使用该结点类型。

//二叉树存储的数据类型
typedef int BTDataType;

//二叉树结点的结构体类型
typedef struct BinaryTreeNode
{
BTDataType data;//data存储数据
struct BinaryTreeNode* left;//指针left指向左孩子结点
struct BinaryTreeNode* right;//指针right指向右孩子结点
}BTNode;

创建二叉树结点函数BuyBTNode

//创建二叉树的一个结点
BTNode* BuyBTNode(BTDataType x)
{
//创建1个二叉树的结点
BTNode* TreeNode = (BTNode*)malloc(sizeof(BTNode));
if (TreeNode == NULL)
{
perror("malloc fail");
exit(-1);
}

//对结点的结构体成员进行初始化
TreeNode->data = x;
TreeNode->left = TreeNode->right = NULL;
return TreeNode;
}

链式二叉树各个功能函数实现的核心思路总结

链式二叉树的功能函数实现主要依赖于递归方法和分治思想:

1.递归分解原则:

  • 遍历到的每个结点被视为子树的根结点。
  • 每个结点都有两棵子树(左子树和右子树),即使它们可能是空树。
  • 空树是递归的基本情况,表示递归的终止。

2.递归分解过程:

  • 从根结点开始,将二叉树分割成根结点和左右子树。
  • 对每个子树重复分割过程,直到遇到空树。

3.分治思想在链式二叉树中的应用:

  • :将复杂问题分解成更小的子问题。
  • :递归地解决这些子问题。
  • :在链式二叉树的操作中,递归调用本身就是一种隐式的合并过程。

4.递归实现链式二叉树的思路:

  • 基于分治策略,将大问题分解为更小的子问题,直到遇到空树。
  • 每个结点负责管理其左右子树,这种管理是递归的。

5.递归深度与终止:

  • 链式二叉树的规模越大,递归分解的层次越多。
  • 递归在遇到空树时终止,递归开始逐层返回。

6.链式二叉树功能函数的递归思路:

  • 分解:通过递归调用,将当前结点的问题分解为其左右子树的问题。
  • 解决:递归地处理每个子树的问题。
  • 合并:在链式二叉树的操作中,不需要显式的合并步骤,递归调用的结果本身就是最终解的一部分。

总体来说,链式二叉树的递归实现利用分治思想,将问题分解为更小的子问题,并通过递归调用解决这些子问题,从而实现对整个树的操作。这种递归分解和解决的方式是链式二叉树功能函数实现的核心。值得注意的是,层序遍历是链式二叉树中唯一一个通常采用非递归实现的功能函数,它通常使用队列来实现。

链式二叉树的4种遍历方式

前序遍历函数PrevOrder

1.前序遍历(也叫先根遍历)思路:

前序遍历的思路是按照 “根结点->左子树->右子树” 的顺序遍历二叉树。对于任何给定的二叉树,我们都首先访问根结点,然后递归地遍历左子树,最后递归地遍历右子树。这个过程会在每个子树上都重复进行,直到遍历完整棵树。

图形1:

图形2:

图形解析:>利用后序遍历整个链式二叉树的遍历顺序:

访问并打印根1的值->访问根1左子树->访问并打印根2的值->访问根2左子树->访问并打印根3的值

->访问根3左子树(空树NULL)->访问根3右子树(空树NULL)->访问根2右子树(空树NULL)->访问根1右子树->访问并打印根4的值->访问根4左子树->访问并打印根5的值->访问根5左子树(空树NULL)->访问根5右子树(空树NULL)->访问根4右子树->访问并打印根6的值->访问根6左子树(空树NULL)->访问根6右子树(空树NULL)。

2.前序遍历代码:根->左子树->右子树 

//前序遍历
void PrevOrder(BTNode* root)
{
//若指针root指向的当前树(子树)是空树(NULL),则当前树的根结点是个空结点(NULL),则此时打印 
    NULL表示当前树的前序遍历结束了。
if (root == NULL)
{
printf("NULL ");
return;
}

//前序遍历遍历顺序:根结点->左子树-》右子树
printf("%d ", root->data);//先访问当前树根结点的值并打印出来
PrevOrder(root->left);//再访问当前树的左子树
PrevOrder(root->right);//后访问当前树的右子树
}

3.代码解析

前序遍历步骤

  • 检查当前结点

    • 如果指针root指向的当前结点为NULL,则表示当前子树为空,此时输出NULL(或不做任何操作),并返回,结束当前递归调用(即结束当前子树的前序遍历)。
  • 访问根结点

    • 如果当前结点不为NULL,则首先访问这个结点,通常是通过打印结点中的数据,例如printf("%d ", root->data)
  • 递归遍历左子树

    • 接着,递归地对当前结点的左子树进行前序遍历,即调用PrevOrder(root->left)。这个调用会将左子树作为新的遍历对象,重复上述步骤。
  • 递归遍历右子树

    • 最后,递归地对当前结点的右子树进行前序遍历,即调用PrevOrder(root->right)。同样,这个调用会将右子树作为新的遍历对象,重复上述步骤。

4.递归展开图

中序遍历函数InOrder

1.中序遍历思路:

中序遍历的思路是按照 “左子树->根结点->右子树” 的顺序遍历二叉树。在遍历过程中,对于遇到的每一个子树,我们首先递归地遍历其左子树,然后访问根节点,最后递归地遍历其右子树。

图形解析:>利用后序遍历整个链式二叉树的遍历顺序:

访问根1左子树- >访问根2左子树->访问根3左子树(空树NULL)->访问并打印根3的值->访问根3右子树(空树NULL)->访问并打印根2的值->访问根2右子树(空树NULL)->访问并打印根1的值->访问根1右子树->访问根4左子树->访问根5左子树(空树NULL)->访问并打印根5的值->访问根5右子树(空树NULL)->访问并打印根4的值->访问根4右子树->访问根6左子树(空树NULL)->访问并打印根6的值->访问根6右子树(空树NULL)。

2.中序遍历代码:左子树->根结点->右子树

//中序遍历
void InOrder(BTNode* root)
{
    //若指针root指向的当前树(子树)是空树(NULL),则当前树的根结点是个空结点(NULL),则此时打印 
    NULL表示当前树的中序遍历结束了。
if (root == NULL)
{
printf("NULL ");
return;
}

InOrder(root->left);//先访问当前树的左子树
printf("%d ", root->data);//再访问当前树根结点的值
InOrder(root->right);//后访问当前树的右子树
}

3.代码解析

  • 检查当前结点

    • 函数InOrder接收一个指向二叉树根结点的指针root
    • 首先检查root是否为NULL。如果为NULL,表示当前子树为空,此时打印"NULL"并直接返回,这表示当前递归分支的中序遍历结束。
  • 递归遍历左子树

    • 如果当前结点不为空(NULL),首先递归调用InOrder函数,传入当前结点的左子结点指针root->left,开始遍历当前结点的左子树。
    • 这个递归调用会一直进行,直到遇到一个空结点,这时最左边的子树遍历完成。
  • 访问根节点

    • 当左子树遍历完成后,执行printf("%d ", root->data);,打印当前结点的值。这是中序遍历中的“根”步骤。
  • 递归遍历右子树

    • 最后,递归调用InOrder函数,传入当前结点的右子结点指针root->right,开始遍历当前结点的右子树。
    • 这个递归调用同样会按照“左-根-右”的顺序遍历右子树。

通过这种方式,中序遍历确保了二叉树中的结点按照从左到右的顺序被访问,且每个结点的左子树在结点本身之前被访问,右子树在结点本身之后被访问。

后序遍历函数PostOrder

1.后序遍历的思路:

后序遍历的思路是按照 “左子树->右子树->根结点” 的顺序遍历二叉树。在遍历过程中,对于遇到的每一个子树,我们首先递归地遍历其左子树,然后递归地遍历其右子树,最后访问根结点。

图形解析:>利用后序遍历整个链式二叉树的遍历顺序:

访问根1左子树- >访问根2左子树->访问根3左子树(空树NULL)->访问根3右子树(空树NULL)->访问并打印根3的值->访问根2右子树(空树NULL)->访问并打印根2的值->访问根1右子树->访问根4左子树->访问根5左子树(空树NULL)->访问根5右子树(空树NULL)->访问并打印根5的值->访问根4右子树->访问根6左子树(空树NULL)->访问根6右子树(空树NULL)->访问并打印根6的值->访问并打印根4的值->访问并打印根1的值。

2.后序遍历代码:左子树->根结点->右子树 

//后序遍历
void PostOrder(BTNode* root)
{
//若指针root指向的当前树(子树)是空树(NULL),则当前树的根结点是个空结点(NULL),则此时打印 
    NULL表示当前树的后序遍历结束了。
if (root == NULL)
{
printf("NULL ");
return;
}

PostOrder(root->left);//先访问当前树的左子树
PostOrder(root->right);//再访问当前树的右子树
printf("%d ", root->data);//后访问当前树根结点的值
}

3.代码解析

检查当前结点是否为空

  • 函数PostOrder接收一个指向二叉树根结点的指针root
  • 首先检查root是否为NULL。如果为NULL,表示当前子树为空,此时打印"NULL"并直接返回,这表示当前递归分支的后序遍历结束。

递归遍历左子树

  • 如果当前结点不为空,首先递归调用PostOrder函数,传入当前结点的左子结点指针root->left,开始遍历当前结点的左子树。
  • 这个递归调用会一直进行,直到遇到一个空结点,这时最左边的子树遍历完成。

递归遍历右子树

  • 在左子树遍历完成后,递归调用PostOrder函数,传入当前结点的右子节点指针root->right,开始遍历当前结点的右子树。
  • 这个递归调用同样会按照“左-右-根”的顺序遍历右子树。

访问当前节点

  • 当左右子树都遍历完成后,访问当前结点的值,并将其打印出来。这是后序遍历中的“根”步骤,发生在左右子树之后。

通过这种方式,后序遍历确保了二叉树中的结点按照从左到右再到根的顺序被访问,且每个结点的左右子树都在结点本身之前被访问。这个过程会递归地发生在每个结点上,直到整棵树都被遍历完毕。

层序遍历函数LevelOrder

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

图形解析:

1.用队列实现层序遍历思路

层序遍历是一种广度优先的遍历策略,它按照树的层级从上到下、从左到右依次访问每个结点。使用队列可以很好地实现这种遍历方式,因为队列是一种先进先出(FIFO)的数据结构,能够确保我们按照正确的顺序访问结点。

用队列实现层序遍历的过程可以概括为:“出上一层,入下一层”。即每次从队列中取出一个结点进行访问,然后将该结点的非空孩子结点入队。这个过程一直重复,直到队列为空,此时所有结点都已经按照层序被访问过。通过这种方式,我们可以实现对二叉树的层序遍历。

图形解析:

用队列实现层序遍历的思路总结:“出上一层,入下一层”。

2.层序遍历代码

2.1.用队列q实现层序遍历时队列存储的数据类型一定是链式二叉树结点地址的原因
  • 访问子结点:队列中存储结点地址的目的是为了能够通过这些地址访问二叉树的每个结点。当我们从队列中取出一个结点的地址时,我们可以使用这个地址来访问该结点的左右子结点,并将它们入队,从而继续遍历过程。

  • 保持结构信息:如果队列中只存储结点中的数据,我们将失去与这些数据相关联的结点结构信息,即我们无法知道每个数据元素的左右子结点是什么。这样,我们就无法实现层序遍历,因为我们无法遍历到下一层的结点。

  • 维护树结构:链式二叉树的结构是通过指针链接来维护的。存储结点地址可以让我们在遍历过程中维护这种结构,确保我们可以根据指针找到每个结点的位置。

  • 递归与迭代:在递归遍历中,我们可以直接访问当前结点及其子结点,因为递归调用栈隐式地存储了这些信息。但在迭代遍历(如层序遍历)中,我们需要显式地使用队列来存储接下来要访问的结点地址。

  • 操作灵活性:存储结点地址使得我们可以对结点进行更复杂的操作,如修改结点的数据、添加或删除子结点等。如果我们只存储数据,这些操作将无法进行。

  • 动态结构:链式二叉树是一个动态的数据结构,其结点可以动态创建和销毁。存储结点地址使得我们能够动态地管理内存,这是通过简单地存储数据值无法实现的。

综上所述,使用队列存储结点地址而不是结点中的数据,是为了在层序遍历过程中保持对二叉树结构的访问和操作能力。

注意:队列的各个功能函数会在下面整个链式二叉树实现工程中提到,这里只说明队列结构体类型、队列存储数据类型、队列链表结点类型。

//队列的数据类型
typedef BTNode* QDataType;//用队列q实现层序遍历时一定是用队列存储二叉树结点的地址。

//队列链表的结点类型
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;

//队列的结构体类型
typedef struct Queue
{
QNode* head;//指针head指向队列队头数据。
QNode* tail;//指针tail指向队列队尾数据。
int size;//统计队列存储数据的个数
}Queue;
2.2.写法1:一行打印完二叉树存放的所有数据
//层序遍历(注意:利用队列实现二叉树的层序遍历)->写法1:一行打印完二叉树存放的所有数据
//注意:层序遍历是不把空结点地址存放队列q中的。而且层序遍历是用非递归方式进行。
void LevelOrder1(BTNode* root)
{
//创建队列q
Queue q;
//对队列q进行初始化为空队列
QueueInit(&q);

//二叉树根结点入队列q中
if (root)
QueuePush(&q, root);

//while(!QueueEmpty(&q))循环中,若队列q不是空队列则持续出上一层,进下一层。
while (!QueueEmpty(&q))
{
//出上一层1个双亲结点地址并进下一层2个左右孩子结点地址
       
//取上一层结点并出上一层结点
//取队头数据并打印出来
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
//删除队头数据
QueuePop(&q);

//进下一层左右孩子结点
if (front->left)//若左孩子不是空结点则入队
QueuePush(&q, front->left);
if (front->right)//若右孩子不是空结点则入队
QueuePush(&q, front->right);
}
printf("\n");

//销毁队列
QueueDestroy(&q);
}

2.2.1.代码解析 

(1)初始化队列

  • 创建一个队列q并使用QueueInit(&q)对其进行初始化。
Queue q;
QueueInit(&q);

(2)根节点入队

  • 如果二叉树不是空的(即root不为NULL),则将根结点的地址入队。
if (root)
    QueuePush(&q, root);

(3)遍历队列

  • 当队列不为空时则持续出上一层,进下一层,直到队列为空才结束while循环。
while (!QueueEmpty(&q))
{
  //…………省略
}

(4)出队并访问结点

  • 从队列中取出(出队)一个结点(该结点是当前层的某个结点),并访问它(例如,打印它的值)。
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
QueuePop(&q);

(5)左右孩子入队

  • 如果当前结点的左孩子存在(不为NULL),则将左孩子的地址入队。
  • 如果当前结点的右孩子存在(不为NULL),则将右孩子的地址入队。
if (front->left)
    QueuePush(&q, front->left);
if (front->right)
    QueuePush(&q, front->right);

(6)结束条件

  • 当队列为空时,表示所有结点都已经访问完毕,层序遍历结束。

(7)销毁队列

  • 在遍历完成后,使用QueueDestroy(&q)销毁队列,释放分配的空间。
QueueDestroy(&q);
2.3.写法2: 一行一行打印二叉树的每一层数据(即严格控制二叉树每一层数据的输出)
//层序遍历(注意:利用队列实现二叉树的层序遍历)->写法2:一行一行打印二叉树的每一层数据(即严格控制二叉树每一层数据的输出)
void LevelOrder2(BTNode* root)
{
//创建队列q
Queue q;
//对队列q进行初始化
QueueInit(&q);

//二叉树每层结点总数
int KLevelSize = 0;
//二叉树根结点入队列q中
if (root)
{
QueuePush(&q, root);
KLevelSize = 1;
}

while (!QueueEmpty(&q))
{
//出上一整层双亲结点地址(注意:上一整层一共有KLevelSize双亲结点地址要出队列q),进 
        下一整层左右孩子结点地址。

while (KLevelSize--)
{
//取上一层结点并出上一层结点
//取队头数据并打印出来
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
//删除队头数据
QueuePop(&q);

//进下一层左右孩子结点
if (front->left)//若左孩子不是空结点则入队
QueuePush(&q, front->left);
if (front->right)//若右孩子不是空结点则入队
QueuePush(&q, front->right);
}
//出完一层双亲结点地址后就要换行
printf("\n");
//更新二叉树每层结点总数
KLevelSize = QueueSize(&q);
}
//销毁队列
QueueDestroy(&q);
}
2.3.1.代码解析 

写法2的层序遍历思路

层序遍历的目的是按照从上到下、从左到右的顺序遍历二叉树的每一层。使用队列可以帮助我们在遍历每一层时跟踪该层的结点数量,从而能够在打印完一层的所有结点后进行换行,实现逐层打印。

层序遍历步骤

(1)初始化队列:创建一个队列q并对其进行初始化,用于存储二叉树结点的指针。

(2)根结点入队:检查根结点是否非空,如果非空,则将其入队,并设置当前层结点数KLevelSize为1。

if (root) {
    QueuePush(&q, root);
    KLevelSize = 1;
}

(3)处理队列中的结点

  • 当队列不为空时,进入循环while (!QueueEmpty(&q))处理:
    • 使用一个内部循环while (KLevelSize--)来处理当前层的所有结点,循环次数为当前层的结点数KLevelSize

(4)出队并访问当前层结点

  • 在内部循环while (KLevelSize--)中,执行以下操作:
    • 从队列中取出(出队)一个结点(队头结点),并打印其数据。
    • 将该节点的左子结点入队(如果存在)。
    • 将该节点的右子结点入队(如果存在)。
    • 内部循环每次迭代时,KLevelSize减1。

(5)换行:在内部循环while (KLevelSize--)结束后,打印一个换行符,表示当前层已全部打印完毕。

(6)更新下一层结点数:在换行后,更新KLevelSize为队列当前的长度,这表示下一层的结点总数。

KLevelSize = QueueSize(&q);

(7)重复步骤3-6:重复步骤3到步骤6,直到队列为空,此时所有层都已遍历完毕。

(8)销毁队列:最后,销毁队列,释放其占用的资源。

总结:通过以上步骤,代码实现了二叉树的层序遍历,并且确保了每一层的结点数据在一行中打印,每层打印完成后换行,从而实现了逐层打印二叉树的目的。

2.3.2.测试代码

4种遍历方式的时间复杂度和空间复杂度的计算过程

1.前序、中序、后序遍历的时间复杂度和空间复杂度

1.1.时间复杂度:O(n)

前序遍历、中序遍历和后序遍历的时间复杂度和空间复杂度计算过程是相似的,因为它们都遵循类似的递归结构。前序遍历、中序遍历和后序遍历的时间复杂度计算过程涉及到对二叉树中每个结点进行访问的次数。下面是计算前序遍历时间复杂度的步骤:

  • 确定遍历次数: 在遍历过程中,每个结点会被访问一次。因此,时间复杂度与二叉树中结点的数量(记为n)成正比。

  • 分析每个结点的访问时间: 假设访问一个结点的时间是常数,即O(1)。

  • 计算总时间: 由于每个结点都访问一次,总的时间复杂度就是每个结点的访问时间乘以结点的总数。因此,总的时间复杂度是O(n)。

1.2.空间复杂度 :O(n)
  • 确定额外空间:在遍历过程中,主要的额外空间消耗来自于递归调用栈。
  • 最坏情况:最坏的情况是二叉树退化成一条链表,此时递归调用栈的深度最大,递归的深度将达到n(即树的高度)。因此,最坏情况下的空间复杂度为O(n)
  • 最好情况:在最好的情况是二叉树是完全二叉树,那么树的高度为log(n),此时递归栈的空间复杂度为O(log(n))

1.3.总结
  • 时间复杂度O(n),因为每个结点都会被访问一次。
  • 空间复杂度
    • 最坏情况:O(n),当二叉树为退出化一个链表时。
    • 最好情况:O(log(n)),当二叉树是满二叉树时。

在大多数情况下,我们考虑的是最坏的空间复杂度,因此可以认为前序遍历、中序遍历和后序遍历的空间复杂度为O(n)

2.层序遍历的时间复杂度和空间复杂度

2.1.时间复杂度O(n)

(1)层序遍历的过程是这样的:

  • 初始化一个队列,并将根结点入队。
  • 当队列不为空时,进行以下操作:
    • 出队一个结点,访问该结点。
    • 如果该结点有左子结点,将左子结点入队。
    • 如果该结点有右子结点,将右子结点入队。

(2)时间复杂度的计算过程

注意:入队操作和出队操作的时间复杂度都是O(1)。

层序遍历的基本操作次数主要涉及以下两个操作:

  • 入队操作:将结点添加到队列中。
  • 出队操作:从队列中移除结点。

在层序遍历中,每个结点都会经历一次入队操作和一次出队操作。因此,对于二叉树中的每个结点,基本操作次数是2(一次入队和一次出队)。

如果二叉树有 n 个结点,那么:

  • 总入队操作次数 = n
  • 总出队操作次数 = n

所以,层序遍历的基本操作次数总和是 2n。这里的 n 是二叉树中结点的总数。这个总和反映了层序遍历算法的时间复杂度,即 O(n)

2.2.空间复杂度O(n)

层序遍历的空间复杂度取决于队列中最多可以存储多少个结点。

  • 空间复杂度分析
    • 在最坏的情况下,队列中会存储二叉树某一层的所有结点。
    • 如果二叉树是完全二叉树,那么最后一层可能包含接近 n/2 个结点。
    • 因此,队列的最大空间复杂度是 O(n)
2.3.总结

总结来说,层序遍历二叉树的时间复杂度是 O(n),空间复杂度也是 O(n)。这里的 n 是二叉树中结点的总数。

求二叉树的结点总数函数TreeSize

1.在用递归求二叉树结点总数时需要注意以下几点:

1.1.不能用局部变量的返回值来求二叉树结点总数的原因分析

(1)错误代码

测试代码

(2)原因

局部变量的限制:局部变量在函数调用结束后就会消失,它们不能在递归调用之间保留状态。因此,如果我们只在局部变量 size 中累加结点数,那么在递归返回时,这些累加的值不会被保留或传递给上一层的递归调用。

1.2.不能用静态变量size的返回值求二叉树结点总数的原因分析

(1)错误代码

 测试代码

(2)原因

注意:① 静态局部变量的生命周期: 静态局部变量在函数内部定义,但是它们存储在程序的静态存储区中,而不是栈上。这意味着静态局部变量的生命周期是整个程序的生命周期,而不是函数调用的生命周期。静态局部变量在程序启动时分配,在程序结束时释放,并且在函数调用之间保持它们的值。② 静态局部变量的初始化: 静态局部变量仅在第一次调用包含它们的函数时初始化一次。在后续的函数调用中,静态局部变量的值不会被重新初始化,而是保持它们在上一次调用后的值。

以下是详细解析为什么通常不使用静态局部变量size的返回值来统计二叉树的结点总数

原因1:状态保持:

  • 静态局部变量在递归调用之间保持其状态。这意味着在第一次调用 TreeSize 时初始化的 size 变量,在后续的递归调用中将继续持有其值,而不是被重新设置为0。
  • 当 TreeSize 函数递归调用自身时,静态局部变量 size 的值会在每次调用时增加,但是因为它不会在每次调用时重置,所以它会累加之前调用的结果。

原因2:多次调用问题

  • 如果在程序中多次调用 TreeSize 函数来统计同一个或不同二叉树的结点总数,静态局部变量 size 将不会在每次调用时重置为0。这将导致在第二次及以后的调用中,size 变量包含了之前调用的结果,从而导致统计结果错误。

原因3:无法重置

  • 由于静态局部变量 size 是在 TreeSize 函数内部定义的,因此无法从函数外部直接访问和重置它的值。这意味着在需要重新统计另一个二叉树的结点总数之前,我们无法将 size 重置为0。

1.3.不能用全局变量size求二叉树结点总数的原因分析

(1)虽然这个代码没什么错误,但是当我们求多棵二叉树的结点总数时,需要在主调函数中手动把全局变量设置为0,这样会非常麻烦,所以平时不用轻易用全局变量解决问题。同时使用全局变量可能会使代码更难以理解和维护,尤其是当全局变量在程序的不同部分被修改时,若没有正确修改则很容易使得TreeSize 函数的统计计算结果发生错误。

测试代码

2.递归求二叉树结点总数的思路及代码

2.1.思路

利用递归和分治思想来求二叉树结点总数。

分治思想的核心是将一个大问题分解成若干个小问题,分别解决这些小问题,然后将小问题的解合并起来,从而解决原始的大问题。在链式二叉树中统计结点总数的问题上,我们可以将整个问题分解为以下3个小问题:

①统计当前二叉树根的个数:在任何非空的二叉树中,根结点的个数总是1。

②统计当前二叉树左子树的所有结点个数:递归地对左子树进行相同的统计操作。

③统计当前二叉树右子树的所有结点个数:递归地对右子树进行相同的统计操作。

2.2.代码

(1)代码思路步骤
  • 递归基:如果root指向的当前二叉树(子树)为空树,即 root == NULL,则该树的结点总数为0。

  • 递归步骤:如果当前二叉树非空树,则结点总数由以下三部分组成:

    • 当前树的根结点个数:总是1。
    • 左子树的结点总数:通过递归调用 TreeSize(root->left) 来计算。
    • 右子树的结点总数:通过递归调用 TreeSize(root->right) 来计算。
  • 合并结果:将上述三部分的结果相加,即 1 + TreeSize(root->left) + TreeSize(root->right),得到当前二叉树的总结点数。

(2)代码
//求二叉树的结点总数
int TreeSize(BTNode* root)
{
//若指针root指向的当前树是个空树,则此时这个树的结点总数是0.
//若指针root指向的当前树是非空树,则当前树的结点总数 = 根 + 左子树的结点总数 + 右子树的结点总数 = 1 + TreeSize(root->left) + TreeSize(root->right).
    //递归计算左子树和右子树的结点数,并加上当前结点
return root == NULL ? 0 : 1 + TreeSize(root->left) + TreeSize(root->right);
}

2.3.递归展开图

注意:利用分治思想求链式二叉树所有结点个数的递归过程。

2.4.TreeSize函数的时间复杂度和空间复杂度 

注意:假设二叉树结点总数为n.

2.4.1.时间复杂度O(n)

(1)无论最好、最坏情况,时间复杂度主要取决于访问每个结点的次数,由于二叉树的结点总数是n,则时间复杂度为O(n)。

2.4.2.空间复杂度O(n)

注意:空间复杂度取决于TreeSize函数递归的深度

(1)最坏情况:二叉树是个链表,则TreeSize函数递归调用栈的深度是 n,则空间复杂度为O(n)。

(2)最好情况:二叉树是个完全二叉树,则TreeSize函数递归调用栈的深度是log(n),空间复杂度为O(log(n))

由于我们一般考虑最坏情况,所以递归求二叉树结点总数的空间复杂度是 O(n)。

求二叉树所有叶子结点总数函数TreeLeafSize

1.思路

递归求二叉树叶子结点总数的分治思路可以概括为以下几个步骤:

  • 分解:将原问题分解为规模更小的子问题。对于二叉树,这意味着将整个树分解为它的左子树和右子树。

  • 解决:递归地解决这些子问题。在每个子树中,重复相同的步骤,即判断该子树的根结点是否是叶子结点,如果是,则返回1;如果不是,则继续分解。

  • 合并:将子问题的解合并为原问题的解。对于二叉树,这意味着将左子树和右子树的叶子结点数相加,得到当前树(由根结点及其子树构成)的叶子结点总数。

以下是具体的分治思路:

  • 基本情况:如果当前结点为空(即当前子树为空),则返回叶子结点数为0。这是递归的基准情况。

  • 递归情况

    • 如果当前结点是一个叶子结点(即没有左子结点和右子结点),则返回1。
    • 如果当前结点不是叶子结点,那么需要分别计算它的左子树和右子树的叶子结点数。
  • 合并结果:将左子树的叶子结点数和右子树的叶子结点数相加,得到当前树(根结点及其子树)的叶子结点总数。

2.代码

(1)思路步骤

递归求解二叉树所有叶子结点总数的思路基于以下3个基本情况:

①空树:如果指针root指向的当前二叉树是空树,则当前二叉树的叶子结点总数为0。

②叶子结点:如果指针root指向的当前二叉树的根结点是叶子结点(没有左子树和右子树),则当前二叉树的叶子结点总数为1。

③非叶子结点:如果指针root指向的当前二叉树的根结点不是叶子结点,则当前二叉树的叶子结点总数 = 左子树的叶子结点总数 + 右子树的叶子结点总数。

(2)代码

//求二叉树的叶子结点总数
int TreeLeafSize(BTNode* root)
{
//若指针root指向的当前树是个空树,则此时这个树的叶子结点总数是0
if (root == NULL)
return 0;

//若指针root指向的当前非空树只有一个结点,则这个结点一定是叶子结点,则此时这个树的叶子结 
    点总数是1.
    //或者说判断指针root指向的当前二叉树(子树)的根结点是否是叶子结点。

if (root->left == NULL && root->right == NULL)
return 1;

//若指针root指向的当前非空树的结点总个数 > 1,则这棵树的叶子结点总数 = 左子树的叶子结点总 
    数 + 右子树的叶子结点总数 = TreeLeafSize(root->left) + TreeLeafSize(root->right)

return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

3.递归展开图

4.测试代码

求二叉树高度函数TreeHeight

注意:①这里默认链式二叉树的高度是从1开始定义的,则空树的高度是0;②求二叉树的高度不要往层序去走这样是走不通的;③若二叉树的高度是从0开始定义的,则空树的高度是-1;④前面求二叉树所有结点或者求二叉树所有叶子结点可以用前序遍历二叉树,但是在求二叉树的高度的时候必须用后序遍历求二叉树的高度。

1.思路

1.1.求二叉树高度思路:

求二叉树高度 = 左右子树高度的最大值 + 1。即二叉树的高度就是最深子树(即最深路径)的高度加1。

1.2.递归求二叉树高度的具体过程:

递归求二叉树高度的过程实际上是一个后序遍历的过程,因为我们需要先知道左右子树的高度,然后才能计算出当前树的高度。这个过程可以总结为:

图形解析:

注意:在二叉树中,每个结点的高度定义为该结点作为根结点时所在子树的高度。

  • 从根结点开始:递归计算二叉树高度的过程从树的根结点开始。

  • 递归地计算左子树和右子树的高度:在递归函数中,我们首先递归地计算当前结点的左子树的高度,然后计算右子树的高度。这是因为我们需要知道子树的高度,才能确定当前结点的高度。

  • 对于每个结点,其高度等于其左右子树高度的最大值加1:当我们有了左右子树的高度后,我们可以确定当前结点的高度。当前结点的高度是左右子树高度的较大值加1,这个“1”代表当前结点本身。

  • 当到达叶子结点时,其高度为0:叶子结点是树中没有子结点的结点。按照通常的定义,叶子结点的高度是0,因为它不包含任何子结点。

  • 最终,根结点的高度即为整个二叉树的高度:当我们从根结点开始递归计算,并最终回到根结点时,根结点的高度就是我们计算出的整个二叉树的高度。这是因为根结点的高度是基于其子树的高度计算出来的,而其子树的高度又是基于它们的子树的高度计算出来的,以此类推,直到叶子结点。

2.代码

//假设二叉树结点总数:n
//时间复杂度:O(n)
//空间复杂度:O(n)
//最好情况:求完全二叉树的高度,函数递归深度是log(n),所以空间复杂度是O(log(n))
//最坏情况:二叉树是个链表,函数递归深度是O(n),所以空间复杂是O(n)

//求二叉树的高度
int TreeHeight(BTNode* root)
{
//若指针root指向的当前二叉树是个空树,则此时当前二叉树的高度是0。
if (root == NULL)
return 0;

//若指针root指向的当前树是个非空树,则此时当前二叉树的高度 = 左右子树中的最大值 + 1.

//递归求左子树高度
int leftheight = TreeHeight(root->left);

//递归右子树高度
int rightheight = TreeHeight(root->right);

    //返回当前二叉树高度(注意:当前二叉树的高度 = 左右子树中的最大值 + 1)
return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}

3.递归展开图

注意:下面图形中箭头返回的数值都是当前结点作为子树根结点所在子树的高度。

4. 注意事项

求二叉树高度函数TreeHeight一定不能写成下面这样

原因:

  • 多次递归调用: 在比较左右子树高度时TreeHeight(root->left) 和 TreeHeight(root->right) 被调用了两次。第一次是在比较操作中,第二次是在选择最大值后加1。这意味着对于每个非叶子结点,我们实际上执行了两次不必要的递归调用。

  • 不必要的重复计算: 由于每次比较左右子树高度时,并没有保存计算结果,所以当确定最大高度后,还需要重新计算一次该子树的高度并加1。这种重复计算在大型二叉树中会导致大量的额外计算,显著降低程序性能。

总结:

  • 递归函数内部调用次数: 在实现递归函数时,应该尽量减少递归调用的次数。在这个代码中,可以通过存储左右子树高度的中间结果来避免重复调用。

  • 使用局部变量存储递归结果: 为了避免重复计算,可以在递归函数内部使用局部变量来存储子树的高度,然后再进行比较和加1操作。

求二叉树第k层结点个数(k > 1)函数TreeKLevelSize

1.思路

1.1.二叉树第k层结点个数思路:

二叉树第k层结点个数 = 左右子树第k - 1层结点个数之和。

1.2.递归求二叉树第k层结点个数的具体过程:

(1)检查空树:首先检查root是否为NULL。如果是,则直接返回0,因为空树在第k层的结点个数是0。

(2)检查第1层:接下来检查k是否等于1。如果等于1,则返回1,因为第1层只有一个结点,即根结点。

(3)递归计算子树

  • 如果k > 1,我们需要递归地计算左子树和右子树中第k-1层的结点个数。
  • 这通过递归调用TreeKLevelSize(root->left, k - 1)TreeKLevelSize(root->right, k - 1)来实现。
  • 将这两个调用的结果相加,就得到了当前树第k层的结点总数。

(4)返回结果:最后,将左子树和右子树第k-1层的结点个数之和返回,这就是第k层的结点总数。

总结:通过这种方式,递归函数不断地将问题分解为更小的子问题,直到达到基本情况。在回溯过程中,每个子问题的解被合并,最终得到整个二叉树第k层的结点总数。这个递归过程确保了每个结点都被正确地计数,且每个子树都被考虑到。

2.代码

//假设二叉树结点总数:n
//时间复杂度:O(n)
//空间复杂度:O(n)
//注意:这里的空间复杂度只考虑了递归栈的空间消耗。
//最好情况:求完全二叉树的高度,函数递归深度是log(n),所以空间复杂度是O(log(n))
//最坏情况:二叉树是个链表,函数递归深度是O(n),所以空间复杂是O(n)

//求二叉树第k层的结点个数(注意:k >= 1)
//注意:这里默认树的高度是从1进行定义的(即空树高度是0,只有一个结点的非空树的高度是1),而不是从0开始定义的。
int TreeKLevelSize(BTNode* root, int k)
{
//若指针root指向的当前树是个空树,则此时这个树在第k层的结点总数是0.
if (root == NULL)
return 0;

//当求指针root指向的当前非空树第k = 1层结点总数时,则此时这个树在第k=1层的结点总数是1.
if (k == 1)
return 1;

//当求指针root指向的当前非空树第k > 1层结点总数时,则此时
//这个树在第k层的结点总数 = 当前树的左子树在第k-1层的结点总数 + 当前树的右子树在第k-1层的结点 
    总数 = TreeKLevelSize(root->left, k-1) + TreeKLevelSize(root->right, k-1).

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

3.递归展开图

图形解析:下面图形中箭头返回的数值都是每个子树在第k层的结点总数(注意:每个子树对应的k都是不一样的)。

4.测试代码

查找函数TreeFind

1.思路

  • 判根:首先检查当前二叉树的根结点是否是要找的结点。
  • 在左子树找:如果根结点不是要找的结点,则递归地在左子树中查找。
  • 在右子树找:如果左子树中没有找到,则递归地在右子树中查找。
  • 返回结果:如果在左右子树中都没有找到,则返回NULL。

总的来说,把在一个二叉树中查找值为x的结点的大问题分治成(注:利用前序遍历来查找):判根是否是想要的结点、在左子树中找想要的结点、在右子树中找想要的结点等3个步骤的子问题来解决。

2.代码

//在二叉树中查找值为x的结点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
//若指针root指向的当前树是个空树,则这个空树没有我们要找的结点,所以返回NULL.
if (root == NULL)
return NULL;


//若指针root指向的当前树是个非空树,则判断当前树的根结点是否是我们要找的结点
if (root->data == x)
return root;


//若当前树的根结点不是我们要找的,则在当前树的左子树中找我们要找的结点
BTNode* ret1 = TreeFind(root->left, x);

if (ret1)//若ret1不是空结点则此时我们在当前树的左子树中找到我们要找的结点,则此时返回ret1.
return ret1;


//若在当前树的左子树没有找到,则在当前树的右子树找我们要找的结点。
BTNode* ret2 = TreeFind(root->right, x);

if (ret2)//若ret2不是空结点则此时我们在当前树的右子树中找到我们要找的结点,则此时返回ret2.
return ret2;


//若当前树的根结点不是我们要找的结点、且在当前树的左右子树都没有我们要找的结点,则返回NULL表示 
    当前树没有我们要找的结点。
return NULL;
}

3.递归展开图

图形解析:要查找的数据是7。(注意:由于在根1二叉树的左子树中找到了值为x结点,所以不需要在根1二叉树的右子树中找值为x的结点,所以下面才没有画根1二叉树的右子树的递归过程)

4.查找函数TreeFind错误写法(注意事项)

4.1.错误写法1

错误原因分析过程:

① 错误的写法位于:return TreeFind(root->left, x) || TreeFind(root->right, x); 

② 错误原因:虽然 TreeFind(root->left, x) || TreeFind(root->right, x) 在逻辑上表示如果当前结点不是我们要找的结点,那么应该先在左子树中查找,如果左子树中找不到,再到右子树中查找。但是,逻辑或运算符 || 的结果是布尔类型,而不是指针类型。因此,即使 TreeFind 函数的目的是返回一个结点的地址,使用 || 运算符会导致返回一个布尔值,而不是期望的指针,这是类型不匹配的错误。 

③ 小结:TreeFind(root->left, x) || TreeFind(root->right, x) 这种写法可以用于判断值为x的结点是否存在于二叉树中,但前提是 TreeFind 函数的返回类型必须是布尔类型 bool

4.2.错误写法2 

错误原因分析过程:
原因1:递归调用未处理返回值

在 TreeFind(root->left, x) 和 TreeFind(root->right, x) 这两个递归调用之后,没有将返回值赋给一个局部变量或者直接返回。这会导致以下问题:

  • 编译器可能会发出警告,因为递归调用的结果没有被使用。
  • 如果在左子树中找到了结点,那么 TreeFind(root->left, x) 会返回结点地址,但是这个返回值没有被使用,代码继续执行并调用 TreeFind(root->right, x),导致左子树的结果被忽略。
原因2:递归调用的结果未被正确利用(这是真正的问题所在)

在递归函数 TreeFind(root, x) 的内部,TreeFind(root->left, x) 和 TreeFind(root->right, x) 没有用一个局部变量来接收并把这个变量返回给主调函数。这意味着即使找到了目标结点,这个信息也会丢失,因为:

  • 如果在左子树中找到了结点,那么 TreeFind(root->left, x) 会返回结点地址,但是这个返回值没有被使用。
  • 如果在右子树中找到了结点,由于没有将 TreeFind(root->right, x) 的返回值赋给一个局部变量或者直接返回,所以这个结点地址也会丢失。

因此,即使二叉树中确实存在值为x的结点,这个错误的写法也会导致最外层的 TreeFind(root, x) 调用无法返回正确的结点地址,而是返回NULL或者一个随机值(取决于编译器的行为)。

小结

(1)写出这种写法的原因是没有真正理解递归的返回过程。递归函数的递归过程是把我们想要结果的值一层一层的返回给上一层的函数,最终通过最外层函数返回才得到我们想要结果的值。因此,若当层递归函数的内部存在调用下一层函数时,则一定要利用return在当层函数的内部返回下一层函数的返回值给上一层函数。

(2)注意:以下的结论只能用在递归函数有返回值的情况下才能使用,若递归函数无返回值的话,则在函数的内部是不能用以下的结论。

  • 递归函数每次利用return递归返回时都是把返回值返回给上一层函数,若上一层函数内部的某些地方没有把下一层函数传递的返回值返回给上上层函数,最终会导致最外层的函数返回的结果不是我们想要的结果。
  • 结论①:若递归函数的内部要调用下一层函数时,必须在当层函数的内部用一个局部变量接收下一层函数的返回值,同时当层函数要用return这个局部变量返回给上一层函数。
  • 结论②:若递归函数的返回值类型是布尔bool类型的且递归函数的内部要调用下一层函数时,必须在当层函数的内部可以直接利用return和逻辑与或者是逻辑或操作符把下一层函数的返回值直接返回给上一层函数,而不用定义一个局部变量来接收下一层函数的返回值。

4.3.错误写法3

错误原因分析过程:
错误原因1:多次递归调用

TreeFind(root->left, x)TreeFind(root->right, x)的条件判断中,每个函数被调用了两次。第一次是在if语句中检查是否找到了结点(返回非NULL值),第二次是在return语句中。实际上,如果第一次调用找到了结点,我们不需要再次调用相同的函数。

错误原因2:不必要的重复计算

由于在if语句中已经进行了递归调用,并且得到了非NULL的结果,这意味着已经找到了目标结点。因此,在return语句中再次调用相同的函数是多余的,并且会导致不必要的重复计算

二叉树销毁函数TreeDestroy

1.思路

递归销毁二叉树的思路步骤总结如下:

(1)检查当前结点:首先检查当前结点是否为空。如果是空结点(root == NULL),则表示已经到达了叶子结点的子结点,此时无需执行销毁操作,直接返回。

(2)递归销毁左子树:如果当前结点非空,按照后序遍历的顺序,首先递归调用TreeDestroy(root->left)来销毁当前结点的左子树。这样做的原因是后序遍历要求在处理当前结点之前,必须先处理其所有子结点,而左子树是当前结点的第一个子结点。

(3)递归销毁右子树:在左子树被销毁之后,递归调用TreeDestroy(root->right)来销毁当前结点的右子树。这样保证了在销毁当前结点之前,其右子树也被完全销毁。

(4)销毁当前结点:当左子树和右子树都已经被销毁之后,最后一步是使用free(root)来释放当前结点所占用的内存空间。由于左右子树在此之前已经被销毁,因此此时释放当前结点是安全的,不会导致对已释放内存的访问。

总结:后序遍历的顺序保证了在销毁任何结点之前,该结点的所有子结点都已经被销毁,从而可以安全地释放每个结点所占用的内存。完成销毁操作后,确实需要在主调函数中将根结点地址设置为空,以避免后续对已释放内存的非法访问。

2.代码

//注意:这种写法的二叉树销毁函数一定要在主调函数中把整个二叉树根结点地址root设置为空指针,以防止在销毁二叉树后在主调函数对野指针root指向的空间进行非法访问。
//二叉树销毁函数(利用后序遍历来销毁二叉树)
void TreeDestroy(BTNode* root)
{
//若指针root指向的当前树是空树的话,则此时不需要销毁空树所以此时利用return来结束销毁当前树。
if (root == NULL)
return;

//注意:在我们销毁二叉树时,一定是利用后序遍历销毁的,若用前序遍历销毁二叉树的话则前序遍历一定 
    会先利用free(root)销毁整棵树的根结点导致我们无法利用根结点指针root找到整棵二叉树的左右子树进 
    而导致我们无法销毁左右子树的所有结点。
//而利用中序遍历来销毁二叉树时也会发生与前序遍历相似的情况,只不过是中序遍历在遍历完当前左子树 
    后再利用free(root)销毁当前树的根结点进而导致无法找到当前树的右子树进而导致无法销毁当前树右子 
    树的所有结点。

//利用后序遍历销毁二叉树所有结点的原理是:后续遍历是通过当前树的根结点root先遍历左子树后再遍历 
     右子树,这样即使销毁了当前树的根结点后也可以找到当前树的左右子树进而销毁左右子树的所有结点, 
    最后才利用free(root)销毁当前树的根结点.

//后序遍历
TreeDestroy(root->left);//先销毁左子树
TreeDestroy(root->right);//再销毁右子树

//最后销毁当前树的根结点
free(root);
}

 判断二叉树是否是完全二叉树函数TreeComplete

注意:①判断这个树是不是完全二叉树之前一定要先把二叉树的层序遍历实现了,而且判断这个树是不是完全二叉树是在二叉树层序遍历的基础上实现的;②虽然判断这个树是不是完全二叉树有其他方法,但是只有在二叉树层序遍历的基础上去实现才更加容易。

1.利用层序遍历判断二叉树是否是完全二叉树的分析过程

注意:这里利用层序遍历遍历二叉树和上面的层序遍历LevelOrder函数的实现思路有点不一样。这里的层序遍历有如下要求:在对二叉树进行层序遍历时不管遍历访问到的当前结点的左右孩子结点是不是空结点都会把子结点地址入队列q中。

这里的层序遍历代码

void LevelOrder(BTNode* root)
{
Queue q;
QueueInit(&q);

//二叉树每层结点总数
int KLevelSize = 0;

//二叉树根结点入队列q中
if (root)
{
QueuePush(&q, root);
KLevelSize = 1;
}

while (!QueueEmpty(&q))
{
//出上一层,进下一层
while (KLevelSize--)
{
BTNode* front = QueueFront(&q);

            if(front == NULL)
            printf("NULL ");
            else
printf("%d ", front->data);
            //出上一层结点
QueuePop(&q);

//进下一层左右孩子结点
            //注意:不管左右孩子结点是否是空结点都会把孩子结点的地址入队列q中
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
//出完一层后就要换行
printf("\n");

//更新二叉树每层结点总数
KLevelSize = QueueSize(&q);
}
//销毁队列
QueueDestroy(&q);
}

1.1.利用上面层序遍历二叉树有如下特性:

//注意:这里的层序遍历遇到空结点时会打印NULL出来。

(1)非完全二叉树遍历结果是:非空结点数据打印结果不是连续的。

结论:当层序遍历到的当前结点是个空结点时,则该当前结点后面还有非空结点。

(2)完全二叉树结点遍历结果是:非空结点数据打印结果是连续的。

结论:当层序遍历到的当前结点是个空结点时,则该当前结点后面的所有结点都是空结点。

注意:完全二叉树利用层序遍历得出的遍历结果是连续不间断的而且遍历结果的中间是不可能嵌空的。

1.2.利用层序遍历判断二叉树是完全二叉树的思路

利用层序遍历二叉树时,当指针front遍历到的当前结点是个空结点时,若该当前结点后面所有结点都是空结点则这个二叉树就是完全二叉树;当指针front遍历到的当前结点是个空结点时,若该当前结点后面还有非空结点则这个二叉树就是非完全二叉树。

图形解析:

2.代码

2.1.代码思路步骤

(1)初始化队列:创建一个队列 q 并使用 QueueInit(&q) 初始化队列,这是为了后续进行层序遍历做准备。

(2)根结点入队:如果二叉树不是空树,将二叉树的根结点地址 root 入队列 q 中,使用 QueuePush(&q, root)

(3)层序遍历

  • 在队列不为空的情况下,使用 while (!QueueEmpty(&q)) 循环,通过 QueueFront(&q) 获取队头元素(当前结点),然后使用 QueuePop(&q) 将其出队。
  • 如果当前结点 front 为空,则使用 break 退出循环。如果当前结点不为空,将其左右孩子结点(无论是否为空)分别入队,使用 QueuePush(&q, front->left) 和 QueuePush(&q, front->right)

(4)判断完全性

  • 在层序遍历结束后,如果是完全二叉树的话,则队列中剩余的结点应当全部为空结点。
  • 再次使用 while (!QueueEmpty(&q)) 循环来检查队列中剩余的结点。
  • 使用 QueueFront(&q) 获取队头元素,如果该元素非空(即存在非空结点),则通过 return false; 返回,表明这不是一个完全二叉树。
  • 如果队列为空,说明所有剩余的结点都是空结点,那么二叉树是完全二叉树,返回 true

(5)销毁队列:最后,使用 QueueDestroy(&q); 销毁队列以释放其占用的资源。

代码逻辑:

  • 当在层序遍历过程中遇到第一个空结点时,停止将新的结点入队。
  • 检查队列中剩余的结点,如果都是空结点,则二叉树是完全二叉树;如果存在非空结点,则二叉树不是完全二叉树。

总结:这个方法有效地利用了完全二叉树的性质:在层序遍历中,一旦遇到一个空结点,则该结点之后的所有结点都必须是空结点。如果出现非空结点,则违反了完全二叉树的定义。通过这种方式,我们可以确定二叉树是否是完全二叉树。

2.2.代码

//假设二叉树结点总数是N.

//时间复杂度:O(N)
//空间复杂度:O(N)
//最好情况:在最好情况下,如果树是完全二叉树,队列中最多会存储树的最后一层的结点数,
即O(2^(h-1)),其中h是树的高度。由于完全二叉树的高度是logN,所以最好情况下的空间复杂度是O(2^(log N - 1)),即O(N)。

//最坏情况:二叉树是个链表,那么队列中将包含所有的N个结点,所以空间复杂度将是O(N)。


//判断二叉树是否是完全二叉树(利用层序遍历实现TreeComplete函数)
bool TreeComplete(BTNode* root)
{
//创建队列q
Queue q;

//对队列q进行初始化
QueueInit(&q);

//二叉树根结点入队列q中
if (root)
QueuePush(&q, root);

//层序遍历过程。若遍历到的双亲结点地址front是空指针则层序遍历结束。
while (!QueueEmpty(&q))
{
//通过取队列q的队头数据和删除队头数据来访问二叉树每一层双亲结点地址
BTNode* front = QueueFront(&q);//取队头数据

QueuePop(&q);//删除队头数据

if (!front)//若取出的双亲结点地址front是空指针,则层序遍历结束。
break;
else//若front不是空指针,则把当前双亲结点地址front的左右孩子结点地址入队
{
//无论双亲结点的左右孩子结点地址是否是空指针都要入队
QueuePush(&q, front->left);

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

//层序遍历结束后,判断队列q中剩余的结点地址是否都是空指针NULL,若全都是则说明这个二叉树是完 
  全二叉树;若队列q中剩余的结点地址存在一个结点地址是非空指针,则说明这个二叉树不是完全二叉树。

while (!QueueEmpty(&q))

{
//通过不断取队头数据后去判断队头数据是否是非空指针再不断来删除队头数据来判断这个二叉树 
        是否是完全二叉树。

BTNode* front = QueueFront(&q);

if (front)//若front是非空指针,则说明这个二叉树不是完全二叉树,则返回false。
return false;

QueuePop(&q);
}

//走到这一步说明队列q中所有剩余的结点地址已经全部出队了即此时队列q为空队列,则此时可以说明 
    队列q中剩余的结点地址都是空指针NULL,则说明这个二叉树是完全二叉树,则此时放回true。

return true;

//销毁队列
QueueDestroy(&q);
}

3.注意事项

注意:h是二叉树的高度,N是二叉树的结点总数,若二叉树是个完全二叉树则二叉树结点总数N在[ 2^(h - 1) ,2^h – 1 ]范围之间。

1.解析不可以利用二叉树结点总数N是否在[ 2^(h - 1) ,2^h – 1 ]范围之间去判断二叉树是个完全二叉树的原因

由于完全二叉树的定义要求除了最后一层外,所有层都是满的。并且完全二叉树最后一层结点必须是从左到右连续不间断排列的。即使二叉树的结点总数N在区间[ 2^(h - 1) ,2^h – 1 ]范围之间但不能确保结点的排列符合完全二叉树的要求,因为若二叉树最后一层结点是不连续的,那就无法判断这个二叉树是否是完全二叉树。

2.可以利用二叉树结点总数N是否等于 2h−12h−1 来判断二叉树是否是一个满二叉树。

满二叉树的定义:一棵二叉树中所有非叶子结点都具有两个子结点,并且所有的叶子结点都在同一层级上。对于满二叉树,其结点总数N与树的高度h之间的关系是固定的,即:N=2^h − 1.

因此,如果二叉树的结点总数N正好等于2^h − 1,则该二叉树是一个满二叉树。这里的h是二叉树的高度,即从根结点到最远叶子结点的最长路径上的边的数目。
 

判断满二叉树的步骤如下:

  • 计算二叉树的高度h。这可以通过递归地计算每个结点的最大深度来实现。

  • 计算二叉树的所有结点总数N。这同样可以通过递归地遍历所有结点并计数来实现。

  • 检查是否满足 N=2h−1N=2h−1。如果满足,则该二叉树是满二叉树;如果不满足,则不是满二叉树。

代码:

// 计算二叉树的结点总数
int TreeSize(BTNode* root) 
{
    if (root == NULL) return 0;
    return 1 + TreeSize(root->left) + TreeSize(root->right);
}

// 计算二叉树的高度
int TreeHeight(BTNode* root) 
{
    if (root == NULL) 
    return 0;

    int leftheight = TreeHeight(root->left);
    int rightheight = TreeHeight(root->right);

    return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}

// 判断二叉树是否为满二叉树
int isFullBinaryTree(BTNode* root) 
{
    int totalNodes = TreeSize(root);

    int height = TreeHeight(root);

    return totalNodes == (int)(pow(2, height) - 1);
}


链式二叉树整个工程代码

1.BinaryTree.h

//链式二叉树的实现

//二叉树存储的数据类型
typedef int BTDataType;

//二叉树结点的结构体类型
typedef struct BinaryTreeNode
{
BTDataType data;
struct BinaryTreeNode* left;
struct BinaryTreeNode* right;
}BTNode;

//前序遍历
void PrevOrder(BTNode* root);

//中序遍历
void InOrder(BTNode* root);

//后序遍历
void PostOrder(BTNode* root);

//层序遍历(注意:利用队列实现二叉树的程序变量)
void LevelOrder(BTNode* root);

//创建二叉树的一个结点
BTNode* BuyBTNode(BTDataType x);

//求二叉树的结点总数
int TreeSize(BTNode* root);

//求二叉树的叶子结点总数
int TreeLeafSize(BTNode* root);

//求二叉树的高度
int TreeHeight(BTNode* root);

//求二叉树第k层的结点个数(注意:k >= 1)
int TreeKLevelSize(BTNode* root, int k);

//在二叉树中查找值为x的结点
BTNode* TreeFind(BTNode* root, BTDataType x);

//二叉树销毁函数
void TreeDestroy(BTNode* root);

//判断二叉树是否是完全二叉树(利用层序遍历实现TreeComplete函数)
bool TreeComplete(BTNode* root);

2.BinaryTree.c

#include "Queue.h"

//前序遍历
void PrevOrder(BTNode* root)
{
//若指针root指向的当前树是个空树,则此时空树的根节点是空结点NULL,所以此时无需访问空树的根节点。
if (root == NULL)
return;

//先访问根结点的值
printf("%d", root->data);
//再遍历左子树
PrevOrder(root->left);
//最后遍历右子树
PrevOrder(root->right);
}

//中序遍历
void InOrder(BTNode* root)
{
//若指针root指向的当前树是个空树,则此时空树的根节点是空结点NULL,所以此时无需访问空树的根节点。
if (root == NULL)
return;

//先遍历左子树
InOrder(root->left);//再访问根结点的值
printf("%d", root->data);
//最后遍历右子树
InOrder(root->right);
}

//后序遍历
void PostOrder(BTNode* root)
{
//若指针root指向的当前树是个空树,则此时空树的根节点是空结点NULL,所以此时无需访问空树的根节点。
if (root == NULL)
return;

//先遍历左子树
PostOrder(root->left);
//再遍历右子树
PostOrder(root->right);
//最后访问根结点的值
printf("%d", root->data);
}

//层序遍历(注意:利用队列实现二叉树的层序遍历)->写法1:一行打印完二叉树存放的所有数据
void LevelOrder1(BTNode* root)//注意:层序遍历是不把空结点地址存放队列q中的。而且层序遍历是用非递归方式进行。
{
//创建队列q
Queue q;
//对队列q进行初始化
QueueInit(&q);

//二叉树根结点入队列q中
if (root)
QueuePush(&q, root);

//while(!QueueEmpty(&q))循环中,若队列q不是空队列则持续出上一层,进下一层。
while (!QueueEmpty(&q))
{
//出上一层1个双亲结点地址并进下一层2个左右孩子结点地址

//取上一层结点并出上一层结点
//取队头数据并打印出来
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
//删除队头数据
QueuePop(&q);

//进下一层左右孩子结点
if (front->left)//若左孩子不是空结点则入队
QueuePush(&q, front->left);
if (front->right)//若右孩子不是空结点则入队
QueuePush(&q, front->right);
}
printf("\n");

//销毁队列
QueueDestroy(&q);
}

//层序遍历(注意:利用队列实现二叉树的层序遍历)->写法2:一行一行打印二叉树的每一层数据(即严格控制二叉树每一层数据的输出)
void LevelOrder2(BTNode* root)
{
//创建队列q
Queue q;
//对队列q进行初始化
QueueInit(&q);

//二叉树每层结点总数
int KLevelSize = 0;
//二叉树根结点入队列q中
if (root)
{
QueuePush(&q, root);
KLevelSize = 1;
}

while (!QueueEmpty(&q))
{
//出上一整层双亲结点地址(注意:上一整层一共有KLevelSize双亲结点地址要出队列q),进下一整层左右孩子结点地址。
while (KLevelSize--)
{
//取上一层结点并出上一层结点
//取队头数据并打印出来
BTNode* front = QueueFront(&q);
printf("%d ", front->data);
//删除队头数据
QueuePop(&q);

//进下一层左右孩子结点
if (front->left)//若左孩子不是空结点则入队
QueuePush(&q, front->left);
if (front->right)//若右孩子不是空结点则入队
QueuePush(&q, front->right);
}
//出完一层双亲结点地址后就要换行
printf("\n");
//更新二叉树每层结点总数
KLevelSize = QueueSize(&q);
}
//销毁队列
QueueDestroy(&q);
}

//创建二叉树的一个结点
BTNode* BuyBTNode(BTDataType x)
{
//创建1个二叉树的结点
BTNode* TreeNode = (BTNode*)malloc(sizeof(BTNode));
if (TreeNode == NULL)
{
perror("malloc fail");
exit(-1);
}

//对结点的结构体成员进行初始化
TreeNode->data = x;
TreeNode->left = TreeNode->right = NULL;
return TreeNode;
}

//求二叉树的结点总数
int TreeSize(BTNode* root)
{
//若指针root指向的当前树是个空树,则此时这个树的结点总数是0.
//若指针root指向的当前树是非空树,则当前树的结点总数 = 根 + 左子树的结点总数 + 右子树的结点总数 = 1 + TreeSize(root->left) + TreeSize(root->right).
return root == NULL ? 0 : 1 + TreeSize(root->left) + TreeSize(root->right);
}

//求二叉树的叶子结点总数
int TreeLeafSize(BTNode* root)
{
//若指针root指向的当前树是个空树,则此时这个树的叶子结点总数是0
if (root == NULL)
return 0;

//若指针root指向的当前非空树只有一个结点,则这个结点一定是叶子结点,则此时这个树的叶子结点总数是1.
if (root->left == NULL && root->right == NULL)
return 1;

//若指针root指向的当前非空树的结点总个数 > 1,则这棵树的叶子结点总数 = 左子树的叶子结点总数 + 右子树的叶子结点总数 = TreeLeafSize(root->left) + TreeLeafSize(root->right)
return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

//求二叉树的高度
int TreeHeight(BTNode* root)
{
//若指针root指向的当前树是个空树,则此时这个树的高度是0。
if (root == NULL)
return 0;

//若指针root指向的当前树是个非空树,则此时这个树的高度 = 左右子树最大值 + 1.
//左子树高度
int leftheight = TreeHeight(root->left);
//右子树高度
int rightheight = TreeHeight(root->right);

return leftheight > rightheight ? leftheight + 1 : rightheight + 1;
}

//求二叉树第k层的结点个数(注意:k >= 1)
int TreeKLevelSize(BTNode* root, int k)//注意:这里默认树的高度是从1进行定义的(即空树高度是0,只有一个结点的非空树的高度是1),而不是从0开始定义的。
{
//若指针root指向的当前树是个空树,则此时这个树在第k层的结点总数是0.
if (root == NULL)
return 0;

//当求指针root指向的当前非空树第k = 1层结点总数时,则此时这个树在第k=1层的结点总数是1.
if (k == 1)
return 1;

//当求指针root指向的当前非空树第k > 1层结点总数时,则此时
//这个树在第k层的结点总数 = 当前树的左子树在第k-1层的结点总数 + 当前树的右子树在第k-1层的结点总数 = TreeKLevelSize(root->left, k-1) + TreeKLevelSize(root->right, k-1).
return TreeKLevelSize(root->left, k - 1) + TreeKLevelSize(root->right, k - 1);
}

//在二叉树中查找值为x的结点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
//若指针root指向的当前树是个空树,则这个空树没有我们要找的结点,所以返回NULL.
if (root == NULL)
return NULL;

//若指针root指向的当前树是个非空树,则判断当前树的根结点是否是我们要找的结点
if (root->data == x)
return root;
//若当前树的根结点不是我们要找的,则在当前树的左子树中找我们要找的结点
BTNode* ret1 = TreeFind(root->left, x);
if (ret1)//若ret1不是空结点则此时我们在当前树的左子树中找到我们要找的结点,则此时返回ret1.
return ret1;

//若在当前树的左子树没有找到,则在当前树的右子树找我们要找的结点。
BTNode* ret2 = TreeFind(root->right, x);
if (ret2)//若ret2不是空结点则此时我们在当前树的右子树中找到我们要找的结点,则此时返回ret2.
return ret2;

//若当前树的根结点不是我们要找的结点、且在当前树的左右子树都没有我们要找的结点,则返回NULL表示当前树没有我们要找的结点。
return NULL;
}

//注意:这种写法的二叉树销毁函数一定要在主调函数中把整个二叉树根结点地址root设置为空指针,以防止在销毁二叉树后在主调函数对野指针root指向的空间进行非法访问。
//二叉树销毁函数(利用后序遍历来销毁二叉树)
void TreeDestroy(BTNode* root)
{
//若指针root指向的当前树是空树的话,则此时不需要销毁空树所以此时利用return来结束销毁当前树。
if (root == NULL)
return;
//注意:在我们销毁二叉树时,一定是利用后序遍历销毁的,若用前序遍历销毁二叉树的话则前序遍历一定会先利用free(root)销毁整棵树的根结点导致我们无法利用根结点指针root找到整棵二叉树的左右子树进而导致我们无法销毁左右子树的所有结点。
//而利用中序遍历来销毁二叉树时也会发生与前序遍历相似的情况,只不过是中序遍历在遍历完当前左子树后再利用free(root)销毁当前树的根结点进而导致无法找到当前树的右子树进而导致无法销毁当前树右子树的所有结点。

//利用后序遍历销毁二叉树所有结点的原理是:后续遍历是通过当前树的根结点root先遍历左子树后再遍历右子树,这样即使销毁了当前树的根结点后也可以找到当前树的左右子树进而销毁左右子树的所有结点,最后才利用free(root)销毁当前树的根结点.
//后序遍历
TreeDestroy(root->left);//先销毁左子树
TreeDestroy(root->right);//再销毁右子树

//最后销毁当前树的根结点
free(root);
}

//判断二叉树是否是完全二叉树(利用层序遍历实现TreeComplete函数)
bool TreeComplete(BTNode* root)
{
//创建队列q
Queue q;
//对队列q进行初始化
QueueInit(&q);

//二叉树根结点入队列q中
if (root)
QueuePush(&q, root);

//层序遍历,若遍历到的双亲结点地址front是空指针则层序遍历结束。
while (!QueueEmpty(&q))
{
//通过取队列q的队头数据和删除队头数据来访问二叉树每一层双亲结点地址
BTNode* front = QueueFront(&q);//取队头数据
QueuePop(&q);//删除队头数据

if (!front)//若取出的双亲结点地址front是空指针,则层序遍历结束。
break;
else//若front不是空指针,则把当前双亲结点地址front的左右孩子结点地址入队
{
//无论双亲结点的左右孩子结点地址是否是空指针都要入队
QueuePush(&q, front->left);
QueuePush(&q, front->right);
}
}

//层序遍历结束后,判断队列q中剩余的结点地址是否都是空指针NULL,若全都是则说明这个二叉树是完全二叉树;若队列q中剩余的结点地址存在一个结点地址是非空指针,则说明这个二叉树不是完全二叉树。
while (!QueueEmpty(&q))
{
//通过不断取队头数据后去判断队头数据是否是非空指针再不断来删除队头数据来判断这个二叉树是否是完全二叉树。
BTNode* front = QueueFront(&q);
if (front)//若front是非空指针,则说明这个二叉树不是完全二叉树,则返回false。
return false;
QueuePop(&q);
}

//走到这一步说明队列q中所有剩余的结点地址已经全部出队了即此时队列q为空队列,则此时可以说明队列q中剩余的结点地址都是空指针NULL,则说明这个二叉树是完全二叉树,则此时放回true。
return true;
//销毁队列
QueueDestroy(&q);
}

3.test.c测试代码 

#include "BinaryTree.h"

void TestBinaryTree1()
{
//创建7个结点
BTNode* n1 = BuyBTNode(1);
BTNode* n2 = BuyBTNode(2);
BTNode* n3 = BuyBTNode(3);
BTNode* n4 = BuyBTNode(4);
BTNode* n5 = BuyBTNode(5);
BTNode* n6 = BuyBTNode(6);
BTNode* n7 = BuyBTNode(7);

//把7个结点链接起来形成树状的二叉树
n1->left = n2;
n1->right = n4;
n2->left = n3;
n2->right = n7;
n4->left = n5;
n4->right = n6;

//测试层序遍历函数
printf("层序遍历:\n");
LevelOrder2(n1);//一层一层打印二叉树的每一层数据
printf("\n");
LevelOrder1(n1);//用一行打印二叉树的所有数据

//前序遍历
printf("前序遍历:\n");
PrevOrder(n1);
printf("\n");

//中序遍历
printf("中序遍历:\n");
InOrder(n1);
printf("\n");

//后序遍历
printf("后序遍历:\n");
PostOrder(n1);
printf("\n");


//统计二叉树的结点总数
printf("二叉树的结点总数:>\n");
printf("TreeSize:%d\n", TreeSize(n1));


//统计二叉树叶子结点总数
printf("二叉树的叶子结点总数:>\n");
printf("TreeLeafSize:%d\n", TreeLeafSize(n1));

//统计树的高
printf("二叉树的高度:>\n");
printf("TreeHeight:%d\n", TreeHeight(n1));

//统计树的第k层的结点总数
printf("二叉树第k层的结点总数:>\n");
int k = 0;
scanf("%d", &k);//k = 3
printf("TreeKLevelSize:%d\n", TreeKLevelSize(n1, k));
scanf("%d", &k);//k = 4
printf("TreeKLevelSize:%d\n", TreeKLevelSize(n1, k));

//测试查找函数TreeFind
printf("查找:>");
printf("n7结点的地址:%p\n", n7);
printf("TreeFind:%p\n", TreeFind(n1, 7));

//测试用例1:

//测试判断二叉树是否是完全二叉树函数TreeComplete
printf("判断该二叉树是否是完全二叉树:>");
printf("%d\n", TreeComplete(n1));

//二叉树销毁函数
TreeDestroy(n1);

}

void TestBinaryTree2()
{
//创建7个结点
BTNode* n1 = BuyBTNode(1);
BTNode* n2 = BuyBTNode(2);
BTNode* n3 = BuyBTNode(3);
BTNode* n4 = BuyBTNode(4);
BTNode* n5 = BuyBTNode(5);
BTNode* n6 = BuyBTNode(6);
BTNode* n7 = BuyBTNode(7);

//把7个结点链接起来形成树状的二叉树
n1->left = n2;
n1->right = n4;
n2->left = n3;
n2->right = n7;
n4->left = n5;
n5->right = n6;

//测试层序遍历函数
printf("层序遍历:\n");
LevelOrder2(n1);//一层一层打印二叉树的每一层数据
printf("\n");
LevelOrder1(n1);//用一行打印二叉树的所有数据

//前序遍历
printf("前序遍历:\n");
PrevOrder(n1);
printf("\n");

//中序遍历
printf("中序遍历:\n");
InOrder(n1);
printf("\n");

//后序遍历
printf("后序遍历:\n");
PostOrder(n1);
printf("\n");


//统计二叉树的结点总数
printf("二叉树的结点总数:>\n");
printf("TreeSize:%d\n", TreeSize(n1));


//统计二叉树叶子结点总数
printf("二叉树的叶子结点总数:>\n");
printf("TreeLeafSize:%d\n", TreeLeafSize(n1));

//统计树的高
printf("二叉树的高度:>\n");
printf("TreeHeight:%d\n", TreeHeight(n1));

//统计树的第k层的结点总数
printf("二叉树第k层的结点总数:>\n");
int k = 0;
scanf("%d", &k);//k = 3
printf("TreeKLevelSize:%d\n", TreeKLevelSize(n1, k));
scanf("%d", &k);//k = 4
printf("TreeKLevelSize:%d\n", TreeKLevelSize(n1, k));

//测试查找函数TreeFind
printf("查找:>");
printf("n7结点的地址:%p\n", n7);
printf("TreeFind:%p\n", TreeFind(n1, 7));

//测试用例1:

//测试判断二叉树是否是完全二叉树函数TreeComplete
printf("判断该二叉树是否是完全二叉树:>");
printf("%d\n", TreeComplete(n1));
//二叉树销毁函数
TreeDestroy(n1);
}


//测试二叉树的功能函数->手动构建二叉树
int main()
{
//测链式二叉树的各个功能函数
//TestBinaryTree1();
TestBinaryTree2();

return 0;
}

4.Queue.h

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <assert.h>
#include "BinaryTree.h"

//队列的数据类型
typedef BTNode* QDataType;

//队列链表的结点类型
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;

//队列的结构体类型
typedef struct Queue
{
QNode* head;//指针head指向队列队头数据。
QNode* tail;//指针tail指向队列队尾数据。
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);

//判断队列是否是空队列
bool QueueEmpty(Queue* pq);

//统计队列中存放数据的个数
int QueueSize(Queue* pq);

//打印函数
void QueuePrint(Queue* pq);

5.Queue.c

#include "Queue.h"

//初始化函数->目的:把队列q初始化为空队列
void QueueInit(Queue* pq)
{
//判断指针pq是否是空指针
assert(pq);

//对队列q的结构体成员进行初始化
//由于队列最初状态是个空队列使得当用单链表实现队列时单链表最初的状态也是空链表,而要使得指针pq->head指向的链表是个空链表,则只需把指针pq->head的值设置为空指针NULL即可。
pq->head = pq->tail = NULL;//由于队列一开始是个空队列使得让队头指针pq->head和队尾指针pq->tail一开始指向空链表。
pq->size = 0;
}

//销毁函数
void QueueDestroy(Queue* pq)
{
//判断指针pq是否是空指针
assert(pq);

//由于队头指针pq->head始终是指向队头数据的,所以想要遍历整个链表的话则我们需要定义一个临时指针cur遍历链表。
QNode* cur = pq->head;
while (cur)//当cur指向NULL,则遍历链表结束。
{
//用指针del保存当前要销毁结点的地址
QNode* del = cur;
//注意:必须是先让cur移动到下一个结点的位置后再删除del位置的结点。
cur = cur->next;
free(del);
}
//当链表的所有结点都删除完后必须把队头指针和队尾指针都设置成空指针,否则没有置为空指针的话若通过主调函数队列的结构体访问到队头指针和队尾指针的话会造成对野指针进行访问的风险。
pq->head = pq->tail = NULL;
pq->size = 0;
}

//入队函数->尾插(插入数据)
//注意:由于在队列的所有功能函数中只有入队函数QueuePush才会增加队列的数据个数,
//而其他队列的功能函数都没有增加队列数据的个数,所以不需要单独写一个函数来创建队列链表的结点。
void QueuePush(Queue* pq, QDataType x)
{
//判断指针pq是否是空指针
assert(pq);
//创建结点
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}

//对新创建结点的成员变量进行初始化
newnode->data = x;
newnode->next = NULL;

//判断是否是头插
if (pq->head == NULL)//或者写成if (pq->tail == NULL)
pq->head = pq->tail = newnode;
else//尾插
{
//把新创建的结点和队尾结点链接起来
pq->tail->next = newnode;
//让队尾指针指向新的队尾结点
pq->tail = newnode;
}
pq->size++;
}

写法1:出队函数->头删(删除数据)
//void QueuePop(Queue* pq)
//{
////判断指针pq是否是空指针
//assert(pq);
//
////判断队列是否是空队列,若是空队列则不继续删除队头数据。
//assert(!QueueEmpty(pq));
//
////判断链表是否删除到还剩一个结点->这里要判断这种情况的原因是:由于我们会使用队头指针和队尾指针判断队列是否是空队列,当队列还剩一个结点而且还要进行头删的时候,
////若没有if语句的判断只执行else语句的内容会使得即使队头指针pq->head会被赋值成空指针NULL,但是队尾指针pq->tail会变成野指针,这样会在QueueEmpty函数中发生对野指针pq->tail进行解引用。
//if (pq->head->next == NULL)
//{
//free(pq->head);
////这种情况一定要把队头指针head和队尾指针tail置成空指针NULL,否则在判断队列是否是空队列时会发生对野指针进行解引用的风险
//pq->head = pq->tail = NULL;
//}
//else//头删
//{
////保存当前要删除结点的地址
//QNode* del = pq->head;
//
////注意:必须先让队头指针指向新的队头结点后再删除del位置的结点。
////让队头指针指向新的队头结点
//pq->head = pq->head->next;
////删除del位置的结点
//free(del);
//}
//pq->size--;
//}

//写法2:出队函数->头删(删除数据)
void QueuePop(Queue* pq)
{
//判断指针pq是否是空指针
assert(pq);

//判断队列是否是空队列
assert(!QueueEmpty(pq));

//头删的过程:
//保存当前要删除的结点
QNode* del = pq->head;
//注意:必须先让队头指针指向新的队头结点后再删除del位置的结点。
pq->head = pq->head->next;//换头
//删除结点
free(del);

if (pq->head == NULL)//若队头指针pq->head = NULL说明此时队列被删除成空队列,但是此时队尾指针pq->tail为野指针,则此时必须把野指针pq->tail设置成空指针。
pq->head = pq->tail = NULL;

pq->size--;
}

//取队头数据
QDataType QueueFront(Queue* pq)
{
//判断指针pq是否是空指针
assert(pq);
//判断队列是否是空队列,若队列为空则不需要取队头数据了。
assert(!QueueEmpty(pq));

return pq->head->data;//由于指针pq->head指向队列队头结点,所以只需通过队头指针访问队头结点中存放的数据即可。
}

//取队尾数据
QDataType QueueBack(Queue* pq)
{
//判断指针pq是否是空指针
assert(pq);
//判断队列是否是空队列,若队列为空则不需要取队尾数据了。
assert(!QueueEmpty(pq));

return pq->tail->data;//由于指针pq->head指向队列队尾结点,所以只需通过队头指针访问队尾结点中存放的数据即可。
}

//判断队列是否是空队列
bool QueueEmpty(Queue* pq)
{
//判断指针pq是否是空指针
assert(pq);

return (pq->head == NULL) && (pq->tail == NULL);//只有队头指针和队尾指针同时为空指针才能说明队列是个空队列
}

//统计队列中存放数据的个数
int QueueSize(Queue* pq)
{
//判断指针pq是否是空指针
assert(pq);

写法1:时间复杂度O(N)
//int size = 0;
//QNode* cur = pq->head;
//while (cur)
//{
//cur = cur->next;
//size++;
//}

//return size;

//写法2:时间复杂度O(1)
return pq->size;
}


//打印函数
void QueuePrint(Queue* pq)
{
//判断指针pq是否是空指针
assert(pq);

QNode* cur = pq->head;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}


原文地址:https://blog.csdn.net/2302_76314368/article/details/142744298

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!