堆+堆排序+topK问题

目录

堆:

1、堆的概念

2、堆的结构

3、堆的实现

3.1、建堆

3.1.1、向上调整建堆(用于堆的插入)

3.1.2、向下调整建堆

3.2、堆的删除

3.3、堆的代码实现

3.3.1、Heap.h

3.3.2、Heap.c

堆排序:(O(N*log(N)))

1、排序如何建堆

2、代码实现(以小堆为例)

topK问题:

1、方法1

2、方法2


堆:

1、堆的概念

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

2、堆的结构

1、堆中某个结点的值总是<=或>=其父结点的值

2、堆 逻辑上 是一棵完全二叉树物理上数组

将根结点最大的堆叫做最大堆或大根堆,根结点最小的堆叫做最小堆或小根堆

3、堆的实现

3.1、建堆

objectivec 复制代码
typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;
3.1.1、向上调整建堆(用于堆的插入)

适用:

1、已经是堆了,要进行插入时,只能用向上调整建堆

2、对于数组(N个元素) ,可以用向上调整建堆,模拟插入的过程(插入一个,调整一次) ,但是其时间复杂度为O(N*log(N)),对于一堆的数据,不建议使用向上调整建堆

objectivec 复制代码
void AdjustUp(HPDataType* a, int child)//建小堆,child为插入元素的下标
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])//要建大堆,< 改 >
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
			break;//没有交换,则就是在这个位置
	}
}

int main()
{
	int arr[] = { 7,4,1,7,9,3,5,2,6,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i < sz; i++)
	{
		AdjustUp(arr, i);
	}
	return 0;
}

3、O(N*log(N))证明

节点数量为N个,根节点为第1层,树的高度为h层,h = log(N+1)

第h层 :2^(h-1)个节点,向上调整h-1次

第h-1层:2^(h-2)个节点,向上调整h-2次

第2层 :2^1个节点,向上调整1次

第1层 :2^0个节点,向上调整0次

F(h) = 2^0 * 0 + 2^1 * 1 + ......+ 2^(h-2) * (h-2) + 2^(h-1) * (h-1) = 2^h * (h-2) + 2

= (N+1)*(log(N+1) - 2) + 2 ,忽略后为 N*log(N)

3.1.2、向下调整建堆

适用:

1、对于数组(N个元素)建议使用 向下调整建堆 时间复杂度为O(N),更高效

2、向下调整建堆的前提左右子树必须是堆(保证向下调整完后,还是堆),因此,从第一个非叶子节点开始(对于叶子节点,可以认为就是堆),向下调整建堆

objectivec 复制代码
void AdjustDown(HPDataType* a, int n, int parent)//建小堆,n为a的元素个数,parent为要向下调整的元素下标
{
	assert(a);
	int child = 2 * parent + 1;
	while (child < n)
	{
		if (a[child] > a[child + 1] && child + 1 < n)//假设法,要建大堆,< 改 >
			child++;
		if (a[child] < a[parent])//要建大堆,< 改 >
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = 2 * child + 1;
		}
		else
			break;
	}
}

