数据结构(五)----树(含堆)

(前情提要:本节内容较多)

过去我们学习的数据结构都是线性的,但是在实际的应用中只有线性结构是无法满足需要的,今天我们学习的"树"就是一种非线性结构。

树的概念及结构

什么是树

树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的.

1.最上面的称为根结点,它没有前驱结点。

2.除了根结点,其余结点被分成多个互不相交 的集合,其中每个集合又是一棵子树,这些子树的根节点只有一个前驱,可以有0或多个后继

3.树是递归定义的。

一棵树:

子树不能相交

树的一些相关概念

以下面这棵树为例:

节点的度:一个节点含有的子树的个数称为该节点的度;如上图:A的为6

叶节点或终端节点:度为0的节点称为叶节点;如上图:B、C、H、I...等节点为叶节点

非终端节点或分支节点:度不为0的节点;如上图:D、E、F、G...等节点为分支节点

双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点;如上图:A是B的父节点孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点;如上图:B是A的孩子节点

兄弟节点:具有相同父节点的节点互称为兄弟节点;如上图:B、C是兄弟节点

树的度:一棵树中,最大的节点的度称为树的度;如上图:树的度为6

节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;

树的高度或深度:树中节点的最大层次;如上图:树的高度为4

堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点

节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先

子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙

森林:由m(m>0)棵互不相交的树的集合称为森林

(理解为主)

树的表示

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既要保存值域,也要保存结点和结点之间的关系,实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法等。

孩子表示法:

c 复制代码
struct TreeNode
{
   int data;
   struct TreeNode* subs[N];//存储此节点的孩子,最多有N个孩子
}

这种方法使用的前提是知道树的度(一个节点最大的孩子的数量 )

孩子兄弟表示法

c 复制代码
typedef int DataType;
struct Node
{
	structNode* _firstChild1;    // 第一个孩子结点
	structNode* _pNextBrother;   // 指向其下一个兄弟结点
	DataType _data;               // 结点中的数据域
};
树的实际应用

多用于文件系统,如linux:

二叉树

由于是非线性的,树的结构是比较复杂的,这里我们先学习最简单也最常用的一类:二叉树。也就是度为2的树。

二叉树的概念以及结构

一棵二叉树是结点的一个有限集合,该集合:

1.或者为空

2.由一个根节点加上两棵别称为左子树和右子树的二叉树组成

从上图中可以看出:

1.二叉树不存在度大于2的结点;

2.二叉树有左右子树之分,次序不能颠倒,是有序树。

二叉树是下图的基本情况或其复合而成:

现实中也有二叉树:

还有一些特殊的二叉树:

  1. 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是2^k-1 ,则它就是满二叉树。

  2. 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。

二叉树的性质:

1.若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有2i-1个结点.

2.若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2h-1.

3.对任何一棵二叉树, 如果度为0的叶结点个数为n0 , 度为2的分支结点个数为n2 ,则有 n0=n2 +1

4.若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1) .
5.对于具有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否则无右孩子

二叉树的存储结构:

二叉树一般可以使用两种结构存储,一种顺序结构,一种链式结构

1.顺序存储

顺序结构存储就是使用数组来存储,一般使用数组只适合表示完全二叉树,因为不是完全二叉树会有空间的浪费。而现实中使用中只有堆才会使用数组来存储,关于堆我们后面讲解。二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

2.链式存储

二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们学习中一般都是二叉链,后面学到高阶数据结构如红黑树等会用到三叉链。


结构定义:

c 复制代码
typedef int BTDataType;
// 二叉链
struct BinaryTreeNode
{
 struct BinTreeNode* _pLeft; // 指向当前节点左孩子
 struct BinTreeNode* _pRight; // 指向当前节点右孩子
 BTDataType _data; // 当前节点值域
}
// 三叉链
struct BinaryTreeNode
{
 struct BinTreeNode* _pParent; // 指向当前节点的双亲
 struct BinTreeNode* _pLeft; // 指向当前节点左孩子
 struct BinTreeNode* _pRight; // 指向当前节点右孩子
 BTDataType _data; // 当前节点值域
};

