数据结构初阶--二叉数

目录

一、树概念及结构

1.1树的概念

1.2 树的相关概念

1.3树的表示

1.4 树在实际中的运用(表示文件系统的目录树结构)

二、二叉树概念及结构

2.1二叉树的概念

2.2现实中的二叉树:​编辑

2.3特殊的二叉树 

2.4二叉树的性质

2.5 二叉树的存储结构

2.6二叉树例题

3.二叉树的顺序结构及实现

3.1 二叉树的顺序结构

3.2 堆的概念及结构

3.3选择题 

3.4堆的实现算法

(1)向下调整算法

(2)向上调整算法(堆的创建)

3.5堆的实现(小堆)

3.6堆的应用

TOP-K问题

实现

堆排序

3.7建堆时间复杂度

 四、二叉树链式结构的实现

4.1前置说明

4.2二叉树的遍历

4.2.1 前序、中序以及后序遍历

4.2.2二叉树节点个数:

 4.2.3二叉树叶子节点的个数

  4.2.4二叉树第K层节点的个数

 4.2.5二叉树深度/高度

4.2.6查找值为x的节点

4.2.7层次/序遍历

4.2.8判断二叉树是否是完全二叉树

4.2.9二叉树的销毁

4.3二叉树基础oj练习与C++进阶中的二叉树遍历


一、树概念及结构

1.1树的概念

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

  • 有一个特殊的结点,称为根结点(树的起始点,也是顶点),根节点没有前驱结点
  • 除根节点外,其余结点被分成M(M>0)个互不相交的集合T1、T2、……、Tm,其中每一个集合Ti(1<= i <= m)又是一棵结构与树类似的子树。每棵子树的根结点有且只有一个前驱,可以有0个或多个后继
  • 因此,树是递归定义的。

image-20220809154630122

注:

  • 任何树都会被分成根和子树
  • 既可能是多个子树,也可能是空树

注意:树形结构中,子树之间不能有交集,否则就不是树,而是另外一种数据结构 – 图。

image-20220809153502380

1.2 树的相关概念

image-20220809153140973

  • 节点的度:一个节点含有的子树的个数称为该节点的度; 如上图:A的为6
  • 叶节点或终端节点:度为0的节点称为叶节点; 如上图:B、C、H、I...等节点为叶节点
  • 非终端节点或分支节点:度不为0的节点; 如上图:D、E、F、G...等节点为分支节点
  • 双亲节点或父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点; 如上图:A是B的父节点
  • 孩子节点或子节点:一个节点含有的子树的根节点称为该节点的子节点; 如上图:B是A的孩子节点
  • 兄弟节点:具有相同父节点的节点互称为兄弟节点; 如上图:B、C是兄弟节点
  • 树的度:一棵树中,含有最多度的节点对应的度称为树的度; 如上图:根节点的度最多,为6,故树的度为6
  • 节点的层次:从根开始定义起,根为第1层,根的子节点为第2层,以此类推;
  • 注:有点树的层次是从0开始的。但尽量选从1开始的,因为这样空树的高度就是0。否则高度就是-1.因为要区分第0层和高度0
  • 树的高度或深度:树中节点的最大层次; 如上图:树的高度为4
  • 堂兄弟节点:双亲在同一层的节点互为堂兄弟;如上图:H、I互为兄弟节点
  • 节点的祖先:从根到该节点所经分支上的所有节点;如上图:A是所有节点的祖先
  • 子孙:以某节点为根的子树中任一节点都称为该节点的子孙。如上图:所有节点都是A的子孙
  • 森林:由m(m>0)棵互不相交的树的集合称为森林; 1.3 树的表示 树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结

1.3树的表示

如何用代码来定义树呢?

 方式一:假设说明了树的度为N

 缺点:

  • 可能会存在空间的浪费
  • 万一没有限定树的度是多少呢?

方式二:顺序表里存储树节点的指针

 缺点:结构相对复杂,会出现二级指针的使用(SLDataType* a <=> struct treeNode** a)

方式三:结构数组存储/双亲表示法

 注:上面的方式各有优缺点,表示树结构的最优方法,左孩子右兄弟表示法

树结构相对线性表就比较复杂了,要存储表示起来就比较麻烦了,既然保存值域,也要保存结点和结点之间 的关系(比如该节点的子节点有几个?每个子节点的相对位置关系?),实际中树有很多种表示方式如:双亲表示法,孩子表示法、孩子双亲表示法以及孩子兄弟表示法 等。我们这里就简单的了解其中最常用的孩子兄弟表示法

typedef int DataType;
struct Node
{
    struct Node* firstChild1; // 第一个孩子结点,永远指向第一个孩子
    struct Node* pNextBrother; // 指向其下一个兄弟结点,指向孩子右边的兄弟
    DataType data; // 结点中的数据域
};

 

1.4 树在实际中的运用(表示文件系统的目录树结构)

image-20220809154408405


二、二叉树概念及结构

2.1二叉树的概念

一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。

image-20220809154728612

 特点:

1、每个节点最多有两棵子树,即不存在超过度为2的节点。

2、二叉树的子树有左右之分,且左右不能颠倒。

