二叉树初阶和堆的详解

        前言:二叉树是一种基础数据结构,它由节点和边构成,其中每个节点最多只有两个子节点,称为左子节点和右子节点。二叉树具有许多应用,例如搜索算法和排序算法,还可以用于创建堆等高级数据结构。

       堆是一种基于完全二叉树的特殊数据结构,具有一些有用的性质。例如,对于最小堆,每个父节点的键值都小于或等于其子节点的键值。这个性质使得堆可以用于优先队列的实现,其中某些元素始终具有更高的优先级,并在任何时候可以立即获得。

       本文将介绍二叉树的基础知识、不同类型的二叉树以及它们的用途,重点就是介绍堆(类似于完全二叉树)的一种二叉树型结构的实现及用途。

目录

 1.树的相关概念

 2.二叉树

2.1 二叉树的概念

2.1.1 特殊的二叉树

 2.2 二叉树的相关性质

2.3 二叉树的存储 

 1. 顺序存储

 2.链式存储

2.4 二叉树的顺序结构及其实现

2.4.1 堆的概念

2.4.2 堆的实现(分大根堆和小根堆,注意区别,这里仅适用于完全二叉树)

         逻辑结构的创建和初始化

         堆数据的插入和向上调整算法

         堆顶(根节点)数据的删除和向下调整算法

         完整代码:

                Heap.h

                Heap.cpp

2.4.3 堆的实际应用 

         1.堆排序

                   第一个问题,我们要建一个大堆还是小堆呢?

                   利用数组建堆有几种方式?

                   那么堆是如何实现排序的呢?

                   堆排序函数实现: 

         2.TOP-K问题

                   首先,建大堆还是建小堆?

                    函数实现(以前k个数为例):

          2.5 完整代码及测试代码(堆的代码还请参考上面给出的即可,这里为了适应应用,已经修改了些许)

         Heap.h

         Heap.cpp

         Test.cpp(堆排序和TOP-k问题函数)

         2.6 堆总结   


 1.树的相关概念

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

1.有一个特殊的结点,称为根结点,根节点没有前驱结点

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

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

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

2.二叉树

2.1 二叉树的概念

       二叉树是一种由节点和边组成的树状数据结构,其中每个节点最多有两个子节点,分别称为左子节点和右子节点。二叉树中每个节点都必须满足以下条件:

1. 每个节点最多只有两个子节点,分别称为左子节点和右子节点;
2. 左子节点和右子节点的顺序不能颠倒
3. 每个节点至多有一个父节点,除了根节点没有父节点;
4. 一个树节点的子树也是二叉树。

2.1.1 特殊的二叉树

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

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

 2.2 二叉树的相关性质

2.3 二叉树的存储 

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

1. 顺序存储

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

 2.链式存储

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

这种就是我们经常使用的,用结构体成员来实现,具体的我们后续会详细展开。

2.4 二叉树的顺序结构及其实现

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

2.4.1 堆的概念

 

        我们可以看出,不论是小根堆还是大根堆,本质上是用数组来实现的,并且,每一个节点所在的路径都满足从大到小或从小到大的顺序,所以,我们在创建这个数组时就需要考虑对新插入的值进行调整,使之符合大小根堆的定义,比如,举个例子,就拿上面的小根堆的逻辑结构来说:

      还有一点需要强调:这时的大根堆和小根堆并不具有有序性,我们所了解到的堆排序,也是需要在建好堆的情况下进行优化才能达到排序效果,我们从定义中也可以看出来,这里只是每一个子树或者说每一条路径上的相对有序,至于相同层次的兄弟节点,并不一定具有有序性。

2.4.2 堆的实现(分大根堆和小根堆,注意区别,这里仅适用于完全二叉树)

       之所以实现以完全二叉树为前提条件的堆,是因为完全二叉树可以让数组的每一个空间都值,从而避免空间的浪费和减少一些对某个位置有没有有效值的判断。

逻辑结构的创建和初始化

      我们堆的本质,实际上就是用数组结构利用二叉树父子节点之间的逻辑关系(即父节点下标*2+1=左孩子下标,父节点下标*2+2=右孩子下标)来达到表示二叉树的逻辑结构的目的,所以,我们选择的数据结构最好是能够动态开辟的顺序表,加上容量和当前保存的位置,组合成为结构体变量即可;

#pragma once
//大根堆和小根堆的数组数组实现
#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* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
}

堆数据的插入和向上调整算法

      在我们已经构建好的堆中,每一个数据的插入都有可能破坏这个堆的规则,所以我们必须要在每一个数据插入数组以后,对数组内的数据进行调整,使之重新符合大堆或小堆的规则,这个过程我们就称之为向上调整算法。

       数据的插入我们可以直接在数组末尾下标直接插入即可,而对于向上调整算法,由于我们在插入数据前已经是一个堆结构了,所以,新插入的数据只会影响其所在的二叉树分支,我们只需要利用二叉树的父子节点在数组中存储对应的下标关系,来找到所插入元素的祖先节点,向上循环查找直到查找到根节点,对其所在的分支进行调整即可。

