数据结构-堆-堆排序-TopK

什么是堆?

 为什么会有向上调整算法?

在实现堆结构插入数据的时候,插入一个数据到数组末尾,这是在逻辑上会影响从这个结点到它的祖先,向上调整后使得数据在逻辑上还是一个堆

为什么会有向下调整算法?

删除堆顶的数据 和数组中最后一个数据交换,交换后不再是一个堆了,向下调整后保证它还是一个堆

堆排序中向下调整是因为向下调整建堆效率是O(N)优于向上调整 NlogN

堆的实现

1. 顺序存储
顺序结构存储就是使用 数组来存储 ,一般使用数组 只适合表示完全二叉树 ,因为不是完全二叉树会有空间的浪费。
二叉树顺序存储在物理上是一个数组,在逻辑上是一颗二叉树。

HeapPush插入

顺序表尾插比较方便,插入一个数据,但仍然要保证还是一个堆,堆又分为大堆和小堆
这里实现大堆,插入一个数据影响他的祖先
如果孩子比父亲大就交换

 parent = (child-1)/2

leftchild = parent* 2+1

rightchild = parent* 2+2
根据此规律可以父子节点可以在数组中相互找到
逻辑上是完全二叉树,存储结构也就是底层是个顺序表

 void AdjustUp(HPDataType* a, int child)

// 除了child这个位置,前面数据构成堆     时间复杂度log(N) 高度次  
void AdjustUp(HPDataType* a, int child)
{
	int parent = (child - 1) / 2;
	while (child > 0)//parent>=0 经典错误水管漏水
	{
		if (a[child] > a[parent])//大堆用大于,方便更改
		{
			Swap(&a[child],&a[parent]);
			child = parent;
			parent = (child - 1) / 2;//这里写child更好理解,计算的是新孩子的父亲
		}
		else
		{
			break;
		}
	}
}

void HeapPush(Heap* php, HPDataType x)
{
	assert(php);

	if (php->size == php->capacity)
	{
		HPDataType* tmp = (HPDataType*)realloc(php->a,sizeof(HPDataType) * php->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		php->a = tmp;
		php->capacity *= 2;
	}
	php->a[php->size++] = x;

	AdjustUp(php->a,php->size-1);//size-1是孩子下标
}
总体上就是顺序表尾插 然后向上调整,但要注意的是经典的水管漏水错误
child = parent;
parent = (child - 1) / 2;
当child = 0 ,parent 也是0  如果判断用parent>=0,程序会正常运行,但是不是我们所预期的逻辑,是从break出去的,而不是期望着 parent到达负数 就停下

HeapPop删除

pop掉尾很简单size--就可以但是对于堆来说没什么意义

大堆要Pop最大的值
步骤:

把堆顶下标0换到数组size-1的位置,size--
原来的size-1的数据破坏了大堆,要继续从 根也就是下标为0的位置向下调整,

parent要小于较大的那个child就交换   

大堆就与较大的子结点交换,小堆就与较小的子节点交换(因为要保证大堆或小堆)

直到调整到叶子,或者parent > child就结束

void HeapPop(Heap* php)

//使用前提:左右子树都是大堆/小堆   时间复杂度log(N) 高度次       
void AdjustDown(HPDataType* a, int n, int parent)
{
	//默认左孩子大,更方便
	int child = parent * 2 + 1;
	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 HeapPop(Heap* php)
{
	assert(php);
	assert(!HeapEmpty(php));

	//删除数据
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

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

并且要注意 向上调整 和向下调整的前提

向上调整 :除了child这个位置,前面数据构成堆

向下调整:左右子树都是大堆/小堆

 代码实现是注意一下

不必定义左右孩子,那样会冗余,判断多
利用leftchild + 1 = rightchild 就能找到右孩子

如果右孩子存在且大于左孩子,那么就只定义一个child,让child +1就是右孩子
 

堆排序实现

上面只是堆的实现,并不是堆排序,不会因为排序而创建一个堆的结构,得不偿失。

可以利用向上调整或者向下调整 建一个堆

并且在这里 排升序需要建立 大堆,降序就用小堆

向上调整就是 把第一个数看作根,从1到N-1 模拟堆的插入 建堆

时间复杂度是NlogN

而向下调整建堆 时间复杂度是O(N)

所以用向下调整

向下调整建堆 

不需要从叶子开始调整,因为叶子可以看作大堆和小堆,

(叶子之后无孩子)向下调整叶子结点无意义

需要从第一个非叶子结点开始调整直到调整到根(2),第一个非叶子结点也就是最后一个结点的parent

建立大堆选出最大,将最大的与最后一个交换,就排好了最大的,再让3向下调整,选出次大的数,而9就不算再堆里,控制n-1  n-2

如图类似,建立大堆 确定最后一个最大的,继续向下调整选出次大的

//排升序-建大堆 排降序-建小堆
void HeapSort(int* a, int n)
{
	//建堆--向上调整建堆 -- 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);
	}

	int end = n - 1;//end正好是前面数据个数
	while(end>0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}
	
}

如果排升序建小堆的坑

TopK

Topk只是要选出所有数据中最大的前K个,选出来就行,无需给K个数据排序

1.直接用排序 杀鸡用牛刀,没必要把40G的数据都排个序

2.建个N个数的大堆,依次pop K次
但是如果数据量过大100亿个整型,大概40G内存存不下

3.求最大的前K个

用前K个数,建小堆
再将剩下N-K个数  依次与堆顶比   如果大  就替换它  向下调整
最后这个小堆的数据就是最大的前K个

如果建大堆  那么如果再最开始最大的数进来,那么剩下的大数就进不来了

只能选出最大的不能选出次大的
如果选最小的前K个建大堆,最小的都往下沉了
 

void PrintTopK(const char* file, int k)
{
	int* topk = (int*)malloc(sizeof(int) * k);
	assert(topk);
	
	FILE* pf = fopen(file, "r");
	assert(pf);
	
	//读出前K个数据
	for (int i = 0; i < k; ++i)
	{
		fscanf(pf, "%d", &topk[i]);//不用加\n 默认空格和换行是分隔
	}
	//找最大前K个-建小堆--取n中前K个数据建小堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(topk, k, i);
	}

	//将剩余N-K个数据依次与堆顶元素比较,如果比堆顶大就替换它向下调整
	int val = 0;
	int ret = fscanf(pf, "%d", &val);
	while (ret != EOF)
	{
		if (val > topk[0])
		{
			topk[0] = val;
			AdjustDown(topk, k, 0);
		}
		ret = fscanf(pf, "%d", &val);
	}

	for (int i = 0; i < k; i++)
	{
		printf("%d ", topk[i]);
	}
	printf("\n");
	
	free(topk);
	fclose(pf);
	pf = NULL;
}
void CreatNdata()
{
	int n = 10000;
	srand((unsigned int)time(0));//设置随机种子,不然rand不够随机
	const char* file = "data.txt";
	FILE* pf = fopen(file, "w");
	
	if (pf == NULL)
	{
		perror("fopen");
		return;
	}
	//随机生成10000个数据
	for (int i = 0; i < n; i++)
	{
		int val = rand() % 10000;//范围0~9999
		fprintf(pf, "%d\n", val);//每一行写一个
	}

	fclose(pf);
	pf = NULL;

}

int main()
{
	
	CreatNdata();
	PrintTopK("data.txt", 10);
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值