注意:对于任意的二叉树都是由以下几种情况复合而成的:

image-20220809154901412

2.2现实中的二叉树:

2.3特殊的二叉树 

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

总结:

  • 所有的叶子节点都在最后一层
  • 所有的分支节点都有两个孩子

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

总结:

  • 前n-1层都是满的
  • 最后一层不满,但是最后一层从左到右都是连续的(中间没有间隔)

2.4二叉树的性质

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

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

3. 对任何一棵二叉树, 如果度为0其叶结点个数为n_{0}^{} , 度为2的分支结点个数为n_{2}^{} ,则有n_{0}^{}n_{2}^{} +1(二叉树中度为0的节点比度为2的节点多一个)

4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log_{2}^{n+1} 

5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对 于序号为i的结点有:

  • 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
  • 若2i+1=n否则无左孩子
  • 若2i+2=n否则无右孩子

2.5 二叉树的存储结构

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

顺序结构:

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

答:因为存储完全二叉树的数组方式可以表示完全二叉树的结构

图解:

假如parent是父节点在数组中的下标,则: 

  • leftChild   = parent*2+1
  • rightChild = parent*2+2

于是我们可以得出推论:

  • 求任意节点对应的子节点,我们以它为父节点parent计算出其对应的的子节点Child,假如数组中不存在该下标,则可以说该子节点不存在。如果左右Child都不存在,则说明该父节点没有字节的,即叶节点
  • 求任意节点对应的父节点,我们不管它是左孩子还是右孩子,可以用公式parent=\frac{Child-1}{2}

用数组表示二叉树来存储的缺陷:

假如不是完全二叉树,则必会造成空间的浪费

链式存储:

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

在这里插入图片描述

   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; // 当前节点值域
   };

2.6二叉树例题

1. 某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )

A 不存在这样的二叉树

B 200

C 198

D 199

2.下列数据结构中,不适合采用顺序存储结构的是( )

A 非完全二叉树

B 堆

C 队列

D 栈

3.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )

A n

B n+1

C n-1

D n/2

4.一棵完全二叉树的节点数位为531个,那么这棵树的高度为( )

A 11

B 10

C 8

D 12

解答:高度为h的树的节点范围是:[2^{h-1},2^{h}-1]

 h=10对应范围是[512,1023]符合题意

5.一个具有767个节点的完全二叉树,其叶子节点个数为()

A 383

B 384

C 385

D 386

解答:

方法一:

 方法二:

假设度为0的节点有n0个

       度为1的节点有n1个

       度为2的节点有n2个

=>   n0+n1+n2=767

因为按n0=n2+1,n1为0或1(这里n1取0)

=>   n0+0+n0-1=767

=>     n0    =384

3.二叉树的顺序结构及实现

3.1 二叉树的顺序结构

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

3.2 堆的概念及结构

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

总结:

  • 大堆:树中一个树及子树中,任何一个父亲都大于等于孩子
  • 小堆:树中一个数及子树中,任何一个父亲都小于等于孩子

注意:所以的数组都可以表示成完全二叉树,但是它不一定是堆

堆的性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值;
  • 堆总是一棵完全二叉树。

3.3选择题 

1.下列关键字序列为堆的是:()

A 100,60,70,50,32,65

B 60,70,65,50,32,100

C 65,100,70,32,50,60

D 70,65,100,32,50,60

E 32,50,100,70,65,60

F 50,100,70,65,60,32

解题方法:画图

A.                                                                      B.

     

 其余同理,答案是:A、

2.已知小根堆为8,15,10,21,34,16,12,删除关键字 8 之后需重建堆,在此过程中,关键字之间的比较次 数是()。

A 1

B 2

C 3

D 4

3.一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为

A(11 5 7 2 3 17)

B(11 5 7 2 17 3)

C(17 11 7 2 3 5)

D(17 11 7 5 3 2)

E(17 7 11 3 5 2)

F(17 7 11 3 2 5)

4.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()

A[3,2,5,7,4,6,8]

B[2,3,5,7,4,6,8]

C[2,3,4,5,7,8,6]

D[2,3,4,5,6,7,8]

3.4堆的实现算法

(1)向下调整算法

堆的向下调整:

(以小堆为例)

  • 先设定根节点为当前节点(通过下标获取,标记为cur),比较左右子树的值,找出更小的值,用child来标记。(找小儿子)
  • 比较child和cur的值,如果child比cur小,则不满足小堆的规则,需要进行交换。
  • 如果child比cur大,满足小堆的规则,不需要交换,调整结束。(结束条件①:父亲比小儿子还小  ②调整到叶节点了,因为叶子没有孩子了)
  • 处理完一个节点之后,从当前的child出发,循环之前的过程。

向下调整(小堆)示例:
向下调整(小堆)

void AdjustDownSmall(int* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;
	//若算出孩子>=n,不存在。没有孩子循环结束
	while (child<n)
	{
		//选出左右孩子中最小的那一个
		if (child + 1 < n && a[child + 1] < a[child])
		{
			++child;
		}//默认左孩子最小,如果右孩子小于左孩子,则选择右孩子与父节点发生交换
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		//子节点大于父节点,循环结束
		else
		{
			break;
		}
	}
}

