二叉树 堆与堆排序的实现(附有TOP-K问题)

目录

二叉树概念及结构

满二叉树与完全二叉树

二叉树的性质

堆的概念及结构

 堆的实现

堆的创建

堆的销毁

交换函数

 堆的打印

堆的插入

向上调整算法

堆的删除

向上调整算法详解图:

向下调整算法

向下调整算法详解图:

获取堆顶元素

获取堆的数据个数

堆的判空

完整代码

Heap.h

Heap.c

test.c

 TopK问题

 堆排序

具体思想:

堆排序详解图:

二叉树概念及结构

形如下图结构的被称为二叉树。

二叉树特点:

1、二叉树不存在大于2的结点。

2、二叉树的子树有左右之分,次序不能颠倒,因此二叉树是有序树。

满二叉树与完全二叉树

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

二叉树的性质

1. 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^(i-1)个结点.
2. 若规定根节点的层数为1,则深度为h的二叉树的最大结点数是2^h-1 .
3. 对任何一棵二叉树, 如果度为0其叶结点个数为n0 , 度为2的分支结点个数为 n2,则有 n0=n2 +1
4. 若规定根节点的层数为1,具有n个结点的满二叉树的深度,h=log2(n+1) . (ps:log2(n+1) 是log以2为底,n+1为对数)
5. 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的数组顺序对所有节点从0开始编号,则对于序号为i的结点有:
1. 若i>0,i位置节点的双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
2. 若2i+1<n,左孩子序号:2i+1,2i+1>=n否则无左孩子
3. 若2i+2<n,右孩子序号:2i+2,2i+2>=n否则无右孩子
 

堆的概念及结构

堆的性质:
1、堆中某个节点的值总是不大于或不小于其父节点的值;
2、堆总是一棵完全二叉树。

 堆的实现

堆的创建

根据存储结构可知,我们可以用一个数组来存储这些数据,然后进行调整(具体调整步骤后面会讲),直到它成为一个堆。

我们用typedef来将int重命名以便以后类型转换的便利。

typedef int HPDataType;

既然本质是一个数组,那么其实创建的过程跟我们之前学习的顺序表没有什么太大区别。

代码如下:

// 堆的构建
typedef int HPDataType;//后面的int都用HPDataType 来代替
void HeapInit(Heap* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

堆的销毁

既然创建跟顺序表类似,那么销毁也没太大区别。

代码如下:

//堆的销毁
void HeapDestroy(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->capacity = hp->size = 0;
}

交换函数

便于交换数据,代码如下:

//交换数据
void Swap(HPDataType* x, HPDataType* y)
{
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}

 堆的打印

打印也如此,直接上代码:

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

堆的插入

特点:

在插入的过程中要有重要的调整步骤,此步骤也是堆的关键步骤,也是我们学习堆的主要学习思想,堆的特性也是在插入的过程中不断的调整从而实现。

这里我把堆的特性再讲一下:

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

主要是第一点,也就是插入的过程中必须保证这第一点,那么怎么才能保证这第一点呢?

我们首先看一下堆插入的代码:

// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	if (hp->capacity == hp->size)
	{
		HPDataType newCapacity = hp->capacity == 0 ? 4 : 2 * (hp->capacity);
		HPDataType* tmp = realloc(hp->a, sizeof(HPDataType)*(newCapacity));
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newCapacity;
	}
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp->a, hp->size - 1);//向上调整函数
}

聪明的你肯定发现了,这个操作其实与顺序表的插入并没有太大区别,但是插入过后要进行重要的一步,那就是向上调整函数。

向上调整算法

这里我们以小堆为例子,当我们插入一个数的时候,要让这个数与它的双亲进行比较,由于是小堆,必须保证双亲必须小于孩子,那么如果双亲大于孩子,那么就要进行交换。

如图是将一个10插入到一个堆里面,然后进行向上调整的过程。

 代码如下:

//向上调整算法
//双亲序列(i-1)/2
void AdjustUp(HPDataType *a, HPDataType child)
{
	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 HeapPop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown(hp->a, hp->size, 0);
}

向下调整算法

我们这里还是以小堆为例,但是这里有一点需要注意的是,当根节点与孩子交换的时候,要找数值较小的那个数据进行交换,这样能保证根节点是两个孩子当中最小的。

向下调整算法详解图:

 代码如下:


//向下调整算法
//左孩子=(2*parent+1) 右孩子=(2*parent+2)
void AdjustDown(HPDataType* a, HPDataType n, HPDataType root)
{
	int parent = root;
	int child = 2 * parent + 1;
	while (child < n)
	{
		//这里的条件是小堆
        //如果要实现大堆 将&&前边条件中的<改为>
		if ((a[child + 1] <a[child]) && (child + 1) < n)
		{
			child++;
		}

		//这里的条件是小堆
        //如果要实现大堆 将<改为>
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}

获取堆顶元素

如果数组不为空的话直接返回数组下标为0的数据就可以了。

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

获取堆的数据个数

直接返回size就可以了。        

// 堆的数据个数
int HeapSize(Heap* hp)
{
	assert(hp);
	return hp->size;
}

堆的判空

只要size为0,堆就为空。


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

完整代码

Heap.h

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

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

//交换数据
void Swap(HPDataType* x, HPDataType* y);

// 堆的构建
void HeapInit(Heap* hp);

// 堆的销毁
void HeapDestroy(Heap* hp);

//堆的打印
void HeapPrint(Heap* hp);

//向上调整算法
void AdjustUp(HPDataType* a, HPDataType child);

//向下调整算法
void AdjustDown(HPDataType* a, HPDataType n, HPDataType root);

// 堆的插入
void HeapPush(Heap* hp, HPDataType x);

// 堆的删除
void HeapPop(Heap* hp);

// 取堆顶的数据
HPDataType HeapTop(Heap* hp);

// 堆的数据个数
int HeapSize(Heap* hp);

// 堆的判空
bool HeapEmpty(Heap* hp);

Heap.c

#include"Heap.h"

//交换数据
void Swap(HPDataType* x, HPDataType* y)
{
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}

// 堆的构建
void HeapInit(Heap* hp)
{
	assert(hp);
	hp->a = NULL;
	hp->capacity = hp->size = 0;
}

//堆的销毁
void HeapDestroy(Heap* hp)
{
	assert(hp);
	free(hp->a);
	hp->capacity = hp->size = 0;
}

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



//向上调整算法
//双亲序列(i-1)/2
void AdjustUp(HPDataType *a, HPDataType child)
{
	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;
		}
	}	
}

//向下调整算法
//左孩子=(2*parent+1) 右孩子=(2*parent+2)
void AdjustDown(HPDataType* a, HPDataType n, HPDataType root)
{
	int parent = root;
	int child = 2 * parent + 1;
	while (child < n)
	{
		//小根堆
		//由于是小根堆 所以找出左右孩子小的那个孩子
		if ((a[child + 1] <a[child]) && (child + 1) < n)
		{
			child++;
		}

		//小根堆
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = 2 * parent + 1;
		}
		else
		{
			break;
		}
	}
}
// 堆的插入
void HeapPush(Heap* hp, HPDataType x)
{
	assert(hp);
	if (hp->capacity == hp->size)
	{
		HPDataType newCapacity = hp->capacity == 0 ? 4 : 2 * (hp->capacity);
		HPDataType* tmp = realloc(hp->a, sizeof(HPDataType)*(newCapacity));
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		hp->a = tmp;
		hp->capacity = newCapacity;
	}
	hp->a[hp->size] = x;
	hp->size++;
	AdjustUp(hp->a, hp->size - 1);
}

// 堆的删除
void HeapPop(Heap* hp)
{
	assert(hp);
	assert(!HeapEmpty(hp));
	Swap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown(hp->a, hp->size, 0);
}

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

// 堆的数据个数
int HeapSize(Heap* hp)
{
	assert(hp);
	return hp->size;
}

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


test.c

#include"Heap.h"


void TestHeap()
{
	int a[] = { 70, 56, 30, 25, 15, 10, 75 };
	Heap hp;
	HeapInit(&hp);
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); i++)
	{
		HeapPush(&hp, a[i]);
	}
	HeapPrint(&hp);

	HeapPop(&hp);
	HeapPrint(&hp);

	HeapPop(&hp);
	HeapPrint(&hp);

	HeapPop(&hp);
	HeapPrint(&hp);

	HeapPop(&hp);
	HeapPrint(&hp);

	HeapPop(&hp);
	HeapPrint(&hp);

	HeapPop(&hp);
	HeapPrint(&hp);

	HeapDestroy(&hp);
}
int main()
{
	TestHeap();
	return 0;
}

 TopK问题

 TopK问题:找出N个数里面最大/最小的前K个问题。
 比如:长葛排名前10的胡辣汤,郑州科技学院大学王者荣耀排名前10的韩信,全国排名前10的李白。等等问题都是Topk问题,
 需要注意:
 找最大的前K个,建立K个数的小堆
 找最小的前K个,建立K个数的大堆

解析:

假设我们需要求N个数里面最大的前k个问题,我们就要建一个小堆,那么为什么我们不建大堆呢? 倘若我们建了大堆,如果第一个就是我们所要找的最大数,那么无论怎么向下调整,其他数都进不去。

代码如下:

void PrintTopK(int* a, int n, int k)
{
	Heap hp;
	HeapInit(&hp);
	//创建一个有k个数据小堆

	for (int i = 0; i < k; i++)
	{
		HeapPush(&hp, a[i]);
	}

	for (int i = 0; i < n; i++)
	{
		if (a[i] > HeapTop(&hp))
			//如果遍历的数据大于堆顶
			//就将其插入
		{
			HeapPop(&hp);
			HeapPush(&hp, a[i]);
		}
	}

	HeapPrint(&hp);
	HeapDestroy(&hp);
}
void TestTopk()
{
	int n = 1000000;
	int* a = (int*)malloc(sizeof(int) * n);//为a创建空间
	if (!a)
	{
		printf("malloc failed");
		exit(-1);
	}
	srand((unsigned)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[5355] = 1000000 + 3;
	a[51] = 1000000 + 4;
	a[15] = 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 HeapSort(int* a, int n)
{

	for (int i = (n - 1 - 1)/2; i >= 0; i--)
	{
		AdjustDown(a, n , i);
	}
	for (int end = n - 1; end > 0; end--)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
	}
}
int main()
{
	int a[] = { 16,17,5,20,3,4 };

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

	return 0;
}

评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

袁百万

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值