顺序二叉树

二叉树的顺序结构及堆

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。需要注意的是这里的堆和操作系统虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

堆的概念:

如果有一个关键码的集合K = { , , ,..., },把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足: <= 且 <= ( >= 且 >= ) i = 0,1,2...,则称为小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆的性质:

1.堆中某个节点的值总是不大于或不小于其父节点的值;

2.堆总是一棵完全二叉树。

堆的实现

堆结构的定义:

typedef int HPDataType;
typedef struct Heap
{
 HPDataType* _a;
 int _size;
 int _capacity; 
}Heap;//结构类似顺序表
// 堆的构建
void HeapCreate(Heap* hp, HPDataType* a, int n);
// 堆的销毁
void HeapDestory(Heap* hp);
// 堆的插入
void HeapPush(Heap* hp, HPDataType x);
// 堆的删除
void HeapPop(Heap* hp);
// 取堆顶的数据
HPDataType HeapTop(Heap* hp);
// 堆的数据个数
int HeapSize(Heap* hp);
// 堆的判空
int HeapEmpty(Heap* hp);
......

初始化:

void HeapInit(Hp* ph)//也和顺序表基本类似
{
	assert(ph);
	ph->array = NULL;
	ph->capacity = 0;
	ph->size = 0;
}

插入:

堆的插入也和顺序表尾插基本一样,但是不同的是,堆插入新数据后不一定是堆了,因为新数据不一定满足大/小堆的条件,此时我们需要一个调整算法,把这个新的数据结构调整为堆。这里我们建立一个小堆,使用向上调整算法

void HeapPush(Hp* ph, HpDataType x)
{
	assert(ph);
	//检查是否需要扩容
	if (ph->capacity == ph->size)
	{
		int newcapacity = ph->capacity == 0 ? 4 : 2 * ph->capacity;
		HpDataType* tmp = (HpDataType*)realloc(ph->array, newcapacity * sizeof(HpDataType));
		assert(tmp);
		ph->capacity = newcapacity;
		ph->array = tmp;
	}
	ph->size++;
	ph->array[ph->size - 1] = x;
	//向上调整;
	UpAdjust(ph, ph->size - 1);
}

那什么是向上调整算法呢?

其实很简单,就是将这个新数据跟它的双亲结点比较,如果它小,那么就把它和它的双亲结点交换,然后对它的新的双亲结点进行同样的操作。

在我们了解完全二叉树的结构时,有一个公式:

1.父结点2+1=左孩子,父结点 2+2=右孩子;

2.不管是左孩子还是右孩子,父结点=(孩子结点-1)/2。

(这里结点指下标)