void Adjustup(HP* php, int childidx)//我们可以通过下标找到其父节点等祖先节点
{
	int parent = (childidx - 1) / 2;
	
	while (childidx >= 0)
	{
		//1.实现小根堆
		//if (php->a[parent] > php->a[childidx])
		//{
		//	//交换变量的值
		//	HPDataType temp = php->a[parent];
		//	php->a[parent] = php->a[childidx];
		//	php->a[childidx] = temp;

		//	//更新节点从而能继续向上比较
		//	childidx = parent;
		//	parent = (childidx - 1) / 2;
		//}
		//2.实现大根堆
		if (php->a[parent] < php->a[childidx])
		{
			//交换变量的值
			HPDataType temp = php->a[parent];
			php->a[parent] = php->a[childidx];
			php->a[childidx] = temp;

			//更新节点从而能继续向上比较
			childidx = parent;
			parent = (childidx + 1) / 2;
		}
		else
			break;//不论是小根堆还是大根堆,在我们插入一个数据以前都是符合要求的,如果第一轮判断就合法,那么就其祖先节点就不会再有非法节点出现

	}

}


//堆的插入
void Heappush(HP* php, HPDataType x)
{
	if (php->size == php->capacity)//如果满了就扩容
	{
		int newcapacity = 2 * php->capacity + 4;
		php->a = (HPDataType*)realloc(php->a,sizeof(HPDataType) * (newcapacity));//防止判断0的情况不分配空间
		if (!php->a)
		{
			perror("分配失败\n");
			return;
		}
		php->capacity = newcapacity;
		
	}
	php->a[php->size++] = x;
	//向上调整函数
	Adjustup(php, php->size-1);
}

堆顶(根节点)数据的删除和向下调整算法

       首先,思考一个问题,我们知道,一旦删除了根节点,就会导致整棵二叉树不再符合堆结构,那么我们要怎样才能再删除数据后,重新恢复堆结构呢

       法1.新建临时数组,将原数组保存,删除数据后,再将原临时数组通过插入的方式重新建一个新的堆;

       法2.将根节点与数组末尾的节点交换值的大小,然后删除尾部数据,再重新调整原来的结构,使之再次成为堆结构;

       选哪一个显而易见了吧?法1新建临时数组,将会极大地拖延程序的进行,每次删除堆顶元素,都会额外伴随着一个复杂度O(n)的时间消耗,随着堆规模的扩大,其影响也会越来越大。

下面,我们开看怎么实现法2:

在堆顶与堆尾交换值后,我们现在已知下列条件:

1.除堆顶元素外,其他的子树结构还是都满足大堆或者小堆的结构的

2.当大堆时,堆顶元素最大,当堆顶元素比其左右孩子中的最大值小;或者,当为小堆时,堆顶元素最小,当堆顶元素比其左右孩子中的最小值大;这两种情况都是需要将堆顶元素与相对应的孩子节点进行交换的。其中,左右孩子选择最大或者最小的目的,是为了尽可能的让上面的节点大(大堆)或者小(小堆),以减少交换次数;

       所以,我们可以从堆顶元素开始,将其与其左右孩子中符合最优比较条件(即是大堆就选择最大的,小堆就选择最小的)进行比较,对符合条件的进行交换,再将被交换的孩子节点当做新的父节点,进行比较,直到到达数组末尾边界即可;

为了更好的理解,我们还是来画个图吧:

void Adjustdown(HP* php, int parentidx)
{
	int child = 2 * parentidx + 1;
	while (child<php->size)
	{
		//1、小堆(即数字小的越在上面)
		//我们需要找到左右孩子中较小的那个来和父节点交换
		//if (child + 1 < php->size && php->a[child + 1] < php->a[child])//此时我们需要将父节点和右孩子交换
		//	++child;
		//if (php->a[parentidx] > php->a[child])
		//{
		//	HPDataType temp = php->a[parentidx];
		//	php->a[parentidx] = php->a[child];
		//	php->a[child] = temp;

		//	//对当前选择的路径继续进行调整
		//	parentidx = child;
		//	child = 2 * parentidx + 1;
		//}
		//2.大堆
		if (child + 1 < php->size && php->a[child + 1] > php->a[child])
			++child;
		if (php->a[parentidx] < php->a[child])
		{
			HPDataType temp = php->a[parentidx];
			php->a[parentidx] = php->a[child];
			php->a[child] = temp;

			//对当前选择的路径继续进行调整
			parentidx = child;
			child = 2 * parentidx + 1;
		}
		else //如果越界或者左右孩子都比父节点大(小堆),或者都比父节点小(大堆),因为原来的结构本来就是堆结构,根节点符合所选择的最值路径而满足堆结构之后,整个树就已经满足了堆要求
			break;
	}
}