注:

  • 没必要定义leftChildrightChild。因为若有孩子,左孩子一定存在,右孩子则不一定存在。故只定义一个child就好了,child+1就是rightChild
  • 右孩子存在的条件是:child+1< 有效长度;

向下调整(大堆)示例: 

向下调整(大堆)

void AdjustDownBig(int* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;
	//若算出孩子>=n,不存在。没有孩子循环结束
	while (child<n)
	{
		//选出左右孩子中最小的那一个
		if (child + 1 < n && a[child + 1] > a[child])
		{
			++child;
		}//默认左孩子最大,如果右孩子大于左孩子,则选择右孩子与父节点发生交换
		if (a[child] > a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		//子节点大于父节点,循环结束
		else
		{
			break;
		}
	}
}

(2)向上调整算法(堆的创建)

下面我们给出两个数组,这个数组逻辑上可以看做一颗完全二叉树,但是还不是一个堆,现在我们通过算法,把它构建成一个堆。

根节点左右子树不是堆,我们怎么调整呢?

这里我们从倒数的第一个非叶子节点的子树开始调整,一直调整到根节点的树,就可以调整成堆。

堆的向上调整:
(以小堆为例)

  • 先设定倒数的第一个叶子节点为当前节点(通过下标获取,标记为child),找出他的父亲节点,用parent来标记。
  • 比较parent和child的值,如果child比parent小,则不满足小堆的规则,需要进行交换。
     
  • 如果child比parent大,满足小堆的规则,不需要交换,调整结束。
  • 处理完一个节点之后,从当前的parent出发,循环之前的过程。
int a[] =    {9,7,8,10,3,6}

向上调整(小堆)示例:在这里插入图片描述

 代码实现:

//向上调整法(小堆):
void AdjustUpSmall(int* 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;
		}
		else
		{
			break;
		}
	}
}

 
int a[] =    {1,5,3,8,7,6}

向上调整(大堆)示例:在这里插入图片描述

代码实现:

//向上调整法(大堆):
void AdjustUpBig(int* 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;
		}
		else
		{
			break;
		}
	}
}

3.5堆的实现(小堆)

堆的定义及创建:

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

堆的初始化:

void HeapInit(HP* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->size = hp->capacity = 0;
}

栈的扩容:

void checkCapacity(HP* hp)
{
	assert(hp);
	if (hp->size == hp->capacity)
	{
		int newCapacity = hp->capacity == 0? 4 : hp->capacity * 2;
		HPDataType* newA = realloc(hp->a, sizeof(HPDataType) * newCapacity);
		if (newA == NULL)
		{
			printf("realloc fail");
			exit(-1);
		}
		hp->capacity = newCapacity;
		hp->a = newA;
	}
}

节点数据交换:

void Swap(HPDataType* px,HPDataType* py)
{
	HPDataType tmp = *px;
	*px = *py;
	*py = tmp;
}

栈的插入:

堆的插入要求原二叉树为一个堆,再插入新数据后依然为一个堆。这就要求对新数据的位置进行调整,对此我们要学习向上调整算法。

注意:插入时只能在堆的末尾进行操作,否则会破坏堆的整个结构。

步骤:将数据插入到数组最后,再进行向上调整。

向上调整算法:把新节点顺着其双亲调整到合适的位置,就能形成一个新的堆。

int a[]={5,10,15,20}
int a[]={5,10,15,20,4}

示例:在这里插入图片描述

void HeapPush(HP* hp, HPDataType x)
{
	assert(hp);
	checkCapacity(hp);
	hp->a[hp->size] = x;
	hp->size++;
    AdjustUp(hp->a,hp->size,hp->size-1);//这里不能写成hp->size--,必须写成hp->size-1。前者改变了size本身,使得函数先增后减,陷入死循环
}

栈的判空:

bool HeapEmpty(HP* hp)
{
	assert(hp);
	return hp->size == 0;
}

栈的长度:

int HeapSize(HP* hp)
{
	assert(hp);
	return hp->size;
}

栈的打印:

void HeapPrint(HP* hp)
{
	assert(hp);
	for (int i = 0; i < hp->size; i++)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

堆的销毁:

void HeapDestory(HP* hp)
{
	assert(hp);
	free(hp->a);
	hp->size = hp->capacity = 0;
}

堆的删除:

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调 整算法。

void HeapPop(HP* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp)); 
	//交换首尾元素
	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	//删除尾元素
	hp->size--;
	//向下调整法:
	AdjustDown(hp->a, hp->size, 0);//从下标为0处开始向下调整
}

注:

  • 堆的删除不是删任意节点数据,而是规定只能删根节点数据
  • 本质上是取删除最值,小堆删最小值,大堆删最大值
  • 不可以直接删除根节点后,就把数组后面的数据往前挪。因为一方面会造成O(N)的消耗。另一方面会把父节点和子节点的关系打乱,二叉树崩溃

步骤:

  1. 把数组首元素与尾元素进行交换
  2. 再size - -,删除尾元素
  3. 此时存在的二叉树肯定不满足原来的大(小)堆,故用向下调整法调整数据,使其满足大(小)堆

完整代码:

Heap.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int HPDataType;

typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;