void UpAdjust(Hp* ph, int child)
{
	assert(ph);
	int parent = (child - 1) / 2;
	while (child > 0)//child=0则到根了
	{
		if (ph->array[child] < ph->array[parent])
		{
			swap(&ph->array[child], &ph->array[parent]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else {
			break;
		}
	}

}

swap函数交由读者自行实现

删除函数:

删除堆是指删除堆顶的数据,这里使用的方法是将堆顶与最后一个数据进行交换然后再删除最后一个数据,最后进行向下调整。

void HeapPop(Hp* ph)
{
	assert(ph);
	assert(ph->array);
	swap(&(ph->array[0]), &(ph->array[ph->size - 1]));
	ph->size--;
	DownAdjust(ph,ph->size);//下面实现
}

向下调整算法:对于堆顶元素,如果比最小的孩子大,那么就交换,然后重复此操作

void DownAdjust(Hp* ph, int size)
{
	assert(ph);
	int parent = 0;
	int child = 2 * parent + 1;
	while (child<size)
	{
		if (ph->array[child] > ph->array[child + 1])
		{
			child++;
			if (child >= size) child--;
		}
		
		if (ph->array[child] < ph->array[parent])
		{
			swap(&ph->array[child], &ph->array[parent]);
			parent = child;
			child = 2 * child + 1;
		}
		else
		{
			break;
		}
	}
}

其他函数:

根据函数名来推断函数的作用吧

HpDataType GetHeapTop(Hp* ph)
{
	assert(ph);
	assert(ph->array);
	return ph->array[0];
}

void HeapDestroy(Hp* ph)
{
	assert(ph);
	free(ph->array);
	ph->capacity = 0;
	ph->size = 0;
}

bool IsHeapEmpty(Hp* ph)
{
	assert(ph);
	return ph->size == 0;
}

堆的应用

堆排序

步骤1.建堆

如果排升序就建大堆,反之则建小堆

步骤2.用堆删除思想来实现排序

通过向下调整算法进行排序的一个例子:

实际上,这是使用了大堆调整后堆顶为最大的特点,再使用交换来进行排序

TOP-K问题

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大。

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前K个元素来建堆
    前k个最大的元素,则建小堆
    前k个最小的元素,则建大堆

  2. 用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素。

  3. 将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。
    为什么这样建堆?
    实际上,就拿取最大K个元素来说,当前K个元素建好小堆之后,前K个小堆的最小值就是堆顶,此时拿新数据和堆顶比较,谁更大,谁就成为新的堆顶,然后再向下调整,取出新堆的最小值作为堆顶。

    我们自己测试一下

    void TestTopk()//topk问题,找出n个数中的前k个最大数
    {
    int n = 10000;
    int* a = (int*)malloc(sizeof(int) * n);
    srand(time(0));
    for (size_t i = 0; i < n; ++i)
    {
    a[i] = rand() % 1000000;//生成一百万以内的随机数
    }
    a[5] = 1000000 + 1;
    a[1231] = 1000000 + 2;
    a[531] = 1000000 + 3;
    a[5121] = 1000000 + 4;
    a[115] = 1000000 + 5;
    a[2335] = 1000000 + 6;
    a[9999] = 1000000 + 7;
    a[76] = 1000000 + 8;
    a[423] = 1000000 + 9;
    a[3144] = 1000000 + 10;
    PrintTopK( a, n, 10);
    }

    void PrintTopK(int* a, int n, int k)
    {
    HN hp;//创建并初始化堆
    HeapInit(&hp);
    //第一步:创建一个k个数的小堆
    for (int i = 0; i < k; i++)//将数组前10个数建立为小堆
    {
    HeapPush(&hp, a[i]);
    }
    //第二步:剩下的n-k个数和堆顶的数据比较,比堆顶大就替换它
    for (int i = k; i < n; i++)
    {
    if (a[i] > HeapTop(&hp))
    {
    HeapPop(&hp);
    HeapPush(&hp, a[i]);//这里直接调用Push函数就不要再写向下调整了,因为此函数中以及包含了向下调整
    //hp.a[0] = a[i];//如果是这种写法就要加上向下调整
    //AdjustDown(hp.a, hp.size, 0);
    }
    }
    HeapPrint(&hp);
    HeapDestroy(&hp);
    }
    //此时打印出来的就是我们设计的数字,这样我们就解决了top-k问题

算法效率:O(K(建堆)+(N-K)(需要比较的数量)*log2 K(每次调整的时间))

在实际中,由于K < < N,因此算法效率接近O(N),这个效率是非常高的。

链式二叉树

(要上强度了哦)

在学习链式二叉树之前,需要先对函数栈帧的创建和销毁有一定了解,不了解的可以前往学长的一篇博客了解

链式二叉树的结构

一些特殊的二叉树(如堆)适合用数组来表示,而一般的二叉树则非常适合用链式结构来表示:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int BTDataType;

typedef struct BinaryTreeNode
{
	BTDataType data;//数据
	struct BinaryTreeNode* left;//左孩子
	struct BinaryTreeNode* right;//右孩子
}BTNode;

这里在介绍链式二叉树的创建之前,我们先手动创建一棵简单的二叉树。

BTNode* BuyNode(BTDataType x)//这个函数用于为新节点开辟空间并且初始化
{
	BTNode* newnode = (BTNode*)malloc(sizeof(BTNode));
	newnode->data = x;
	newnode->left = newnode->right = NULL;
	return newnode;
}

BTNode* CreatBinaryTree()//手动创建一共二叉树
{
	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;
	nodeC->left = nodeE;
	nodeC->right = nodeF;
	return nodeA;
}

二叉树的遍历

学习二叉树结构,最简单的方式就是遍历。所谓二叉树遍历(Traversal)是按照某种特定的规则,依次对二叉树中的节点进行相应的操作,并且每个节点只操作一次。访问结点所做的操作依赖于具体的应用问题。 遍历是二叉树上最重要的运算之一,也是二叉树上进行其它运算的基础。分为:

1.前序遍历(Preorder Traversal 亦称先序遍历)------访问根结点的操作发生在遍历其左右子树之前。

2.中序遍历(Inorder Traversal)------访问根结点的操作发生在遍历其左右子树之中(间)。

3.后序遍历(Postorder Traversal)------访问根结点的操作发生在遍历其左右子树之后。

前序遍历

前序遍历是先访问根节点,再访问左孩子最后有孩子,我们假设将所有的NULL(空节点)编个号:记B的右孩子是1,最下面一层从左到右为234567那么对于这个二叉树的前序遍历顺序应该是:A B D 2 3 1 C E 4 5 F 6 7.将所有空节点去掉后就得到: ABDCEF.

代码实现:

void PreOrder(BTNode* root)//前序遍历二叉树,递归的方法(中左右)
{
	if (root == NULL)//节点为空就打印NULL后再返回
	{
		printf("NULL ");
		return;
	}
	printf("%c ", root->data);//要体现出我们在遍历,这里将值打印出来
	PreOrder(root->left);
	PreOrder(root->right);
}

整个过程分为往下递和往上归的过程,如下图,这里可能不是很好理解,但是尽量理解

中序遍历

我们先预测结果:

2 D 3 B 1 A 4 E 5 C 6 F 7.将所有空指针去掉可以得到:DBAECF

中序代码:

void MidOrder(BTNode* root)//中序遍历二叉树,递归(左中右)
{
	if (root == NULL)
	{
		printf("NULL");
		return;
	}
	MidOrder(root->left);
	printf("%c ", root->data);
	MidOrder(root->right);
}//各位读者可以自己测试结果

相信各位已经发现了,巧妙的处理在于,代码只是把前序遍历的打印放在了左右孩子的递归中间

后序遍历

经历过前序和中序,相信你一定也会后序的代码实现了

void PostOrder(BTNode* root)//后续遍历二叉树(左右中)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%c ", root->data);
}

二叉树的结点个数

maybe你会觉得结点个数很好数啊,只要每次递归的时候把数量加一就好了,然后写出这样的代码:

int BinaryTreeSize(BTNode* root)
{
  if(root==NULL)
  {
    return;
  }
  int count=0;

  count++;//遍历多少次非空节点就将count加几次
  BinaryTreeSize(root->left);
  BinaryTreeSize(root->right);

然而问题并没有那么简单,因为count是局部变量,每次创建的count并不能保存和累加(这也是循环和递归的不同之处),即使是用static将count放在静态区也不行,这是因为我们在第一次用函数求结点个数的时候没有问题,但是下次再用的时候就不是从0开始了,所以我们需要修改一下代码

void BinaryTreeSize(BTNode* root, int* p)//二叉树节点的个数,这里的p是外面main定义的变量的地址
{
	if (root == NULL)
	{
		return;
	}
	++(*p);
	BinaryTreeSize(root->left,p);
	BinaryTreeSize(root->right, p);
}
//这里我们使用传进一个int*的变量p,每次递归时都把*p加一,这样就解决了上面的问题

除此之外,还有一种写法可以得到二叉树结点的个数,各位可以自己理解一番(最好用画图辅助)

int BinaryTreeSize2(BTNode* root)//求二叉树节点个数,直接返回节点个数
{
	return root == NULL ? 0 :
		BinaryTreeSize2(root->left) 
		+ BinaryTreeSize2(root->right) + 1;
}

二叉树叶子结点的个数

叶子结点:左右孩子为空的结点。有了结点个数的遍历经验,我们首先想到的是,对于一个树的根结点,如果是空树,那就返回0,如果也是叶子结点(左右孩子为空),那么就返回1,如果都不是,那么就返回左右子树的叶子结点。

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层的结点个数

对于求第K层的结点个数的问题,当树为空时,应该返回0,如果不空,K等于1时,也就是第一层,结果一定是1,如果k不为1,那一定在这个根节点的左右子树中,也就是左右子树的K-1层的结点个数

int BinaryTreeLeaveSize(BTNode* root, int k)//返回二叉树第k层节点数
{
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	//root既不是空,k也不为1时,说明第k层在root的字树中,转换为求左右子树的第k-1层的节点树,本质也是递归
	return BinaryTreeLeaveSize(root->left, k - 1) + BinaryTreeLeaveSize(root->right, k - 1);
}

求二叉树的高度

我们用之前的树作为例子:

同样的思路,如果是空树,那么高度应该为0,返回0,如果不为空,说明有子树,就我们就返回左右子树高度的最大值+1,然后求子树的高度时继续递归

int BinaryTreeDepth(BTNode* root)//求树的高度(按第一层为1的标准)
{
	if (root == NULL)//树为空或节点为空时返回0
	{
		return 0;
	}
	int left = BinaryTreeDepth(root->left);//将值保存起来,否则开辟的栈帧过多
	int right = BinaryTreeDepth(root->right);
	return left > right ? left + 1 : right + 1;
}

查找值为x的结点

首先我们查找到值后,应该将对应的节点返回到函数外面去,所以这里函数的返回值应该是BTNode*.还是一样的思想:

第一步,当树为空树时,里面根本没有节点,所以外面返回空,

第二步,当树或节点不为空时,就应该判断当前节点对应的值是不是与X相同,如果当前节点的值与X相同就应该返回当前节点.

第三步,当我们把一二步都走完后,走到第三步这个位置说明当前节点即不为空节点,并且当前节点对应的值也不为X,那么这时我们就应该去当前节点的左右子树去寻找.

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)//查找值为x的节点,返回节点地址
{
	if (root == NULL)//空树或空节点就返回空
	{
		return NULL;
	}
	if (root->data == x)//对应上了就返回节点的地址
	{
		return root;
	}
	BTNode* left = BinaryTreeFind(root->left, x);
	if (left != NULL)//left会再次进入到此函数直到遇见NULL或对应值X
	{
		return left;//当left不为空时,代表left中一定存储着对应值X的地址
	}
	BTNode* right = BinaryTreeFind(root->right, x);
	if (right != NULL)
	{
		return right;
	}
	return NULL;//当左右子树都为空时,左右子树都不返回,这个节点下面所有的节点都不能与X对应上
}

这里还有一个设计细节,那就是如果在根找到了,左右子树就不用再找;如果在左子树找到了,右子树就不用再找了

二叉树的层序遍历

事实上,除了前序、中序、后序遍历,二叉树还有一个层序遍历,如下:

实现这个遍历,我们需要用到之前学的一个数据结构:队列

基本步骤:

1.先将根入队列

2.当前节点出队列后,将次此节点的左右孩子入队列

3.一直这样循环往复直到队列为空,说明最后一层已经没有节点了,遍历结束

如上个图,A入队,A出队,BC入队,B出队,D入队,C出队,EF入队,D出队,GH入队,E出队,I入队,F出队,GH出队,I出队

void BinaryTreeLevelOrder(BTNode* root)//层序遍历,需要使用队列
{
	if (root == NULL)
	{
		return;
	}
	Queue q;
	QueueInit(&q);//先定义一个队列后初始化它,再将根入队
	QueuePush(&q, root);
	while (!QueueEmpty(&q))//当队列不为空时就继续
	{
		BTNode* front = QueueFront(&q);//将队头出来并打印
		QueuePop(&q);//取出队头元素并保存在front后将队头删除掉
		printf("%c ", front->data);
		if (front->left != NULL)//若孩子不为空就将孩子入队
		{
			QueuePush(&q, front->left);
		}
		if (front->right != NULL)
		{
			QueuePush(&q, front->right);
		}
	}
	printf("\n");
	QueueDestroy(&q);//使用完后销毁队列
}

判断一个二叉树是不是完全二叉树

这里就用到了我们刚实现的层序遍历了

如果一棵树是完全二叉树,那么在遇到第一个NULL之后后面都是NULL,反之遇到第一个NULL后后面还有可能有非空结点。

// 判断二叉树是否是完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front == NULL)//当遇见第一个NULL时就直接跳出循环来到下面的语句
		{
			break;
		}
		else
		{
			QueuePush(&q, front->left);
			QueuePush(&q, front->right);
		}
	}

	// 遇到空了以后,检查队列中剩下的节点
	// 1、剩下全是空,则是完全二叉树
	// 2、剩下存在非空,则不是完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front)//如果不为空就返回false
		{
			QueueDestroy(&q);
			return false;
		}
	}

	QueueDestroy(&q);
	return true;//全部节点都走完了没有发现非空,说明队列里全是空就返回true.
}