//删除堆顶元素(二叉树的根节点)首尾节点交换法删除
void Heappop(HP* php)
{
	assert(php);
	assert(!Heapempty(php));
	//交换首尾元素
	HPDataType temp = php->a[0];
	php->a[0] = php->a[php->size - 1];
	php->a[php->size - 1] = temp;
	//删除尾结点
	php->size--;

	//调用调整算法使逻辑结构重新调整为大堆或者小堆,注意我们交换完首尾之后,根的左右子树需仍然保持堆结构,我们才能使用向下调整算法来再次调为堆
	Adjustdown(php,0);
}

完整代码:

Heap.h

#pragma once
//大根堆和小根堆的数组数组实现
#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* php);

//堆的销毁
void Heapdestory(HP* php);

//堆的插入
void Heappush(HP* php, HPDataType x);

//删除堆顶元素(二叉树的根节点)
void Heappop(HP* php);

//获取堆顶元素
HPDataType Heaptop(HP* php);

//判断堆是否为空
bool Heapempty(HP* php);

Heap.cpp

#include "Heap.h"

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

void Adjustup(HP* php, int childidx)//我们可以通过下标找到其父节点等祖先节点
{
	int parent = (childidx - 1) / 2;
	
	while (childidx >= 0)
	{
		//1.实现小根堆
		//if (php->a[parent] > php->a[childidx])
		//{
		//	//交换变量的值
		//	HPDataType temp = php->a[parent];
		//	php->a[parent] = php->a[childidx];
		//	php->a[childidx] = temp;

		//	//更新节点从而能继续向上比较
		//	childidx = parent;
		//	parent = (childidx - 1) / 2;
		//}
		//2.实现大根堆
		if (php->a[parent] < php->a[childidx])
		{
			//交换变量的值
			HPDataType temp = php->a[parent];
			php->a[parent] = php->a[childidx];
			php->a[childidx] = temp;

			//更新节点从而能继续向上比较
			childidx = parent;
			parent = (childidx + 1) / 2;
		}
		else
			break;//不论是小根堆还是大根堆,在我们插入一个数据以前都是符合要求的,如果第一轮判断就合法,那么就其祖先节点就不会再有非法节点出现

	}

}
void Adjustdown(HP* php, int parentidx)
{
	int child = 2 * parentidx + 1;
	while (child<php->size)
	{
		//1、小堆(即数字小的越在上面)
		//我们需要找到左右孩子中较小的那个来和父节点交换
		//if (child + 1 < php->size && php->a[child + 1] < php->a[child])//此时我们需要将父节点和右孩子交换
		//	++child;
		//if (php->a[parentidx] > php->a[child])
		//{
		//	HPDataType temp = php->a[parentidx];
		//	php->a[parentidx] = php->a[child];
		//	php->a[child] = temp;

		//	//对当前选择的路径继续进行调整
		//	parentidx = child;
		//	child = 2 * parentidx + 1;
		//}
		//2.大堆
		if (child + 1 < php->size && php->a[child + 1] > php->a[child])
			++child;
		if (php->a[parentidx] < php->a[child])
		{
			HPDataType temp = php->a[parentidx];
			php->a[parentidx] = php->a[child];
			php->a[child] = temp;

			//对当前选择的路径继续进行调整
			parentidx = child;
			child = 2 * parentidx + 1;
		}
		else //如果越界或者左右孩子都比父节点大(小堆),或者都比父节点小(大堆),因为原来的结构本来就是堆结构,根节点符合所选择的最值路径而满足堆结构之后,整个树就已经满足了堆要求
			break;
	}
}
//堆的销毁
void Heapdestory(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

//堆的插入
void Heappush(HP* php, HPDataType x)
{
	if (php->size == php->capacity)//如果满了就扩容
	{
		int newcapacity = 2 * php->capacity + 4;
		php->a = (HPDataType*)realloc(php->a,sizeof(HPDataType) * (newcapacity));//防止判断0的情况不分配空间
		if (!php->a)
		{
			perror("分配失败\n");
			return;
		}
		php->capacity = newcapacity;
		
	}
	php->a[php->size++] = x;
	//向上调整函数
	Adjustup(php, php->size-1);
}

//删除堆顶元素(二叉树的根节点)首尾节点交换法删除
void Heappop(HP* php)
{
	assert(php);
	assert(!Heapempty(php));
	//交换首尾元素
	HPDataType temp = php->a[0];
	php->a[0] = php->a[php->size - 1];
	php->a[php->size - 1] = temp;
	//删除尾结点
	php->size--;

	//调用调整算法使逻辑结构重新调整为大堆或者小堆,注意我们交换完首尾之后,根的左右子树需仍然保持堆结构,我们才能使用向下调整算法来再次调为堆
	Adjustdown(php,0);
}

//获取堆顶元素
HPDataType Heaptop(HP* php)
{
	assert(php);
	return php->a[0];
}

//判断堆是否为空
bool Heapempty(HP* php)
{
	assert(php);
	return php->size == 0;
}

2.4.3 堆的实际应用 

1.堆排序

第一个问题,我们要建一个大堆还是小堆呢?

       假设我们现在需要升序序列,而我们现在建了一个大堆(大的元素在上面),我们要求升序序列,就需要每次找到最小的元素,但是由于堆的相同层次的各个兄弟节点之间的大小关系是不确定的,所以直接在堆末尾取元素显然是不正确的,那如果我们初始就建一个小堆,每次取最小的元素,就像上面提到的向上调整算法一样,每次将最小的元素,也就是堆顶元素与尾部元素交换然后输出,再删除该元素,重复此过程即可,也即,我们通过建立一个小堆来实现数组的升序排序,类似的,我们也可以通过建一个大堆来实现数组降序排序

利用数组建堆有几种方式?

       利用数组建堆的目的是想先将数组元素按大堆或者小堆的规则建好堆,方便我们做后序处理。但是现在我们可以有两种方式建堆:

       两种方式的时间复杂度:向上调整算法需要调整最后一层节点,而向下调整算法只需要从倒数第二层节点开始,所以向上调整的的时间复杂度(以完全二叉树为例)为O(Nlog(N)),而向下调整算法的时间复杂的为O(N).具体的计算这里不再展开.而对于总的堆排序,我们需要调用建堆和向下调整算法,所以堆排序的时间复杂度严格来说是O(N+N*log(N)).

 那么堆是如何实现排序的呢?

     我们通过上面的例子发现,两种建堆方式虽然都是大堆,但是产生的二叉树逻辑结构并不完全一致,也就是说,像建堆产生的二叉树结构,是不能直接给数组排好序的,那,要怎么办呢?

        唉,还记得我们的取堆顶元素的函数吗?每次取出的堆顶元素,都会是当前堆内的最大或者是最小的那个数,这就好办了,我们每次输出堆顶元素,再从堆中删除它不相当于排好序了吗?嗯,这就是堆排序的大致远离了。下面我们就来实现叭~ 

堆排序函数实现: 

void Heapsort(int* a, int n, int f)
{
	//首先,我们要会根据排降序还是升序来确定要建一个大堆还是小堆

	if (f == 0)
	{   //1.升序每次需要取出堆顶的元素是最小值,所以要建小堆,我们有两种方式建堆

		//向上调整算法建堆
		//for (int i = 1; i < n; i++)
		//{
		//	int child = i;
		//	int parent = (child - 1) / 2;//父节点下标
		//	while (child >= 0)//到根节点结束
		//	{
		//		if (a[parent] > a[child])//如果父节点更大,就挪到下边
		//		{
		//			int temp = a[parent];
		//			a[parent] = a[child];
		//			a[child] = temp;

		//			//更新节点
		//			child = parent;
		//			parent = (child - 1) / 2;
		//		}
		//		else
		//			break;

		//	}
		//}

		//向下调整算法建堆
		for (int i = (n - 1 - 1) / 2; i >= 0; i--)//其中,n-1是数组最大下标,再-1是求最右下角子树的父节点
		{
			int parent = i;
			int child = 2 * i + 1;//左孩子下标
			while (child < n)
			{
				if (child + 1 < n && a[child + 1] < a[child])//选择孩子层最小的元素往上走
					child++;
				if (a[parent] > a[child])//交换
				{
					int temp = a[parent];
					a[parent] = a[child];
					a[child] = temp;

					//更新节点,目的是更新下方的子树
					parent=child;
					child = 2 * parent + 1;
				}
				else
					break;
			}
		}

		//每输出堆顶元素,完成排序
		int end = n - 1;
		while (end >= 0)
		{
			//先交换首尾元素
			int temp = a[0];
			a[0] = a[end];
			a[end] = temp;
			printf("%d ", a[end--]);

			//调用向下调整算法

			int parent = 0;
			int child = 2 * parent + 1;
			while (child < end + 1)//end减了1,但是实际上的元素个数还是end个
			{
				if (child + 1 < end+1 && a[child + 1] < a[child])
					child++;
				if (a[child] < a[parent])
				{
					int temp = a[parent];
					a[parent] = a[child];
					a[child] = temp;

					//更新节点
					parent = child;
					child = 2 * parent + 1;
				}
				else
					break;
			}
		}
	}

	else if (f == 1)
	{
		//1.降序序每次需要取出堆顶的元素是最大值,所以要建大堆,我们还是给出两种方式建堆,其实就是改个大于小于号就行了

	   //向上调整算法建堆
		//for (int i = 1; i < n; i++)
		//{
		//	int child = i;
		//	int parent = (child - 1) / 2;//父节点下标
		//	while (child >= 0)//到根节点结束
		//	{
		//		if (a[parent] < a[child])//如果父节点更大,就挪到下边
		//		{
		//			int temp = a[parent];
		//			a[parent] = a[child];
		//			a[child] = temp;

		//			//更新节点
		//			child = parent;
		//			parent = (child - 1) / 2;
		//		}
		//		else
		//			break;
		//	}
		//}

		//向下调整算法建堆
		for (int i = (n - 1 - 1) / 2; i >= 0; i--)//其中,n-1是数组最大下标,再-1是求最右下角子树的父节点
		{
			int parent = i;
			int child = 2 * i + 1;//左孩子下标
			while (child < n)
			{
				if (child + 1 < n && a[child + 1] > a[child])//选择孩子层最小的元素往上走
					child++;
				if (a[parent] < a[child])//交换
				{
					int temp = a[parent];
					a[parent] = a[child];
					a[child] = temp;

					//更新节点,目的是更新下方的子树
					parent = child;
					child = 2 * parent + 1;
				}
				else
					break;
			}
		}

		//每输出堆顶元素,完成排序
		int end = n - 1;
		while (end >= 0)
		{
			//先交换首尾元素
			int temp = a[0];
			a[0] = a[end];
			a[end] = temp;
			printf("%d ", a[end--]);

			//调用向下调整算法

			int parent = 0;
			int child = 2 * parent + 1;
			while (child < end + 1)//end减了1,但是实际上的元素个数还是end个
			{
				if (child + 1 < end+1 && a[child + 1] > a[child])
					child++;
				if (a[child] > a[parent])
				{
					int temp = a[parent];
					a[parent] = a[child];
					a[child] = temp;

					//更新节点
					parent = child;
					child = 2 * parent + 1;
				}
				else
					break;
			}
		}

	}
}

//int a[] = { 65,100,70,32,50,60 };

2.TOP-K问题

       顾名思义,就是在N个数据中的前K个或者后K个大小的数据,比如专业前十,排行榜前一百之类的,现在如果数据有很多,但是选择的前K个却很小,这其中最大的问题就是空间复杂的的问题,我们可以利用堆操作,只是建立一个规模为K的堆,依次筛选即可,由于数据量较大,所以我们选择从文件中读数和存数,下面我们来具体实现:

首先,建大堆还是建小堆?

      如果我们求的是前K个,也就是说我们需要K个最大值,我们在将前k个数据导入并建好堆之后,需要依次对后面的N-K个数遍历并与堆内的元素比较,因为堆只有堆顶元素是有序的,其余的都需要在经过处理,而我们每次需要将更大一些的值给保留下来,所以说我们应该建一个小堆,否则就会出现类似于第3个大的数被第二个大的数阻挡在堆外进不了堆的情况,相对的,求后K个就需要建大堆,来让更小的值能够进入堆中去。

函数实现(以前k个数为例):

void CreateNDate()
{
	// 造数据
	int n = 1000000;
	srand((unsigned int)time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");//会直接在该工程目录文件下创建该文件
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (size_t i = 0; i < n; ++i)
	{
		//我们模拟造几个个较大的数,方面测试我们的程序
		if(i==5)
			fprintf(fin, "%d\n", 1000000 + 1);
		else if (i == 1231)
			fprintf(fin, "%d\n", 1000000 + 2);
		else if (i == 531)
			fprintf(fin, "%d\n", 1000000 + 3);
		else if (i == 5121)
			fprintf(fin, "%d\n", 1000000 + 4);
		else if (i == 115)
			fprintf(fin, "%d\n", 1000000 + 5);
		else if (i == 2335)
			fprintf(fin, "%d\n", 1000000 + 30);
		else if (i == 8888)
			fprintf(fin, "%d\n", 1000000 + 10);
		else if (i == 9999)
			fprintf(fin, "%d\n", 1000000 + 7);
		else if (i == 76)
			fprintf(fin, "%d\n", 1000000 + 8);
		else if (i == 423)
			fprintf(fin, "%d\n", 1000000 + 9);
		else if (i == 3144)
			fprintf(fin, "%d\n", 1000000 + 20);
		else
		{
			int x = rand() % 100000;
			fprintf(fin, "%d\n", x);
		}

	}
    
	fclose(fin);
}

void PrintTopK(int k)
{
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}

	int* kminheap = (int*)malloc(sizeof(int) * (k+1));
	if (kminheap == NULL)
	{
		perror("malloc error");
		return;
	}

	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &kminheap[i]);
	}
	
	// 我们这里是求前K个数,所以需要建一个小堆
	//向下调整算法建小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		int parent = i;
		int child = 2 * i + 1;//左孩子下标
		while (child < k)
		{
			if (child + 1 < k && kminheap[child + 1] < kminheap[child])//选择孩子层最小的元素往上走
					child++;
			if (kminheap[parent] > kminheap[child])//交换
			{
				int temp = kminheap[parent];
				kminheap[parent] = kminheap[child];
				kminheap[child] = temp;

				//更新节点,目的是更新下方的子树
				parent = child;
				child = 2 * parent + 1;
			}
			else
				break;
		}
	}

	//我们需要再将N-k个元素进行逐一遍历比较
	int val = 0;
	while (!feof(fout))
	{
		fscanf(fout, "%d", &val);
		if (val > kminheap[0])//如果比堆顶元素(当前堆的最小元素大),我们就要将该元素入堆,原堆顶元素就可以出堆了
		{
			kminheap[0] = val;

			//向下调整算法恢复小堆
			int end = k - 1;
			//先交换首尾元素
			int temp = kminheap[0];
			kminheap[0] = kminheap[end];
			kminheap[k-1] = temp;

			//调用向下调整算法
			int parent = 0;
			int child = 2 * parent + 1;
			while (child < end+1 )//end减了1,但是实际上的元素个数还是end个
			{
				if (child + 1 < end+1 && kminheap[child + 1] < kminheap[child])
					child++;
				if (kminheap[child] < kminheap[parent])
				{
					int temp = kminheap[parent];
					kminheap[parent] = kminheap[child];
					kminheap[child] = temp;

					//更新节点
					parent = child;
					child = 2 * parent + 1;
				}
				else
					break;
			}

		}
	}

	//最后堆中的元素就是前k个元素,如果没有顺序要求直接输出即可
	printf("在所有数组中最大的%d个数是(无先后顺序):->\n", k);
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
	printf("\n");
	
}

