【数据结构】二叉树与堆

news2024/11/26 11:50:38

文章目录

    • 1.树概念及结构
      • 1.1树的相关概念
      • 1.2树的结构
    • 2.二叉树概念及结构
      • 2.1相关概念
      • 2.2特殊的二叉树
      • 2.3二叉树的性质
      • 2.4二叉树的存储结构
    • 3.二叉树的顺序结构及实现
      • 3.1二叉树的顺序结构
      • 3.2堆的概念
      • 3.3堆的实现
        • Heap.h
        • Heap.c
      • 3.4堆的应用
        • 3.4.1 堆排序
        • 3.4.2 TOP-K
        • OJ题
          • 最小K个数
    • 4.二叉树链式结构的实现
      • 4.1遍历操作
      • 4.2其他操作
      • 4.3判断完全二叉树
    • 5.总结

1.树概念及结构

1.1树的相关概念

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

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

注意:树形结构中,子树之间不能有交集,否则就不是树形结构

image-20220809181742844

image-20220809181925803

节点的度:个节点含有的子树的个数称为该节点的度; 如上图: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)棵互不相交的树的集合称为森林

概念的知识很多,但是我们还是需要去理解!

1.2树的结构

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

image-20220809182528111

树在实际中的运用(表示文件系统的目录树结构,网上找的图片) :

image-20220809183345009

2.二叉树概念及结构

2.1相关概念

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

1.或者为空

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

image-20220809183538677

  • 二叉树不存在度大于2的结点
  • 二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树

2.2特殊的二叉树

  • 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 ,则它就是满二叉树。
  • 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树

看了概念可能不太好理解,我们来看图对比一下就知道区别在哪里了

image-20220809183826569

2.3二叉树的性质

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

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

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

  • 若规定根节点的层数为1,具有n个结点的满二叉树的深度h=long(n+1) . (ps:是log以2为底,n+1为对数)

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

    1.若i>0i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点

    2.若2i+1<n,左孩子序号:2i+12i+1>=n否则无左孩子

    3.若2i+2<n,右孩子序号:2i+22i+2>=n否则无右孩子

了解完二叉树的性质之后,我们不妨来做几道选择题:

1.某二叉树共有 399 个结点,其中有 199 个度为 2 的结点,则该二叉树中的叶子结点数为( )
A 不存在这样的二叉树
B 200
C 198
D 199

答案:B。n0 = n2+1,既n0=199+1 = 200
2.下列数据结构中,不适合采用顺序存储结构的是( )
A 非完全二叉树
B 堆
C 队列
D 栈

答案:A。

3.在具有 2n 个结点的完全二叉树中,叶子结点个数为( )
A n
B n+1
C n-1
D n/2

答案:A。总结点数n = n0+n1+n2;又n2 = n0-1,既n=n0+n1+n0-1,在完全二叉树中,n1只有0个或者1个,代入选A

4.一棵完全二叉树的节点数位为531个,那么这棵树的高度为( )
A 11
B 10
C 8
D 12

答案:Bimage-20220815151205546

5.一个具有767个节点的完全二叉树,其叶子节点个数为()
A 383
B 384
C 385
D 386

答案:B。类似于第3题,总结点n = n0+n1+n0-1;n1为0或者1,代入能整除的选B

2.4二叉树的存储结构

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

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

image-20220809195550127

  1. 链式存储
    二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分为二叉链和三叉链,当前我们使用的是二叉链

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

3.1二叉树的顺序结构

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结
构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储。

image-20220809200130790

3.2堆的概念

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

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

我们来做一道选择题试一试:

下列关键字序列为堆的是:()
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

解析:这里没有明确说明是大堆还是小堆,我们直接在草稿上简单画一下就知道答案了:

image-20220809214454246

3.3堆的实现

Heap.h

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <time.h>
#include <stdbool.h>

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* a;
	int size;
	int capacity;
}HP;
//初始化堆
void HeapInit(HP* php);
//销毁堆
void HeapDestory(HP* php);
//打印堆
void HeapPrint(HP* php);
//插入X继续保持堆形态
void HeapPush(HP* php,HPDataType x);
//删除堆顶元素
void HeapPop(HP* php);
//判断是否为空
bool HeapEmpty(HP* php);
//返回堆顶元素
HPDataType HeapTop(HP* php);
//返回堆的元素大小
int HeapSize(HP* php);