二叉树的销毁

猜猜二叉树在销毁时用的是怎样的遍历方法呢?

没错,是后序,因为需要先把左右子树销毁掉才能销毁根节点,否则如果根被销毁了就找不到子树了。

// 二叉树销毁
//void BinaryTreeDestory(BTNode** root)传二级指针就不用在main函数中将root置空
void BinaryTreeDestory(BTNode* root)//后序遍历释放所有节点(先释放了根会找不到儿子)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeDestory(root->left);
	BinaryTreeDestory(root->right);
	free(root);
}

总结

总的来说,二叉树的主要难点在于链式二叉树,而其中最重要的思想在于------递归,把大事化为一个重复递归的小事,而贯穿其中的方法是:画图。

对了,要想真正掌握好树和二叉树,最重要的还是要找题目多加练习啊(如牛客、力扣)

相关推荐
lb36363636362 小时前
分享一下arr的意义(c基础)(必看)(牢记)
c语言·知识点
南东山人4 小时前
一文说清:C和C++混合编程
c语言·c++
stm 学习ing4 小时前
FPGA 第十讲 避免latch的产生
c语言·开发语言·单片机·嵌入式硬件·fpga开发·fpga
茶猫_9 小时前
力扣面试题 - 25 二进制数转字符串
c语言·算法·leetcode·职场和发展
ö Constancy9 小时前
Linux 使用gdb调试core文件
linux·c语言·vim
lb36363636369 小时前
介绍一下strncmp(c基础)
c语言·知识点
wellnw9 小时前
[linux] linux c实现共享内存读写操作
linux·c语言
Hera_Yc.H10 小时前
数据结构之一:复杂度
数据结构
肥猪猪爸11 小时前
使用卡尔曼滤波器估计pybullet中的机器人位置
数据结构·人工智能·python·算法·机器人·卡尔曼滤波·pybullet
linux_carlos11 小时前
环形缓冲区
数据结构