void HeapInit(HP* hp);
void HeapDestory(HP* hp);
void HeapPush(HP* hp, HPDataType x);
void HeapPop(HP* hp);
void AdjustUp(int* a, int n, int child);
void AdjustDown(int* a, int n, int parent);
void checkCapacity(HP* hp);
void HeapPrint(HP* hp);
bool HeapEmpty(HP* hp);
int HeapSize(HP* hp);
void Swap(HPDataType* px, HPDataType* py);

Heap.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//初始化
void HeapInit(HP* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->size = hp->capacity = 0;
}

//扩容
void checkCapacity(HP* hp)
{
	assert(hp);
	if (hp->size == hp->capacity)
	{
		int newCapacity = hp->capacity == 0? 4 : hp->capacity * 2;
		HPDataType* newA = realloc(hp->a, sizeof(HPDataType) * newCapacity);
		if (newA == NULL)
		{
			printf("realloc fail");
			exit(-1);
		}
		hp->capacity = newCapacity;
		hp->a = newA;
	}
}

//向上调整法(小堆):
void AdjustUp(int* a, int n, 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;
		}
		else
		{
			break;
		}
	}
}

//向下调整法(小堆):
void AdjustDown(int* a, int n, int parent)
{
	assert(a);
	int child = parent * 2 + 1;
	//若算出孩子>=n,不存在。没有孩子循环结束
	while (child<n)
	{
		//选出左右孩子中最小的那一个
		if (child + 1 < n && a[child + 1] < a[child])
		{
			++child;
		}//默认左孩子最小,如果右孩子小于左孩子,则选择右孩子与父节点发生交换
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		//子节点大于父节点,循环结束
		else
		{
			break;
		}
	}
}

//交换
void Swap(HPDataType* px,HPDataType* py)
{
	HPDataType tmp = *px;
	*px = *py;
	*py = tmp;
}

//插入
void HeapPush(HP* hp, HPDataType x)
{
	assert(hp);
	checkCapacity(hp);
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp->a, hp->size, hp->size - 1);
}

//打印
void HeapPrint(HP* hp)
{
	assert(hp);
	for (int i = 0; i < hp->size; i++)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

//判空
bool HeapEmpty(HP* hp)
{
	assert(hp);
	return hp->size == 0;
}

//长度
int HeapSize(HP* hp)
{
	assert(hp);
	return hp->size;
}

//删除
void HeapPop(HP* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp)); 
	//交换首尾元素
	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	//删除尾元素
	hp->size--;
	//向下调整法:
	AdjustDown(hp->a, hp->size, 0);//从下标为0处开始向下调整
}