int main()
{
	int arr[] = { 7,4,1,7,9,3,5,2,6,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (int i = (sz - 1 - 1) / 2; i >= 0; i--)//倒数第一个非叶子节点,开始遍历
	{
		AdjustDown(arr, sz, i);
	}
	return 0;
}

3、 O(N)证明

节点数量为N个,根节点为第1层,树的高度为h层,h = log(N+1)

第h层 :2^(h-1)个节点,向下调整0次

第h-1层:2^(h-2)个节点,向下调整1次

第2层 :2^1个节点,向上调整h-2次

第1层 :2^0个节点,向上调整h-1次

F(h) = 2^0 * (h-1) + 2^1 * (h-2) +......+ 2^(h-2) * 1 + 2^(h-1) * 0 = 2^h - 1 - h = N - log(N+1)

忽略后为 N

3.2、堆的删除

删除最后一个元素,无意义,删除堆是删除堆顶的数据,

如果是挪动数据,关系乱了,要重新建堆,效率低

所以将堆顶的数据最后一个数据交换,size--,再进行向下调整算法

objectivec 复制代码
void HeapPop(Heap* php)
{
	assert(php);
	assert(php->_size > 0);
	Swap(&(php->_a[0]), &(php->_a[php->_size - 1]));
	php->_size--;
	AdjustDown(php->_a, php->_size, 0);
}

3.3、堆的代码实现

3.3.1、Heap.h
objectivec 复制代码
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int HPDataType;
typedef struct Heap
{
	HPDataType* _a;
	int _size;
	int _capacity;
}Heap;

void Swap(HPDataType* p1, HPDataType* p2);
void AdjustUp(HPDataType* a, int child);
void AdjustDown(HPDataType* a, int n, int parent);

void HeapInit(Heap* php);
void HeapDestory(Heap* php);
void HeapPush(Heap* php, HPDataType x);
void HeapPop(Heap* php);
HPDataType HeapTop(Heap* php);
int HeapSize(Heap* php);
int HeapEmpty(Heap* php);
3.3.2、Heap.c
objectivec 复制代码
#include "Heap.h"

void HeapInit(Heap* php)
{
	assert(php);
	php->_a = NULL;
	php->_size = php->_capacity = 0;
}

void HeapDestory(Heap* php)
{
	assert(php);
	free(php->_a);
	php->_a = NULL;
	php->_size = php->_capacity = 0;
}

void HeapCapacity(Heap* php)
{
	assert(php);
	if (php->_size == php->_capacity)
	{
		php->_capacity = php->_capacity == 0 ? 4 : 2 * php->_capacity;
		HPDataType* tmp = (HPDataType*)realloc(php->_a,sizeof(HPDataType) * php->_capacity);
		if (tmp == NULL)
		{
			perror("HeapCapacity()::realloc()");
			return;
		}
		php->_a = tmp;
	}
}

void Swap(HPDataType* p1, HPDataType* p2)
{
	HPDataType tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

void AdjustUp(HPDataType* a, int child)//建小堆,child为插入元素的下标
{
	assert(a);
	int parent = (child - 1) / 2;
	while (child > 0)
	{
		if (a[child] < a[parent])//要建大堆,< 改 >
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (parent - 1) / 2;
		}
		else
			break;//没有交换,则就是在这个位置
	}
}

void AdjustDown(HPDataType* a, int n, int parent)//建小堆,n为a的元素个数,parent为要向下调整的元素下标
{
	assert(a);
	int child = 2 * parent + 1;
	while (child < n)
	{
		if (a[child] > a[child + 1] && child + 1 < n)//假设法,要建大堆,< 改 >
			child++;
		if (a[child] < a[parent])//要建大堆,< 改 >
		{
			Swap(&a[parent], &a[child]);
			parent = child;
			child = 2 * child + 1;
		}
		else
			break;
	}
}

void HeapPush(Heap* php, HPDataType x)
{
	assert(php);
	HeapCapacity(php);//扩容
	php->_a[php->_size++] = x;
	AdjustUp(php->_a, php->_size-1);
}

void HeapPop(Heap* php)
{
	assert(php);
	assert(php->_size > 0);
	Swap(&(php->_a[0]), &(php->_a[php->_size - 1]));
	php->_size--;
	AdjustDown(php->_a, php->_size, 0);
}

HPDataType HeapTop(Heap* php)
{
	assert(php);
	assert(php->_size > 0);
	return php->_a[0];
}

int HeapSize(Heap* php)
{
	assert(php);
	return php->_size;
}

int HeapEmpty(Heap* php)
{
	assert(php);
	if (php->_size == 0)
		return 1;
	else
		return 0;
}

堆排序:(O(N*log(N))

对于数组,建堆时,用向下调整建堆,效率高,

此时是一定程度脱离了堆这个数据结构(没有用push,向上调整建堆),只是用了向下调整的方法

1、排序如何建堆

升序:建大堆

如果建小堆,第1个数已经排好了,后面的数据,关系乱了(兄弟变父子,左右子树很可能不是堆了),要多次向下调整,才建好堆,效率低,

那建大堆,首尾交换 (兄弟还是兄弟,左右子树还是堆,一次向下调整就建好堆)再伪删除,"删除"完,就排好升序了

建堆 (向下调整),时间复杂度为O(N)

排序 (首尾交换,向下调整),时间复杂度为O(N*log(N))

节点数量为N个,根节点为第1层,树的高度为h层,h = log(N+1)

第h层 :2^(h-1)个节点,交换,向下调整h-1次

第h-1层:2^(h-2)个节点,交换,向下调整h-2次

第2层 :2^1个节点,交换,向下调整1次

第1层 :2^0个节点,交换,向下调整0次

F(h) = 2^0 * 0 + 2^1 * 1 + ......+ 2^(h-2) * (h-2) + 2^(h-1) * (h-1) = 2^h * (h-2) + 2

= (N+1)*(log(N+1) - 2) + 2 ,忽略后为 N*log(N)

降序:建小堆

依次类推

2、代码实现(以小堆为例)

objectivec 复制代码
void Heapsort(int* arr,int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)//倒数第一个非叶子节点,开始遍历
	{
		AdjustDown(arr, n, i);
	}
	int end = n;
	while (end>1)
	{
		Swap(&(arr[0]), &(arr[end-1]));
		end--;//伪删除
		AdjustDown(arr,end,0);
	}
}

int main()
{
	int arr[] = { 7,4,1,7,9,3,5,2,6,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	Heapsort(arr,sz);
	return 0;
}

topK问题:

TOP-K问题:即求数据结合中前K个最大的元素或者最小的元素,一般N远大于K

以下 以找出前K个最大的数为例 N远大于K

1、方法1

建N个数的大堆O(N)

popK次 O(K*log(N))

时间复杂度为O(N) ,但对空间要求太大

2、方法2

建一个K个小堆(先从文件里读K个元素,向下调整建堆),再从文件里读一个个元素,大于堆顶元素,就覆盖,向下调整

objectivec 复制代码
void CreateNDate()
{
	// 造数据
	int n = 100000;
	srand(time(0));
	const char* file = "data.txt";
	FILE* fin = fopen(file, "w");
	if (fin == NULL)
	{
		perror("fopen error");
		return;
	}

	for (int i = 0; i < n; ++i)
	{
		int x = (rand()+i) % 10000000;//自己可以改10个大于10000000,相当于10标记,验证结果
		fprintf(fin, "%d\n", x);
	}

	fclose(fin);
}

void TestHeap3()
{
	int k;
	printf("请输入k>:");
	scanf("%d", &k);
	int* kminheap = (int*)malloc(sizeof(int) * k);
	if (kminheap == NULL)
	{
		perror("malloc fail");
		return;
	}
	const char* file = "data.txt";
	FILE* fout = fopen(file, "r");
	if (fout == NULL)
	{
		perror("fopen error");
		return;
	}

	// 读取文件中前k个数
	for (int i = 0; i < k; i++)
	{
		fscanf(fout, "%d", &kminheap[i]);
	}

	// 建K个数的小堆
	for (int i = (k-1-1)/2; i>=0 ; i--)
	{
		AdjustDown(kminheap, k, i);
	}

	// 读取剩下的N-K个数
	int x = 0;
	while (fscanf(fout, "%d", &x) > 0)
	{
		if (x > kminheap[0])
		{
			kminheap[0] = x;
			AdjustDown(kminheap, k, 0);
		}
	}

	printf("最大前%d个数:", k);
	for (int i = 0; i < k; i++)
	{
		printf("%d ", kminheap[i]);
	}
	printf("\n");
	fclose(fout);
}

注意:

交换后,向下调整,是一次,O(log(N))

直接堆数组向下调整建堆,是多次,O(N)

降序,找出前K个最大,建小堆

相关推荐
菜鸡中的奋斗鸡→挣扎鸡3 小时前
滑动窗口 + 算法复习
数据结构·算法
axxy20004 小时前
leetcode之hot100---240搜索二维矩阵II(C++)
数据结构·算法
Uu_05kkq6 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法
1nullptr7 小时前
三次翻转实现数组元素的旋转
数据结构
TT哇7 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
A懿轩A8 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
1 9 J9 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
汝即来归9 小时前
选择排序和冒泡排序;MySQL架构
数据结构·算法·排序算法
aaasssdddd9612 小时前
C++的封装(十四):《设计模式》这本书
数据结构·c++·设计模式
芳菲菲其弥章12 小时前
数据结构经典算法总复习(下卷)
数据结构·算法