这就是一些要实现的接口,下面通过Heap.c对以上接口进行说明。

Heap.c

初始化

//初始化
void HeapInit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->size = php->capacity = 0;
}

插入X并保持堆

插入之前是小根堆(假设堆已经创建好,后面会说到堆的创建,不要急,先看这一部分,便于理解,后面堆的创建就简单多了),我们是在尾部把X插入的,然后再进行向上调整(原来必须是大堆或者小堆了)

比如先插入一个10到数组的尾上 ,再进行向上调整算法,直到满足堆

image-20220809203019107

最后一个位置是:php->size-1.要找到父节点:(child-1)/2,如此我们去比较大小进行交换调整,就能够实现我们的向上调整:

//交换
void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
//向上调整(这里是小堆)
void AdjustUp(HPDataType* a,int child)
{
	//父亲结点
	int parent = (child - 1) / 2;

	//终止条件:孩子等于0,大于0就继续调整
    //不要拿父亲作为条件,父亲和孩子都等于0的时候,paretn = (0-1)/2还是0,死循环了
    //while(parent>=0)
	while (child > 0)
	{
		//孩子小于父亲——小堆
        //如果要建大堆的话直接反过来就行了
		if (a[child] < a[parent])
		{
			//交换
			Swap(&a[child], &a[parent]);
			//继续往上迭代
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}
//插入X继续保持堆形态
void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		//进行扩容
		HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * newCapacity);
		if (NULL == tmp)
		{
			perror("malloc fail");
			exit(-1);
		}
		php->a = tmp;
		php->capacity = newCapacity;
	}
	//插入
	php->a[php->size] = x;
	php->size++;
	//向上调整,最后一个位置
	AdjustUp(php->a, php->size - 1);
}

我们不妨来测试一下这段代码:

image-20220809214028865

删除堆顶元素

开始之前,想想删除堆顶元素能干什么❓可以找出次大获知次小

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

image-20220809214606723

//向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
	//最小的默认为左孩子
	int minchild = 2 * parent + 1;
	while (minchild <n)
	{
		//找出小的那个孩子
		if (minchild+1<n&&a[minchild + 1] < a[minchild])
		{
			minchild++;
		}
		if (a[minchild] < a[parent])
		{
			Swap(&a[minchild], &a[parent]);
			parent = minchild;
			minchild = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}
//删除堆顶元素——找出次大或者次小
void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	Swap(&php->a[0], &php->a[php->size - 1]);

	php->size--;
	AdjustDown(php->a, php->size, 0);

}

判断是否为空

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

取堆顶元素

//返回堆顶元素
HPDataType HeapTop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	return php->a[0];
}

元素个数

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

打印

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

销毁

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

3.4堆的应用

3.4.1 堆排序

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

  1. 建堆
    升序:建大堆(如果建小堆的话,小堆不一定有序,只是堆顶最小,那下一个最小的数呢?如何依次选最小的数据?就算是选出来了之后,结点之间的关系就全乱了,无法继续利用优势,只能重新建堆O(N),选出次小的数据,效率太低了)建大堆直接交换堆顶和最后一个,进行向下调整即可

    降序:建小堆

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

    建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。回顾一下向下调整的过程:image-20220809232058329

这里的建堆,是直接用数组进行建堆,可以用向上调整算法进行建堆,也可以利用向下调整建堆,有什么区别?我们先来看代码的实现:

//建堆——a,利用向上调整建堆——O(N*logN)
	//直接插入
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}*/
	//建完堆之后无序,我们排个序


	//建堆——向下调整建堆(从最后一个节点的父亲开始,开始向下调整,直到根)——O(N)
	for (int i = (n-1-1)/2; i>=0; i--)
	{
		AdjustDown(a, n, i);
	}

两种的时间复杂度不一样:向上调整建堆的时间复杂度是O(N*logN);向下调整建堆的时间复杂度是O(N)。所以我们选用的是向下调整算法进行建堆,关于向下调整算法时间复杂度的推导过程:

image-20220809233233322

因此:建堆的时间复杂度为O(N)。

关于向上调整算法时间复杂度的推导过程(注意红色部分,这就是这两种的区别):

image-20220809234931329

了解完以后,我们来实现堆排序:

//O(N*longN)
//向下调整
void AdjustDown(HPDataType* a, int n, int parent)
{
	//最小的默认为左孩子
	int minchild = 2 * parent + 1;
	while (minchild <n)
	{
		//找出小的那个孩子
		if (minchild+1<n&&a[minchild+1] < a[minchild])
		{
			minchild++;
		}
        //小堆
		if (a[minchild] < a[parent])
		{
			Swap(&a[minchild], &a[parent]);
			parent = minchild;
			minchild = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

void HeapSort(int* a, int n)
{
	//建堆——a,利用向上调整建堆
	//直接插入
	/*for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}*/
	//建完堆之后无序,我们排个序


	//建堆——向下调整建堆(从最后一个节点的父亲开始,开始向下调整,直到根)
	for (int i = (n-1-1)/2; i>=0; i--)
	{
		AdjustDown(a, n, i);
	}
	//选数
	int i = 1;
	while (i < n)
	{
		Swap(&a[0], &a[n - i]);
		AdjustDown(a, n - i, 0);
		i++;
	}

}


int main()
{
	int a[] = { 15,1,19,25,8,34,65,4,27,7 };
	HeapSort(a, sizeof(a) / sizeof(int));

	for (int i = 0; i < sizeof(a) / sizeof(int); i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
	return 0;
}

image-20220810131240864

这里建的是小堆自然是降序。

3.4.2 TOP-K

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般情况下数据量都比较大 有人说:简单啊,排个序(NlogN),选出前K个即可。但是:如果数据量非常大,排序就不太可取了 (可能数据都不能一下子全部加载到内存中 )

最佳的方式就是用堆来解决:

  1. 用数据集合中前K个元素来建堆

    前k个最大的元素,则建小堆(建大堆呢,有什么区别)

    前k个最小的元素,则建大堆

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

    将剩余N-K个元素依次与堆顶元素比完之后,堆中剩余的K个元素就是所求的前K个最小或者最大的元素。

image-20220810140711442

代码实现(这里采用前面学的文件的一些函数):

//创建一个文件,并且随机生成一些数字
void CreateDataFile(const char* filename, int N)
{
	FILE* Fin = fopen(filename, "w");
	if (Fin == NULL)
	{
		perror("fopen fail");
		exit(-1);
	}
	srand(time(0));
	for (int i = 0; i < N; i++)
	{
		fprintf(Fin, "%d ", rand()%10000);
	}
}

//建堆,选前K个最大的数并且打印
void PrintTopK(const char* filename, int k)
{
	assert(filename);
	FILE* fout = fopen(filename, "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		exit(-1);
	}
	//小堆
	int* minHeap = (int*)malloc(sizeof(int) * k);
	if (minHeap == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}
	//读前K个元素
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &minHeap[i]);
	}

	//建k个数的堆
	for (int j = (k - 1 - 1) / 2; j >= 0; j--)
	{
		AdjustDown(minHeap, k, j);
	}


	//读取后N-k个
	int val = 0;
	while (fscanf(fout,"%d",&val) != EOF)
	{
		if (val > minHeap[0])
		{
			minHeap[0] = val;
			AdjustDown(minHeap, k, 0);
		}
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", minHeap[i]);
	}
	printf("\n");

	free(minHeap);

	fclose(fout);
}

int main()
{
	int N = 10000;
	const char* filename = "Data.txt";
	int k = 10;
	CreateDataFile(filename, N);
	PrintTopK(filename, k);
	return 0;
}

image-20220810145303781

随机生成10000以内的数字,至此,TOP-K代码的实现到这里结束。下面,我们可以来做一道OJ题目:

OJ题

最小K个数

点我

image-20220810155320284

一看就是上面说的TOP-k问题,直接上手代码

void Swap(int*p1,int*p2)
 {
     int*tmp =*p1;
     *p1 = *p2;
     *p2 = tmp;
 }
 void AdjustDown(int*a,int n,int parent)
 {
     int minchild = parent*2+1;
     while(minchild<n)
     {
         if(minchild+1<n&&a[minchild+1]>a[minchild])
         {
             minchild++;
         }
         if(a[minchild]>a[parent])
         {
             Swap(&a[minchild],&a[parent]);
             parent = minchild;
             minchild = parent*2+1;
         }
         else
         {
             break;
         }
     }
 }
int* smallestK(int* arr, int arrSize, int k, int* returnSize){
    if(k==0)
    {
        *returnSize = 0;
        return NULL;
    }
    int *minHeap = (int*)malloc(sizeof(int)*(k));
    for(int i = 0;i<k;i++)
    {
        minHeap[i] = arr[i];
    }
    //前K个数建大堆
    for(int j = (k-1-1)/2;j>=0;j--)
    {
        AdjustDown(minHeap,k,j);
    }
    //后N-K个数进入
    for(int i = k;i<arrSize;i++)
    {
        if(arr[i]<minHeap[0])
        {
            minHeap[0] = arr[i];
            AdjustDown(minHeap,k,0);
        }
    }
    *returnSize = k;
    return minHeap;
}

image-20220810155403330

4.二叉树链式结构的实现

在学习二叉树的基本操作前,需先要创建一棵二叉树,然后才能学习其相关的基本操作。为了降低学习成本,此处手动快速创建一棵简单的二叉树,这里以简单的实现为主。后续在来讨论其创建方式

以这颗树为例子:image-20220810163832470

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

typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;


BTNode* CreatTree()
{
	BTNode* n1 = (BTNode*)malloc(sizeof(BTNode));
	assert(n1);
	BTNode* n2 = (BTNode*)malloc(sizeof(BTNode));
	assert(n2);
	BTNode* n3 = (BTNode*)malloc(sizeof(BTNode));
	assert(n3);
	BTNode* n4 = (BTNode*)malloc(sizeof(BTNode));
	assert(n4);
	BTNode* n5 = (BTNode*)malloc(sizeof(BTNode));
	assert(n5);
	BTNode* n6 = (BTNode*)malloc(sizeof(BTNode));
	assert(n6);

	n1->data = 1;
	n2->data = 2;
	n3->data = 3;
	n4->data = 4;
	n5->data = 5;
	n6->data = 6;

	n1->left = n2;
	n1->right = n4;
	n2->left = n3;
	n2->right = NULL;
	n3->left = NULL;
	n3->right = NULL;
	n4->left = n5;
	n4->right = n6;
	n5->left = NULL;
	n5->right = NULL;
	n6->left = NULL;
	n6->right = NULL;

	return n1;
}

这就大概搭建起了二叉树的框架,下面,来说一说其操作:

4.1遍历操作

先序、中序、后序遍历递归操作:

//先序
void PreOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	printf("%d ", root->data);
	PreOrder(root->left);
	PreOrder(root->right);
}

//中序
void InOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	InOrder(root->left);
	printf("%d ", root->data);
	InOrder(root->right);
}

