对二叉树堆排序的升级&TOPK问题(跑路人笔记)

舒文未来目标: 进大厂啊进大厂~🤪.让我家人放松一些,努力让生活更棒,好耶!
舒文现状:大一菜鸡,从食品转码,目前已经结识了很多学习的朋友.(挺棒的👍)
博客目的:写博客是为了记录自己的学习路径.也是为了让面试官眼前一亮,然后就是装逼.
小小推荐: 我在CSDN和朋友创建管理了一个社区大家如果感兴趣的话可以来看看👉非科班转码社区).👈
(舒文会在该社区进行答疑和打卡大家一起加入这个大家庭啊😍)
现阶段目标: 好好学习基础知识多多了解计算机行业情况.保持良好的身体素质,多多交朋友,多多犯错多多从错误中学习😍.
上一个我期文章介绍树和树的计算公式👉树的储存形势&代码实现

前言

对堆排序进行一下升级,我们之前使用堆需要先实现一下整个堆的函数而且还要将数据导入到堆结构单独开辟的空间内,非常没必要所以我们为什么不直接将我们的原数组内的元素直接当做堆来进行堆排序的所需呢?
说搞就搞请添加图片描述

堆的应用

简易实现堆

之前我们通过实现堆的代码然后用堆代码来实现我们的堆排序,这样的堆排序的

空间复杂度O(N)

时间复杂度为O(N)

代码如下:

void HeapSort(int* a, int size)
{
	Heap hp;
	HeapCreat(&hp);
	for (int i = 0; i < size; ++i)
	{
		HeapPush(&hp, a[i]);
	}

	size_t j = 0;
	while (!HeapEmpty(&hp))
	{
		a[j] = HeapTop(&hp);
		j++;
		HeapPop(&hp);
	}

	HeapDestory(&hp);
}

而且我们想使用这样的堆排序甚至要专门搞个堆来实现.非常痛苦.

其实我们完全可以通过稍微更改一下向上和向下调整函数来实现堆排序

调整后的函数我通过注释的方式进行讲解吧=-=.毕竟之前就讲过

void Swap(HPDataType* a, HPDataType* b)
{
	HPDataType tmp = *a;
	*a = *b;
	*b = tmp;
}
void AdjustDown(HPDataType* a, size_t size, size_t root)//root就是我们要调整的父亲节点.
{
	size_t parent = root;
	size_t child = parent * 2 + 1;//公式得到的child是parent的左孩子
	while (child < size)
	{
		// 1、根据我们需要的情况去调整我们的大于小于号
		if (child + 1 < size && a[child + 1] < a[child])
		{
			++child;
		}

		// 2、孩子与父亲比较为真则交换,并继续往下调整
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}
void AdjustUp(HPDataType* a, size_t child)
{
	size_t parent = (child - 1) / 2;//公式无论是左孩子还是右孩子都可以得到我们的parent
	while (child > 0)
	{
		if (a[child] < a[parent])
		{
			Swap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			break;
		}
	}
}

我们得到调整后的函数后我们就可以将传来的数组调整成堆形式然后通过一个手段(会讲的啦)来得到我们想要的排序结果.

我们主要讲一下我们用向下调整和向上调整形成堆的时间复杂度.

堆的排序我们要使用向下调整的方式来实现.

首先我们先得到我们的堆可以通过向上调整和向下调整两种方式得到.

首先我们先将一下向上调整的方式的代码和时间复杂度讲一下.

	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}

向上调整的函数直接将所有的元素都检查一下并将所有位置都进行调整,下面这种的调整也许大家更容易理解

	for (int i = n; i >0; i--)
	{
		AdjustUp(a, i);
	}

我们来讲一下他的时间复杂度.

假设我们每次的AdjustUp都是最坏的情况及从尾一直走到头,及执行了他树高度的次数,我们知道他的N值通过公式的h = log2(N+1)(以2为底的log).

再因为for循环每次循环次数从n~1而每层节点数如下.

image-20220411152049255

我们以满二叉树为例,因为我们的cpu的运算能力就算少哪几个节点或多几个节点对时间影响不大.

所以我们可以列出公式如下:

image-20220411152758164

这就是很经典的等比数列乘以等差数列了,我们直接用错位相减的方式可得:

image-20220411152905820

然后又有N= 2h -1

所以我们的时间复杂度就为O(N*logN).

向下调整的函数的时间复杂度我们也稍微讲一下.

先看看我们使用的方式吧.

	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
    //void AdjustDown(HPDataType* a, size_t size, size_t root)//root就是我们要调整的父亲节点.
	}

相信大家注意到了我们这个int i = (n-1-1)/2这个怪怪的表达式了.

我们这个是需要从最后一个非叶节点开始调整的,n的第一次-1是为了找到最后一个节点,再-1是因为我们要找父节点的公式所以可以解读长((n-1)-1)/2,其中的n-1就是为了得到最后一个节点的下标.

既然读懂了这个步骤的操作我们就来看看他的时间复杂度吧.

类似于我们的向上调整这个时间的复杂度我们也用满二叉树来讲解.

image-20220411152049255

我们的向下调整建堆就比向上调整建堆减少了一层.

所以根据最坏次数得到的时间复杂度如下公式

假设有n个元素h层

image-20220411155712886

依旧是错位相减即可(高中知识啊).

最后得

image-20220411155814364

所以时间复杂度为O(N)

堆排序

OK.现在我们所有的方式的建堆都得到了.

现在我们想用这些堆来实现堆排序我们需要做什么呢.

这时候我们要通过向下调整来得到我们的排序结果.

使用向下调整的原因也很简单.因为向上调整函数会破坏我们已经创建好的堆结构.而向下调整不会.

不过我们不能普普通通的使用AdjustDown函数我们需要更改结尾位置

	int end = n - 1;
	while (end > 0)
	{
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
	}

为什么我们要end–呢?

其实很简单我们要通过更改end的位置将根节点(a[0])传输到从数值的倒数第一的位置再到倒数第二的位置…这样我们的排序就完成了.🤪

TOPK问题

TOPK问题分为两种,

第一种总体N和K的数值差距不大 如: 从50个人里找到前10.

第二种总体N和K的数值相差巨大 如:从10亿个人里找到前十

第一种我们直接通过排序或者建堆然后用HeapPop函数来得到我们的值

不过如果是第二种情况呢?

总不能建立一个10亿的堆吧.我们这10亿个数据都存储在文件中的时候我们如何得到前K的数据呢?

其实我们可以建一个只有K个数值的小堆,然后我们遍历那个10亿的数据发现比小堆根节点根节点大的就将其进行调换并重新将堆格式进行调整.

这样我们就得到了K个最大的值,如果还想进行排序就简单多了.

结尾

下一篇我的计划是将分治思想.也不知道会有多大的篇幅.🤪

  • 46
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 55
    评论
评论 55
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

就一个挺垃圾的跑路人

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

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

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

打赏作者

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

抵扣说明:

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

余额充值