2.5 完整代码及测试代码

Heap.h

#pragma once
//大根堆和小根堆的数组数组实现
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int HPDataType;
typedef struct Heap {
	HPDataType* a;
	int size;//当前存储个数
	int capacity;//容量
	int flag;//flag=0表示建小根堆,flag=1表示建大根堆
}HP;

//堆的初始化
void Heapinit(HP* php);

//堆的销毁
void Heapdestory(HP* php);

//堆的插入
void Heappush(HP* php, HPDataType x);

//删除堆顶元素(二叉树的根节点)
void Heappop(HP* php);

//获取堆顶元素
HPDataType Heaptop(HP* php);

//判断堆是否为空
bool Heapempty(HP* php);


Heap.cpp

#include "Heap.h"
#include <bits/stdc++.h>
//堆的初始化
void Heapinit(HP* php)
{
	assert(php);
	php->a = NULL;
	php->capacity = php->size = 0;
	php->flag = 0;//我这里默认为小根堆,可以自行调节
}

void Adjustup(HP* php,int childidx)//我们可以通过下标找到其父节点等祖先节点
{
	int parent = (childidx - 1) / 2;
	
	while (childidx >= 0)
	{
		if (php->flag == 0)
		{
			//1.实现小根堆
			if (php->a[parent] > php->a[childidx])
			{
				//交换变量的值
				HPDataType temp = php->a[parent];
				php->a[parent] = php->a[childidx];
				php->a[childidx] = temp;

				//更新节点从而能继续向上比较
				childidx = parent;
				parent = (childidx - 1) / 2;
			}
			else
				break;//不论是小根堆还是大根堆,在我们插入一个数据以前都是符合要求的,如果第一轮判断就合法,那么就其祖先节点就不会再有非法节点出现
		}
		else if(php->flag==1)
		{
			//2.实现大根堆
			if (php->a[parent] < php->a[childidx])
			{
				//交换变量的值
				HPDataType temp = php->a[parent];
				php->a[parent] = php->a[childidx];
				php->a[childidx] = temp;

				//更新节点从而能继续向上比较
				childidx = parent;
				parent = (childidx + 1) / 2;
			}
			else
				break; 
		}
	}
}
void Adjustdown(HP *php, int parentidx)
{
	int child = 2 * parentidx + 1;
	while (child< php->size)
	{
		if (php->flag == 0)
		{
			//1、小堆(即数字小的越在上面)
			//我们需要找到左右孩子中较小的那个来和父节点交换
			if (child + 1 < php->size && php->a[child + 1] < php->a[child])//此时我们需要将父节点和右孩子交换
				++child;
			if (php->a[parentidx] > php->a[child])
			{
				HPDataType temp = php->a[parentidx];
				php->a[parentidx] = php->a[child];
				php->a[child] = temp;

				//对当前选择的路径继续进行调整
				parentidx = child;
				child = 2 * parentidx + 1;
			}
			else //如果越界或者左右孩子都比父节点大(小堆),或者都比父节点小(大堆),因为原来的结构本来就是堆结构,根节点符合所选择的最值路径而满足堆结构之后,整个树就已经满足了堆要求
				break;
		}
		else if (php->flag == 1)
		{
			//2.大堆
			if (child + 1 < php->size && php->a[child + 1] > php->a[child])
				++child;
			if (php->a[parentidx] < php->a[child])
			{
				HPDataType temp = php->a[parentidx];
				php->a[parentidx] = php->a[child];
				php->a[child] = temp;

				//对当前选择的路径继续进行调整
				parentidx = child;
				child = 2 * parentidx + 1;
			}
			else //如果越界或者左右孩子都比父节点大(小堆),或者都比父节点小(大堆),因为原来的结构本来就是堆结构,根节点符合所选择的最值路径而满足堆结构之后,整个树就已经满足了堆要求
				break;
		}
		
	}
}
//堆的销毁
void Heapdestory(HP* php)
{
	assert(php);
	free(php->a);
	php->a = NULL;
	php->size = php->capacity = 0;
}