//后序
void PostOrder(BTNode* root)
{
	if (root == NULL)
	{
		printf("NULL ");
		return;
	}
	PostOrder(root->left);
	PostOrder(root->right);
	printf("%d ", root->data);
}

我们不难发现,二叉树的遍历操作通过递归实现非常地简单,实现并不难,但是你真的理解了吗❓函数怎么执行的(我们这里以前序遍历为例)

前序遍历递归图解:

image-20220810165526334

image-20220810165556180

除了这三种遍历的方式,二叉树还有层序遍历

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

如何实现层序遍历

我们可以借用一个队列,把结点代入队列, 不为空出队列,在把孩子带入队列,在出队列,在把孩子代入,如此往复,直到结点全部出队列,即队列为空层序遍历结束。简单来说:就是上一层结点出的时候带入下一层结点。这里用C语言实现:所以我们首先是要去手写一个队列。

//二叉树
typedef int BTDataType;
typedef struct BinaryTreeNode
{
	BTDataType data;
	struct BinaryTreeNode* left;
	struct BinaryTreeNode* right;
}BTNode;

//队列(用链表实现,data的类型是BTNode*)
typedef BTNode* QDataType;
typedef struct QueueNode
{
	struct QueueNode* next;
	QDataType data;
}QNode;

typedef struct Queue
{
	QNode* head;
	QNode* tail;
}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 QueueInit(Queue* pq)
{
	assert(pq);
	pq->head = pq->tail = NULL;
}

void QueueDestroy(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->head;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);

		cur = next;
	}

	pq->head = pq->tail = NULL;
}

void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;

	if (pq->tail == NULL)
	{
		pq->head = pq->tail = newnode;
	}
	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}
}
void QueuePop(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	if (pq->head->next == NULL)
	{
		free(pq->head);
		pq->head = pq->tail = NULL;
	}
	else
	{
		QNode* next = pq->head->next;
		free(pq->head);
		pq->head = next;
	}
}