//销毁
void HeapDestory(HP* hp)
{
	assert(hp);
	free(hp->a);
	hp->size = hp->capacity = 0;
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
int main()
{
	int a[] = { 70,56,30,25,15,10,75 };
	HP hp;
	HeapInit(&hp);
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
	{
		HeapPush(&hp,a[i]);
	}
	HeapPrint(&hp);
	
	HeapPop(&hp);
	HeapPrint(&hp);

	HeapDestory(&hp);

	return 0;
}

3.6堆的应用

TOP-K问题

就是找到一堆数中前K个最大的数或最小的数。比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

假如现在要从1000个数中找到最大的前10个数,怎么搞?

方式一:先排降序,前十个就是最大的。时间复杂度O({\color{DarkGreen} N*log^{N}})

方式二:N个数依次插入大堆,Pop K次,每次取堆顶的数据就是前K个。时间复杂度O(N+\log ^{N*k}{\color{DarkGreen} })

方式三:假设N非常大,N为10亿,内存中存不下这些数,它们存在文件中。k是100。方式一、二都不能用了。

注:

10亿个整数,大概占用多大的空间--4G

  • 1G=1024MB
  • 1G=1024*1024KB
  • 1G=1024*1024*1024Byte
  • 1G约为10亿字节

注:我们平时所用的排序是内排序,也就是数据存储于内存中。如果数据太大了,内存存不下,那内排序就用不上了。也就只能用外排序了,一种经典的外排序方法归并。但也有缺陷,毕竟现在数据存储在磁盘当中,排起来比在内存当中慢很多。故只能用k个数的小堆

方式三思路:

  1. 用前K个数建立一个K个数的小堆(找前k个最大元素则建小堆;找前k个最小元素则建大堆
  2. 剩下的N-K个数,依次跟堆顶的数据进行比较
  3. 如果堆顶的数据大,就替换堆顶的数据,再向下调整
  4. 最后堆里的K个数就是最大的K个数

时间复杂度:O(K+(N-K)\log ^{K})

实现

设置前K个最大的数据:

void TestTopK()
{
	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;
	}
	//再去设置10个比100w大的数
	a[5] = 1000000 + 1;
	a[1231] = 1000000 + 2;
	a[531] = 1000000 + 3;
	a[5121] = 1000000 + 4;
	a[115] = 100000 + 5;
	a[2335] = 100000 + 6;
	a[9999] = 100000 + 7;
	a[76] = 100000 + 8;
	a[423] = 100000 + 9;
	a[3144] = 100000 + 10;
	PrintTopK(a, n, 10);
}

获取栈顶数据:

HPDataType HeapTop(HP* hp)
{
	assert(hp);
	assert(!HeapEmpty(&hp));
	return hp->a[0];
}

打印前K个数:

方法一:

void PrintTopK(int* a, int n, int k)
{
	HP hp;
	HeapInit(&hp);
	//创建一个K个数的小堆 
	for (int i = 0; i < k; ++i)
	{
		HeapPush(&hp, a[i]);
	}
	//剩下的N-K个数跟堆顶的数据比较,比堆顶数据大,就替换它进堆
	for (int i = k; i < n; ++i)
	{
		if (a[i] > HeapTop(&hp))
		{
			HeapPop(&hp);
			HeapPush(&hp, a[i]);
		}
	}
	HeapPrint(&hp);
	HeapDestory(&hp);
}

方法二:

void PrintTopK(int* a, int n, int k)
{
	HP hp;
	HeapInit(&hp);
	//创建一个K个数的小堆 
	for (int i = 0; i < k; ++i)
	{
		HeapPush(&hp, a[i]);
	}
	//剩下的N-K个数跟堆顶的数据比较,比堆顶数据大,就替换它进堆
	for (int i = k; i < n; ++i)
	{
		if (a[i] > HeapTop(&hp))
		{
			hp.a[0] = a[i];
			AdjustDown(hp.a, hp.size, 0);
		}
	}
	HeapPrint(&hp);
	HeapDestory(&hp);
}

完整代码:

//获取堆顶数据
HPDataType HeapTop(HP* hp)
{
	assert(hp);
	assert(!HeapEmpty(&hp));
	return hp->a[0];
}

//打印前K个数
void PrintTopK(int* a, int n, int k)
{
	HP hp;
	HeapInit(&hp);
	//创建一个K个数的小堆 
	for (int i = 0; i < k; ++i)
	{
		HeapPush(&hp, a[i]);
	}
	//剩下的N-K个数跟堆顶的数据比较,比堆顶数据大,就替换它进堆
	for (int i = k; i < n; ++i)
	{
		if (a[i] > HeapTop(&hp))
		{
			hp.a[0] = a[i];
			AdjustDown(hp.a, hp.size, 0);
		}
	}
	HeapPrint(&hp);
	HeapDestory(&hp);
}

void TestTopK()
{
	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;
	}
	//再去设置10个比100w大的数
	a[5] = 1000000 + 1;
	a[1231] = 1000000 + 2;
	a[531] = 1000000 + 3;
	a[5121] = 1000000 + 4;
	a[115] = 100000 + 5;
	a[2335] = 100000 + 6;
	a[9999] = 100000 + 7;
	a[76] = 100000 + 8;
	a[423] = 100000 + 9;
	a[3144] = 100000 + 10;
	PrintTopK(a, n, 10);
}

int main()
{
	TestTopK();

	return 0;
}

堆排序

按照TopK问题的思路写一个堆的升序:空间复杂度O(N)

void HeapSort(int* a, int n)
{
	HP hp;
	HeapInit(&hp);
	//建立一个N个小堆
	for (int i = 0; i < n; i++)
	{
		HeapPush(&hp, a[i]);
	}
	//Pop N次
	for (int i = 0; i < n; i++)
	{
		a[i]=HeapTop(&hp);
		HeapTop(&hp);

	}
	HeapDestory(&hp);
}

int main()
{
	int a[] = { 70,56,30,25,15,10,70 };
	HeapSort(a, sizeof(a) / sizeof(a[0]));
	for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++)
	{
		printf("%d ", a[i]);
	}
	return 0;
}

要求优化到O(1) <=> 不能开辟新空间<=>不能用Heap <=> 把a构建成堆

堆排序即利用堆的思想来进行排序,总共分为两个步骤:

(1)建堆

升序建大堆,降序建小堆。

(2) 利用堆删除思想来进行排序

以排升序为例,先建好大堆,然后把最大的数也就是第一个数和最后一个数进行交换。把最后一个数看作从堆中取中,再把第一个数进行向下调整建立新堆,即能找到次大的数。反复重复上述步骤,便可以实现排序。

方法一:向上调整法

void HeapSort(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		AdjustUpSmall(a,i);
	}
}

方法二:向下调整法(小堆)

注:

  • 这种方法有个前提,那就是左右子树都是小堆
  • 但我现在连根节点都还没插进去,哪有子树用来判断呢?
  • 用迭代,倒着调
  • 但是叶子所在的子树不需要调,所以从倒着走第一个非叶子节点的子树(即最后一个节点的父亲)开始调

不建议建小堆:

 建议建大堆:

void HeapSort(int* a, int n)
{
    //建堆
    //时间复杂度O(n)
	for (int i = (n-1-1)/2; i >= 0; --i)
	{
		AdjustDownBig(a,n,i);
	}
    //依次选数,调堆=》排序
    //时间复杂度O(nlogn)
    for(int end=n-1;end>0;--end)
    {
        Swap(&a[end],&a[0]);
        //再调堆,选出次小的数
        AdjustDownBig(a,end,0);
    }
}

3.7建堆时间复杂度