//堆的插入
void Heappush(HP* php, HPDataType x)
{
	if (php->size == php->capacity)//如果满了就扩容
	{
		int newcapacity = 2 * php->capacity + 4;
		php->a = (HPDataType*)realloc(php->a, sizeof(HPDataType) * (newcapacity));//防止判断0的情况不分配空间
		if (!php->a)
		{
			perror("分配失败\n");
			return;
		}
		php->capacity = newcapacity;

	}
	php->a[php->size++] = x;
	//向上调整函数

	Adjustup(php, php->size - 1);
}

//删除堆顶元素(二叉树的根节点)首尾节点交换法删除
void Heappop(HP* php)
{
	assert(php);
	assert(!Heapempty(php));
	//交换首尾元素
	HPDataType temp = php->a[0];
	php->a[0] = php->a[php->size - 1];
	php->a[php->size - 1] = temp;
	//删除尾结点
	php->size--;

	//调用调整算法使逻辑结构重新调整为大堆或者小堆,注意我们交换完首尾之后,根的左右子树需仍然保持堆结构,我们才能使用向下调整算法来再次调为堆
	Adjustdown(php,0);
}

//获取堆顶元素
HPDataType Heaptop(HP* php)
{
	assert(php);
	return php->a[0];
}

//判断堆是否为空
bool Heapempty(HP* php)
{
	assert(php);
	return php->size == 0;
}