QDataType QueueFront(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->head->data;
}

QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));

	return pq->tail->data;
}

bool QueueEmpty(Queue* pq)
{
	assert(pq);
	return pq->head == NULL;
}

int QueueSize(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->head;
	int size = 0;
	while (cur)
	{
		++size;
		cur = cur->next;
	}

	return size;
}

//层序遍历
void TreeLevelOrder(BTNode*root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		printf("%d ",front->data);

		//下一层
		if (front->left)
			QueuePush(&q, front->left);
		if (front->right)
			QueuePush(&q, front->right);

	}
	printf("\n");

	QueueDestroy(&q);
}

我们不妨来测试上面的层序遍历:

image-20220815233953354

DFS 深度优先遍历——前序

BFS广度优先遍历——层序

这里有一个小问题:根据前序和中序(中序和后序也可以,一定要有中序)重建一棵树:

image-20220816102758151

4.2其他操作

结点个数

int TreeSize(BTNode* root)
{
	if (root == NULL)
		return;
	return TreeSize(root->left) +
		TreeSize(root->right) + 1;
}

叶结点个数

//叶子结点个数
int TreeLeafSize(BTNode* root)
{
	if (root == NULL)
		return 0;
	if (root->left == NULL && root->right == NULL)
		return 1;
	return TreeLeafSize(root->left) + TreeLeafSize(root->right);
}

高度

//高度
int TreeHeight(BTNode* root)
{
	if (root == NULL)
		return 0;
	int left = TreeHeight(root->left);
	int right = TreeHeight(root->right);
	return left > right ? left + 1 : right + 1;
}

求第K层结点个数

//求第K层结点个数
int TreeKLevel(BTNode* root, int k)
{
	assert(k > 0);
	if (root == NULL)
		return 0;
	if (k == 1)
		return 1;
	return TreeKLevel(root->left, k - 1) +
		TreeKLevel(root->right, k - 1);
}

返回x所在的结点

//返回X所在的结点
BTNode* TreeFind(BTNode* root, BTDataType x)
{
	if (root == NULL)
		return NULL;
	if (root->data == x)
		return root;
	//先去左树找
	BTNode* left = TreeFind(root->left, x);
	if (left != NULL)
		return left;
	//左树找不到,在去右树找
	BTNode* right = TreeFind(root->right, x);
	if (right != NULL)
		return right;
	return NULL;
}

销毁

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

通过主函数来调用测试上面的几个函数接口:

int main()
{
	BTNode* root = CreatTree();
	PreOrder(root);
	printf("\n");

	InOrder(root);
	printf("\n");

	PostOrder(root);
	printf("\n");

	printf("Size:%d\n", TreeSize(root));

	printf("LeafSize:%d\n", TreeLeafSize(root));

	printf("TreeHeight:%d\n", TreeHeight(root));

	printf("TreeKLevel:%d\n", TreeKLevel(root,1));

	printf("Tree Find:%p\n", TreeFind(root, 6));
    
    BinaryTreeDestory(root);
    root=NULL;
	return 0;
}

image-20220810184945477

4.3判断完全二叉树

我们该怎么去判断是否为完全二叉树呢❓这就可以用到我们上面说的层序遍历了:

我们一层一层地走,对于完全二叉树来说,一层一层地走,遇到空以后,后面就不会有非空了(因为完全二叉树是从左到右依次连续的),有非空的话那就不是完全二叉树了。

//判断是否为完全二叉树
bool BinaryTreeComplete(BTNode* root)
{
	Queue q;
	QueueInit(&q);
	if (root)
		QueuePush(&q, root);
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front == NULL)
		{
			break;
		}
		QueuePush(&q, front->left);
		QueuePush(&q, front->right);
	}
	//遇到空以后,后面全是空——完全二叉树
	while (!QueueEmpty(&q))
	{
		BTNode* front = QueueFront(&q);
		QueuePop(&q);
		if (front != NULL)
		{
			QueueDestory(&q);
			return false;
		}
	}
	QueueDestroy(&q);
	return true;
}

5.总结