向上调整建堆的时间复杂度为O(N*log N),向下调整的时间复杂度为O(N),所以我们常用向下调整建堆。

下面我将重点演示向下调整建堆的时间复杂度计算,感兴趣的小伙伴也可以自己验证一下向上调整的时间复杂度,方法相同。

 从图中我们也可以看出,每一层节点越多,需要移动的次数就越少,故导致最后的时间复杂度为 O(N)。

 四、二叉树链式结构的实现

注:普通二叉树增删查改没有什么价值,因为用来存储数据呢,太复杂了。不如链表顺序表

        故我们只关注它的递归遍历结构

  • 为后面学习更有用的树打基础
  • 很多OJ题结构就是普通二叉树
普通二叉树

价值体现:在它的基础之上,增加一些性质,才有意义:

1.搜索二叉树​​​

  • 最多查找高度次
  • 时间执行最坏情况:\frac{h}{2},故时间复杂度为O(n)
  • 进阶版是平衡二叉树:AVLTree和红黑树
  • 再进阶就是B数

 2.huffman tree

4.1前置说明

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。由于现在大家对二 叉树结构掌握还不够深入,为了降低大家学习成本,此处手动快速创建一棵简单的二叉树,快速进入二叉树 操作学习,等二叉树结构了解的差不多时,我们反过头再来研究二叉树真正的创建方式。

typedef int BTDataType;
//定义树
typedef struct BinaryTreeNode
{
	struct BinaryTreeNode* leftChild;
	struct BinaryTreeNode* rightChild;
	BTDataType data;
}BTNode;
//创建新节点
BTNode* BuyNode(BTDataType x)
{
	BTNode* node = (BTNode*)malloc(sizeof(BTNode));
    if (node == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	node->leftChild = node->rightChild = NULL;
	node->data = x;
}
//插入整数
BTNode* CreatBinaryTree()
{
	BTNode* node1 = BuyNode(1);
	BTNode* node2 = BuyNode(2);
	BTNode* node3 = BuyNode(3);
	BTNode* node4 = BuyNode(4);
	BTNode* node5 = BuyNode(5);
	BTNode* node6 = BuyNode(6);

	node1->leftChild = node2;
	node1->rightChild = node4;
	node2->leftChild = node3;
	node4->leftChild = node5;
	node4->rightChild = node6;
	return node1;
}
//插入字符
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->leftChild = nodeB;
	nodeA->rightChild = nodeC;
	nodeB->leftChild = nodeD;
	nodeC->leftChild = nodeE;
	nodeC->rightChild = nodeF;
	//BuyNode把未处理部分已经赋为NULL了
	return nodeA;
}

注意:上述代码并不是创建二叉树的方式,真正创建二叉树方式后序详解重点讲解。

4.2二叉树的遍历

我们以后看待二叉树,要把它看成根、左子树、右子树三部分

 

 。。。。重复以上操作,直到根无子树即为结束

4.2.1 前序、中序以及后序遍历

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

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

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

操作顺序:根、左子树、右子树

  • 先遍历根A, A遍历完后遍历A的左子树B,左子树B遍历完了才轮到右子树C
  • 遍历A的左子树B,B遍历完后遍历B的左子树D,左子树D遍历完了才轮到右子树\Phi
  • 遍历B的左子树D,D遍历完后遍历D的左子树\Phi,左子树\Phi遍历完了才轮到右子树\Phi
  • 因为D的两个子树为NULL,故B的左子树遍历完了
  • 接下来遍历B的右子树\Phi
  • 因为B的左右子树都遍历完了,故A的左子树遍历完了
  • 接下来遍历A的右子树C,C遍历完后遍历C的左子树E,左子树E遍历完了才轮到右子树F
  • 遍历C的左子树E,E遍历完后遍历E的左子树\Phi,左子树\Phi遍历完了才轮到右子树\Phi
  • 因为E的两个子树为NULL,故C的左子树遍历完了
  • 接下来遍历C的右子树F

  • F遍历完后,遍历它的左子树 \Phi, 左子树\Phi遍历完了才轮到右子树\Phi

  • 因为F的两个子树为NULL,故C的右子树遍历完了

 总结一下顺序:A B D NULL NULL NULL C E NULL NULL F NULL NULL

在这里插入图片描述
先序遍历可以想象为,一个小人从一棵二叉树根节点为起点,沿着二叉树外沿,逆时针走一圈回到根节点,路上遇到的元素顺序,就是先序遍历的结果

代码实现:

// 二叉树前序遍历
void PreOrder(BTNode* root)         //这里的root接收的是根节点A的地址
{
	if (root == NULL)				//递归中遇到NULL,返回上一层节点
	{
		printf("NULL ");
		return;
	}
	printf("%c ", root->data);
	PreOrder(root->leftChild);		//递归遍历左子树
	PreOrder(root->rightChild);		//递归遍历右子树
}

int main()
{
	BTNode* root = CreatBinaryTree();
	// 二叉树前序遍历
	PreOrder(root);

}

运行结果:

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

操作顺序:左子树、根、右子树

总结一下顺序:NULL D NULL B NULL A NULL E NULL C NULL F NULL 

在这里插入图片描述
记住,中序遍历就是从最左边开始,把每个节点垂直投影到同一直线上,然后从左往右读值就可以了,多看几遍动图就理解了