Test.cpp(堆排序和TOP-k问题函数)

#define _CRT_SECURE_NO_WARNINGS

#include "Heap.h"
#include <stdio.h>
#include <time.h>

//这里我们为了使堆排序与程序的耦合性降低,方便我们可以直接摘出来当函数用,我们采用单独实现的方法,单独实现堆的相关函数
void Heapsort(int* a, int n, int f)
{
	//首先,我们要会根据排降序还是升序来确定要建一个大堆还是小堆

	if (f == 0)
	{   //1.升序每次需要取出堆顶的元素是最小值,所以要建小堆,我们有两种方式建堆

		//向上调整算法建堆
		//for (int i = 1; i < n; i++)
		//{
		//	int child = i;
		//	int parent = (child - 1) / 2;//父节点下标
		//	while (child >= 0)//到根节点结束
		//	{
		//		if (a[parent] > a[child])//如果父节点更大,就挪到下边
		//		{
		//			int temp = a[parent];
		//			a[parent] = a[child];
		//			a[child] = temp;

		//			//更新节点
		//			child = parent;
		//			parent = (child - 1) / 2;
		//		}
		//		else
		//			break;

		//	}
		//}

		//向下调整算法建堆
		for (int i = (n - 1 - 1) / 2; i >= 0; i--)//其中,n-1是数组最大下标,再-1是求最右下角子树的父节点
		{
			int parent = i;
			int child = 2 * i + 1;//左孩子下标
			while (child < n)
			{
				if (child + 1 < n && a[child + 1] < a[child])//选择孩子层最小的元素往上走
					child++;
				if (a[parent] > a[child])//交换
				{
					int temp = a[parent];
					a[parent] = a[child];
					a[child] = temp;

					//更新节点,目的是更新下方的子树
					parent=child;
					child = 2 * parent + 1;
				}
				else
					break;
			}
		}

		//每输出堆顶元素,完成排序
		int end = n - 1;
		while (end >= 0)
		{
			//先交换首尾元素
			int temp = a[0];
			a[0] = a[end];
			a[end] = temp;
			printf("%d ", a[end--]);

			//调用向下调整算法

			int parent = 0;
			int child = 2 * parent + 1;
			while (child < end + 1)//end减了1,但是实际上的元素个数还是end个
			{
				if (child + 1 < end+1 && a[child + 1] < a[child])
					child++;
				if (a[child] < a[parent])
				{
					int temp = a[parent];
					a[parent] = a[child];
					a[child] = temp;

					//更新节点
					parent = child;
					child = 2 * parent + 1;
				}
				else
					break;
			}
		}
	}

	else if (f == 1)
	{
		//1.降序序每次需要取出堆顶的元素是最大值,所以要建大堆,我们还是给出两种方式建堆,其实就是改个大于小于号就行了

	   //向上调整算法建堆
		//for (int i = 1; i < n; i++)
		//{
		//	int child = i;
		//	int parent = (child - 1) / 2;//父节点下标
		//	while (child >= 0)//到根节点结束
		//	{
		//		if (a[parent] < a[child])//如果父节点更大,就挪到下边
		//		{
		//			int temp = a[parent];
		//			a[parent] = a[child];
		//			a[child] = temp;

		//			//更新节点
		//			child = parent;
		//			parent = (child - 1) / 2;
		//		}
		//		else
		//			break;
		//	}
		//}

		//向下调整算法建堆
		for (int i = (n - 1 - 1) / 2; i >= 0; i--)//其中,n-1是数组最大下标,再-1是求最右下角子树的父节点
		{
			int parent = i;
			int child = 2 * i + 1;//左孩子下标
			while (child < n)
			{
				if (child + 1 < n && a[child + 1] > a[child])//选择孩子层最小的元素往上走
					child++;
				if (a[parent] < a[child])//交换
				{
					int temp = a[parent];
					a[parent] = a[child];
					a[child] = temp;

					//更新节点,目的是更新下方的子树
					parent = child;
					child = 2 * parent + 1;
				}
				else
					break;
			}
		}

		//每输出堆顶元素,完成排序
		int end = n - 1;
		while (end >= 0)
		{
			//先交换首尾元素
			int temp = a[0];
			a[0] = a[end];
			a[end] = temp;
			printf("%d ", a[end--]);

			//调用向下调整算法

			int parent = 0;
			int child = 2 * parent + 1;
			while (child < end + 1)//end减了1,但是实际上的元素个数还是end个
			{
				if (child + 1 < end+1 && a[child + 1] > a[child])
					child++;
				if (a[child] > a[parent])
				{
					int temp = a[parent];
					a[parent] = a[child];
					a[child] = temp;

					//更新节点
					parent = child;
					child = 2 * parent + 1;
				}
				else
					break;
			}
		}

	}
}