我们从树的概念开始,了解了树的相关知识、以及引出了二叉树的概念,了解二叉树的一些性质,通过二叉树的顺序存储,我们又认识了堆,堆的主要算法是向上调整算法以及向下调整算法,以及说了堆的应用,堆排序以及TOP-K问题,以及根据TOP-K解决实际的OJ题。最后还说了二叉树的遍历操作以及一些函数接口,但是在这个地方,我们并没有说得那么详细,对于二叉树的了解还是比较基础的,还存在许多的内容我们没有展开出来。后面会把这一部分内容补充完整。同时,对于二叉树有了一定的了解之后,我们可以去多做一些OJ题目了,没事就去多刷刷题目。这次博客就先到这里结束了。🌹

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

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

相关文章

ESXI主机安装Zabbix 6.2

1&#xff1a;首先下载Zabbix Appliance 2&#xff1a;还需要下载VMware converter &#xff0c;这个需要VMware的账号&#xff0c;或者从其他地方下载也可以。 3:vmdk格式的 image 可直接在 VMware Player、 Server 和 Workstation 产品中使用。要在 ESX,、ESXi 和 vSphere 中…

下一站,冠军|走进2022 OceanBase数据库大赛12强

欢迎访问OceanBase官网获取更多信息&#xff1a;https://www.oceanbase.com/ 还记得 2021 OceanBase 数据库大赛中 令人羡慕的神雕侠侣 “NoPassCET4”吗&#xff1f; 打比赛&#xff0c;也打蚊子的“两只老虎吃萝卜”吗&#xff1f; 一个人扛起一支队伍的 “lying_flat”吗…

小孩用的台灯什么样的品牌好?2023儿童台灯灯具品牌排行榜

现代社会高速发展&#xff0c;孩子小小年纪就使用上了各种电子产品&#xff0c;加上学习压力较大&#xff0c;近视问题也普遍比较严重。据统计&#xff0c;我国青少年儿童近视率常年维持在50%以上&#xff0c;半数以上的孩子都患有近视&#xff0c;高居世界第一&#xff0c;情况…

c---冒泡排序模拟qsort

一、冒泡排序 二、冒泡排序优化排各种类型数据 文章目录一、冒泡排序二、冒泡排序优化排各种类型数据冒泡排序 冒泡排序原理&#xff1a;两两相邻元素进行比较 初级版 void bulle_sort(int* a, int sz) {int i 0;for (int i 0; i < sz-1; i){int j 0; for (j 0; j…

阿里大佬翻遍全网Java面试文章,总结出这份1658页文档,GitHub收获25K+点赞

就目前大环境来看&#xff0c;跳槽成功的难度比往年高很多。一个明显的感受&#xff1a;今年的面试&#xff0c;无论一面还是二面&#xff0c;都很考验Java程序员的技术功底。这不又到了面试跳槽的黄金段&#xff0c;成功升职加薪&#xff0c;不成功饱受打击。当然也要注意&…

使用日历丰富产品的用户体验

前言 经过一段时间的梳理和遴选&#xff0c;我挑选出了Android知识图谱中重要的部分&#xff0c;制作了一张脑图。读者朋友们可按照脑图查漏补缺了&#xff0c; 图片尺寸较大&#xff0c;仅附链接 。 当然&#xff0c;这是我按照自己的判断、结合参考其他博主的观点进行的挑选…

sort函数对结构体|pair对组|vector容器|map排序|二维数组的第x列 的排序

目录 sort对 vector容器 sort对 vector<pair<int,int>>对组 sort对 结构体 结构体外部规定排序 结构体内部运算符重载 map容器的排序 map的键排序 map的值排序 sort对二维数组的排序 sort对 vector容器 sort()函数可以用于对vector容器进行排序。具体来…

java基础学习 day49(JDK8的接口新特性,JDK9的新特性,接口的应用场景)

JDK8以后&#xff0c;接口中新增的默认方法 允许在接口中定义默认方法&#xff0c;需要使用关键字default修饰&#xff0c;作用为&#xff0c;解决接口升级时&#xff0c;需要强制修改所有实现类的问题接口中默认方法的定义格式&#xff1a; a. 格式&#xff1a; public defau…

【黄河流域公安院校网络空间安全技能挑战赛】部分wp