代码实现:

void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->leftChild);
	printf("%c ", root->data);
	InOrder(root->rightChild);
}

int main()
{
	BTNode* root = CreatBinaryTree();
	//二叉树中序遍历
	InOrder(root);
}

运行结果:

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

操作顺序:左子树、右子树、根

总结一下顺序:NULL NULL D NULL B NULL NULL E NULL NULL F C A 

在这里插入图片描述
就是围着树的外围绕一圈,如果发现一剪刀就能剪下的葡萄(必须是一颗葡萄)(也就是葡萄要一个一个掉下来,不能一口气掉超过1个这样),就把它剪下来,组成的就是后序遍历了。

 代码实现:

void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL");
		return;
	}
	PostOrder(root->leftChild);
	PostOrder(root->rightChild);
	printf("%c ", root->data);
}

int main()
{
	BTNode* root = CreatBinaryTree();
	//二叉树后序遍历
	PostOrder(root);
}

运行结果:

4.2.2二叉树节点个数:

错误的写法:

int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	int count = 0;
	++count;
	BinaryTreeSize(root->leftChild);
	BinaryTreeSize(root->rightChild);
	return count;
}

int main()
{
	//二叉树的节点个数
	printf("节点个数是:%d", BinaryTreeSize(root));
}

运行结果:

注:虽然每进一个节点我加一次,看似可以。但是我里面定义了int count=0,每进一个节点前先清空再加1,结果永远为1

改进:用static修饰count

int BinaryTreeSize(BTNode* root)
{
	if (root == NULL)
	{
		return 0;//不给个数字会报错,因为返回类型是int
	}
	static int count = 0;
	++count;
	BinaryTreeSize(root->leftChild);
	BinaryTreeSize(root->rightChild);
	return count;
}

int main()
{
	//二叉树的节点个数
	printf("节点个数是:%d", BinaryTreeSize(root));
    printf("节点个数是:%d", BinaryTreeSize(root));
}

运行结果:

 注:这种写法让int count=0;使用一次,后续不会影响count的加1,确实会得到正确结果。但是这种写法也是错的!因为一旦你之后调用这个函数,它会把之前的节点树也一并加上。即使改成全局变量,也会有相似的问题

改进:

void BinaryTreeSize(BTNode* root,int* pn)
{
	if (root == NULL)
	{
		return;
	}
	++*pn;
	BinaryTreeSize(root->leftChild,pn);
	BinaryTreeSize(root->rightChild,pn);
}

int main()
{

	//二叉树的节点个数
	int n1 = 0;
	BinaryTreeSize(root,&n1);
	printf("节点个数是:%d\n", n1);

	int n2 = 0;
	BinaryTreeSize(root, &n2);
	printf("节点个数是:%d\n", n2);
}

运行结果:

 简洁牛逼的写法:

int BinaryTreeSize(BTNode* root)
{
	return root == NULL ? 0 : BinaryTreeSize(root->leftChild) + BinaryTreeSize(root->rightChild) + 1;
}

int main()
{
	//二叉树的节点个数
	printf("节点个数是:%d", BinaryTreeSize(root));
}

原理:打个比方,校长要调查全校学生的人数。他不可能挨个寝室记录。它会叫来每个学院的院长,让他们把该院的师生人数统计好,交上去,以此类推。得到每个学院的师生人数后,再把自己加上去就是全校的师生人数了。当然了,如果为空,就表示包括校长在内的全校师生压根就不存在,鬼故事嘛你懂的

 4.2.3二叉树叶子节点的个数

有缺陷的写法:

//二叉树叶子节点个数
int BinaryTreeLeafSize(BTNode* root)
{
	return root == NULL ? 1 : BinaryTreeLeafSize(root->leftChild) + BinaryTreeLeafSize(root->rightChild);
}

int main()
{
	BTNode* root = CreatBinaryTree();
	//二叉树叶子节点个数
	printf("叶子节点个数是:%d\n",BinaryTreeLeafSize(root));
}

 运行结果:

 注:该写法没有考虑到当二叉树为空时的情况,如果二叉树为空,那就不存在叶子节点

正确的写法:

int BinaryTreeLeafSize(BTNode* root)
{
	
	if (root == NULL)		//二叉树不存在,叶节点为0
	{
		return 0;		
	}
	if (root->leftChild == NULL && root->rightChild == NULL)			//二叉树存在,节点的两个子树为空,则该节点为叶子节点,+1
	{
		return 1;
	}
	return BinaryTreeLeafSize(root->leftChild) + BinaryTreeLeafSize(root->rightChild);			//二叉树存在,节点的两个子树不是叶子,那么叶子节点等于左子树的叶子节点+右子树的叶子节点
}

int main()
{
	BTNode* root = CreatBinaryTree();
	//二叉树叶子节点个数
	printf("叶子节点个数是:%d\n",BinaryTreeLeafSize(root));
}

运行结果:

  4.2.4二叉树第K层节点的个数