void CreateNDate()
{
	// 造数据
	int n = 1000000;
	srand((unsigned int)time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");//会直接在该工程目录文件下创建该文件
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (size_t i = 0; i < n; ++i)
	{
		//我们模拟造几个个较大的数,方面测试我们的程序
		if(i==5)
			fprintf(fin, "%d\n", 1000000 + 1);
		else if (i == 1231)
			fprintf(fin, "%d\n", 1000000 + 2);
		else if (i == 531)
			fprintf(fin, "%d\n", 1000000 + 3);
		else if (i == 5121)
			fprintf(fin, "%d\n", 1000000 + 4);
		else if (i == 115)
			fprintf(fin, "%d\n", 1000000 + 5);
		else if (i == 2335)
			fprintf(fin, "%d\n", 1000000 + 30);
		else if (i == 8888)
			fprintf(fin, "%d\n", 1000000 + 10);
		else if (i == 9999)
			fprintf(fin, "%d\n", 1000000 + 7);
		else if (i == 76)
			fprintf(fin, "%d\n", 1000000 + 8);
		else if (i == 423)
			fprintf(fin, "%d\n", 1000000 + 9);
		else if (i == 3144)
			fprintf(fin, "%d\n", 1000000 + 20);
		else
		{
			int x = rand() % 100000;
			fprintf(fin, "%d\n", x);
		}

	}
    
	fclose(fin);
}
void PrintTopK(int k)
{
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}

	int* kminheap = (int*)malloc(sizeof(int) * (k+1));
	if (kminheap == NULL)
	{
		perror("malloc error");
		return;
	}

	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &kminheap[i]);
	}
	
	// 我们这里是求前K个数,所以需要建一个小堆
	//向下调整算法建小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		int parent = i;
		int child = 2 * i + 1;//左孩子下标
		while (child < k)
		{
			if (child + 1 < k && kminheap[child + 1] < kminheap[child])//选择孩子层最小的元素往上走
					child++;
			if (kminheap[parent] > kminheap[child])//交换
			{
				int temp = kminheap[parent];
				kminheap[parent] = kminheap[child];
				kminheap[child] = temp;

				//更新节点,目的是更新下方的子树
				parent = child;
				child = 2 * parent + 1;
			}
			else
				break;
		}
	}

	//我们需要再将N-k个元素进行逐一遍历比较
	int val = 0;
	while (!feof(fout))
	{
		fscanf(fout, "%d", &val);
		if (val > kminheap[0])//如果比堆顶元素(当前堆的最小元素大),我们就要将该元素入堆,原堆顶元素就可以出堆了
		{
			kminheap[0] = val;

			//向下调整算法恢复小堆
			int end = k - 1;
			//先交换首尾元素
			int temp = kminheap[0];
			kminheap[0] = kminheap[end];
			kminheap[k-1] = temp;

			//调用向下调整算法
			int parent = 0;
			int child = 2 * parent + 1;
			while (child < end+1 )//end减了1,但是实际上的元素个数还是end个
			{
				if (child + 1 < end+1 && kminheap[child + 1] < kminheap[child])
					child++;
				if (kminheap[child] < kminheap[parent])
				{
					int temp = kminheap[parent];
					kminheap[parent] = kminheap[child];
					kminheap[child] = temp;

					//更新节点
					parent = child;
					child = 2 * parent + 1;
				}
				else
					break;
			}

		}
	}

	//最后堆中的元素就是前k个元素,如果没有顺序要求直接输出即可
	printf("在所有数组中最大的%d个数是(无先后顺序):->\n", k);
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
	printf("\n");
	
}
//int main()
//{
//	Heap hp;
//	Heapinit(&hp);
//	int a[] = { 65,100,70,32,50,60 };
//	/*for (int i = 0; i < 6; i++)
//		Heappush(&hp, a[i]);
//	Heappop(&hp);*/
//	Heapsort(a, 6, 0);
//	return 0;
//}

int main()
{
	CreateNDate();
	PrintTopK(10);
	return 0;
}

2.6 堆总结   

       堆的原理及其应用,实质上还是依靠完全二叉树和数组之间的逻辑转换关系,这其中最重要最核心的,当属向上调整算法和向下调整算法,理解了这两个算法,其他的也就好理解了。其他的已经在上面写的很详细了,这里不再赘述,毕竟,能看到这里的,都是对知识极其渴望的人吧~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值