文章目录webbabyPHPfunnyPHPEzphp**遍历文件目录的类**1、DirectoryIterator&#xff1a;2、FilesystemIterator:3、**Globlterator**读取文件内容的类&#xff1a;SplFileObjectMisc套娃web babyPHP <?php highlight_file(__FILE__); error_reporting(0);$num $_GET[nu…

排序模型:DIN、DINE、DSIN

目录 DIN 输入 输出&#xff1a; 与transformer注意力机制的区别与联系&#xff1a; DINE 改善DIN 输入&#xff1a; DSIN 动机&#xff1a; DIN 适用与精排&#xff0c;论文&#xff1a; Deep Interest Network for Click-Through Rate Prediction DIN模型提出的动…

蓝桥web基础知识学习

HTMLCSS 知识点重要指数HTML 基础标签&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;HTML5 新特性&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;HTML5 本地存储&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;CSS 基础语法…

Java中方法引用(引用静态方法、引用成员方法(引用其他类的成员方法、引用本类的成员方法、引用父类的成员方法)、引用构造方法、其他调用方式、小练习)

方法引用&#xff1a;把已经存在的方法拿过来用&#xff0c;当作函数式接口中抽象方法的方法体 我们前面学到Arrays工具类中的sort方法&#xff0c;当我们需要指定排序规则时&#xff0c;需要传递Comparator接口的实现类对象&#xff0c;我们之前使用匿名内部类类的形式作为参…

下一个元宇宙热点?探讨元宇宙婚礼的未来趋势

欢迎来到Hubbleverse &#x1f30d; 关注我们 关注宇宙新鲜事 &#x1f4cc; 预计阅读时长&#xff1a;7分钟 本文仅代表作者个人观点&#xff0c;不代表平台意见&#xff0c;不构成投资建议。 专家认为&#xff0c;在不久的将来&#xff0c;传统婚礼和元宇宙婚礼有可能共存…

taobao.item.barcode.update( 更新商品条形码信息 )

&#xffe5;免费必须用户授权 通过该接口&#xff0c;将商品以及SKU上得条形码信息补全 公共参数 请求地址: HTTP地址 http://gw.api.taobao.com/router/rest 公共请求参数: 公共响应参数: 请求参数 响应参数 点击获取key和secret 请求示例 TaobaoClient client new Def…

【Spring】资源操作管理:Resource、ResourceLoader、ResourceLoaderAware;

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 资源操作&#xff1a;Spring Resources一、Res…

智能家居创意产品一Homkit智能通断器

智能通断器&#xff0c;也叫开关模块&#xff0c;可以非常方便地接入家中原有开关、插座、灯具、电器的线路中&#xff0c;通过手机App或者语音即可控制电路通断&#xff0c;轻松实现原有家居设备的智能化改造。 随着智能家居概念的普及&#xff0c;越来越多的人想将自己的家改…

Pytest自动化测试框架-权威教程06-使用Marks标记测试用例

使用Marks标记测试用例通过使用pytest.mark你可以轻松地在测试用例上设置元数据。例如, 一些常用的内置标记&#xff1a;skip - 始终跳过该测试用例skipif - 遇到特定情况跳过该测试用例xfail - 遇到特定情况,产生一个“期望失败”输出parametrize - 在同一个测试用例上运行多次…

自旋锁,读写锁以及他们的异同

自旋锁 自旋锁是一种用于多线程编程的同步机制。它是一种基于忙等待的锁&#xff0c;当线程尝试获取锁时&#xff0c;如果锁已被其他线程占用&#xff0c;则该线程会一直循环检查锁是否被释放&#xff0c;直到获取到锁为止。 在自旋锁的实现中&#xff0c;使用了CPU的硬件特性…

ArcGIS制图之阴影效果的表达与运用

一、运用制图表达进行投影表达 在专题图的制作过程中&#xff0c;经常需要将目标区域从底图中进行突显&#xff0c;运用制图表达制作图层投影可以较好地实现这一目的。具体步骤如下&#xff1a; 1.将目标图层存储于数据库中并加载至窗口&#xff08;shapefile格式数据无法支持…

Android Looper简介

本文基于安卓11。 Looper是一个用具&#xff0c;在安卓程序中&#xff0c;UI线程是由事件驱动的&#xff08;onCreate, onResume, onDestory&#xff09;&#xff0c;Looper就是处理这些事件的工具&#xff0c;事件被描述为消息&#xff08;Message&#xff09;&#xff0c;Lo…