int BinaryTreeKSize(BTNode* root,int k)
{
	assert(k > 0);
	if (root == NULL)
	{
		return 0;
	}
	if (k == 1)
	{
		return 1;
	}
	//root不为空,k也不等于1,说明root这棵树的第k节点在子树里面
	//转换成求左右子树的第k-1层的子节点数量
	return BinaryTreeKSize(root->leftChild, k - 1) + BinaryTreeKSize(root->rightChild, k - 1);
}
int main()
{
	BTNode* root = CreatBinaryTree();
	int k = 0;
	printf("请输入要求哪一层的节点:");
	scanf("%d", &k);
	printf("第k层节点的个数是:%d\n", BinaryTreeKSize(root, k));
}

运行结果:

 4.2.5二叉树深度/高度

有缺点的代码:

//二叉树的深度/高度
int BinaryTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	return BinaryTreeDepth(root->leftChild) > BinaryTreeDepth(root->rightChild) ? BinaryTreeDepth(root->leftChild) + 1 : BinaryTreeDepth(root->rightChild) + 1;
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//二叉树的深度/高度
	printf("二叉树的深度/高度是:%d\n", BinaryTreeDepth(root));
}

当前树的深度/高度=左子树的深度或右子树的深度大的+1

缺点:调用次数过多,且因没保存要重复计算

改进写法:

int BinaryTreeDepth(BTNode* root)
{
	if (root == NULL)
	{
		return 0;
	}
	int leftDepth = BinaryTreeDepth(root->leftChild);
	int rightDepth = BinaryTreeDepth(root->rightChild);
	return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
int main()
{
	BTNode* root = CreatBinaryTree();
	//二叉树的深度/高度
	printf("二叉树的深度/高度是:%d\n", BinaryTreeDepth(root));
}

思路类似校长调查全校师生人数,不过现在是调查身高最高的那个人是谁。命令被一层一层下达后,最底层的室长把所收取的数据做个比较,把最大的舍友身高数据报了上去。老师收到后把全班的室长传来的数据做个比较,把最大的那一份身高数据传了上去。依次类推,直到最终的校长得到最大的数据

4.2.6查找值为x的节点

看似正确,实则会报错的写法:

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	BinaryTreeFind(root->leftChild, x);
	BinaryTreeFind(root->rightChild, x);
}

报错的原因:虽然这种写法符合执行逻辑,看起来能够正确运行,但是VS有一个特点,它是根据语法来判断是否运行的。在他看来,如果if (root == NULL)和if (root->data == x)没有返回,那么后续就没有返回了,这是不符合它认为的语法的。

改进:

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	if (BinaryTreeFind(root->leftChild, x))
	{
		return BinaryTreeFind(root->leftChild, x);
	}
	if (BinaryTreeFind(root->rightChild, x))
	{
		return BinaryTreeFind(root->rightChild, x);
	}
}

这种写法可以运行,但是效率太低。低在和找高度问题一个道理:我好不容易通过递归千辛万苦找到了对应节点地址,但是呢我没有保存下来它的地址,而是报告说找到了,它存在。然后让return从头到尾再重复找一次

改进:

BTNode* BinaryTreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
	{
		return NULL;
	}
	if (root->data == x)
	{
		return root;
	}
	BTNode* leftRet=BinaryTreeFind(root->leftChild, x);
	if (leftRet)
	{
		return leftRet;
	}
	BTNode* rightRet = BinaryTreeFind(root->rightChild, x);
	if(rightRet)
	{
		return rightRet;
	}
	return NULL;//找完了,没有对应x的节点
}

4.2.7层次/序遍历

层次遍历很好理解,就是从根节点开始,一层一层,从上到下,每层从左到右,依次写值就可以了

层次遍历结果:A B C D E F G H I J K在这里插入图片描述

 解释外圈跑的意思:

绕着外围跑一整圈的真正含义是:遍历所有结点时,都先往左孩子走,再往右孩子走。

实现思路:

 代码实现:

void BinaryTreeLevelOrder(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	While(!QueueEmpty(&q))
	{
		NTNode** front = QueueFront(&q);
		QueuePop(&q);
		printf("%d ", front->data);

		//左孩子放进队列
		if (front->leftChild)
		{
			QueuePush(&q, front->leftChild);
		}
        //右孩子放进队列
		if (front->rightChild)
		{
			QueuePush(&q, front->rightChild);
		}

	}
	printf("\n");
	QueueDestory(&q);
}

int main()
{
    BinaryTreeLevelOrder(root);
}

运行结果:

4.2.8判断二叉树是否是完全二叉树

bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
		{
			break;
		}
		else
		{
			QueuePush(&q, front->leftChild);
			QueuePush(&q, front->rightChild);
		}
	}
	//遇到空了以后,检查队列中剩下的节点
	//1.剩下的全是空,则是完全二叉树
	//2.剩下存在非空,则不是完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);

		if (front)//非空
		{
			return false;
		}
	}
	QueueDestory(&q);
	return true;
}

4.2.9二叉树的销毁

void BinaryTreeDestory(BTNode* root)
{
	if (root == NULL)
	{
		return;
	}
	BinaryTreeDestory(root->leftChild);
	BinaryTreeDestory(root->rightChild);
	free(root);
}

4.3二叉树基础oj练习与C++进阶中的二叉树遍历

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值