堆的实际应用(topk问题以及堆排序)

这篇具有很好参考价值的文章主要介绍了堆的实际应用(topk问题以及堆排序)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

前言:

一:解决topk问题

二:堆排序

【1】第一种方法(很少用)

【2】第二种方法(很实用)


前言:

上一次我们进行了二叉树的初步介绍并实现了堆的基本功能,但堆的作用并不是存储数据,它可以用来解决topk问题(求一组数据较大或者较小的前k个)以及对数据进行排序

附上一期链接:http://t.csdn.cn/pMOia

一:解决topk问题

在讨论topk问题之前我们先来回顾一下堆的性质:

(1)大堆的父亲节点总是大于孩子节点

(2)小堆的父亲节点总是小于孩子节点

由此我们可以得到一个结论:

根部节点一定是这个堆的最大或者最小值(大堆为最大,小堆为最小)。

【1】我们可以把所有数据存储在堆中,得到根部的数据后删除根部数据,然后通过调整保持堆的结构,不断重复这个操作直到找到前k个数。

代码(我建立的是大堆,找较大的数):

头文件Heap.h

#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>

typedef int HPDataType;
typedef struct Heap
{
	//存储数据
	HPDataType* a;
	//有效数据个数
	int size;
	//容量
	int capacity;
}HP;
//初始化
void HeapInit(HP* hp);
//销毁
void HeapDestory(HP* hp);
//交换函数
void HeapSwap(int* p1, int* p2);
//判空函数
bool HeapEmpty(HP* hp);
//调整函数
void AdjustUp(HPDataType* a,int child);
//向下调整,n是有效个数
void AdjustDown(HPDataType* a, int n, int parent);
//插入数据
void HeapPush(HP* hp, HPDataType x);
//打印数据
void HeapPrint(HP* hp);
//删除数据
void HeapPop(HP* hp);
//取堆头部数据
HPDataType HeapTop(HP* hp);

Heap.c(函数实现)

#define _CRT_SECURE_NO_WARNINGS 1
#include "Heap.h"
//初始化
void HeapInit(HP* hp)
{
	//断言,不能传空的结构体指针
	assert(hp);
	hp->a = NULL;
	//初始化size和容量都为0
	hp->size = hp->capacity = 0;
}

//销毁
void HeapDestory(HP* hp)
{
	free(hp->a);
	hp->size = hp->capacity = 0;
}

//交换函数
void HeapSwap(int* p1, int* p2)
{
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}

//判空函数
bool HeapEmpty(HP* hp)
{
	return hp->size == 0;
}

//调整函数
//向上调整
void AdjustUp(HPDataType* a, int child)
{
	//断言,不能传空指针
	assert(a);
	//找到父结点的下标
	int parent = (child - 1) / 2;
	//循环,以child到树根为结束条件
	while (child > 0)
	{
		//如果父结点比child小,交换并更新
		if (a[child] > a[parent])
		{
			HeapSwap(&a[child], &a[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		//如果父结点比child大,跳出循环
		else
		{
			break;
		}
	}
}
//向下调整
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加1变成右孩子
			child++;
		}
		//如果父亲比大孩子小,进行调整,否则跳出
		if (a[child] > a[parent])
		{
			HeapSwap(&a[child], &a[parent]);
			//迭代
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}


//插入数据
void HeapPush(HP* hp, HPDataType x)
{
	if (hp->size == hp->capacity)
	{
		//判断扩容多少
		int newcapacity = hp->capacity == 0 ? 4 : hp->capacity * 2;
		//扩容
		HPDataType* tmp =
			(HPDataType*)realloc(hp->a, sizeof(HPDataType) * newcapacity);
		//更新
		hp->capacity = newcapacity;
		hp->a = tmp;
	}
	//存储数据
	hp->a[hp->size] = x;
	hp->size++;
	//进行调整
	AdjustUp(hp->a, hp->size-1);
}

//打印数据
void HeapPrint(HP* hp)
{
	//断言,不能传空的结构体指针
	assert(hp);
	int i = 0;
	for (i = 0; i < hp->size; i++)
	{
		printf("%d ", hp->a[i]);
	}
	printf("\n");
}

//删除数据
void HeapPop(HP* hp)
{
	//断言,不能传空的结构体指针
	assert(hp);
	//如果为空,不能删除,避免数组越界
	assert(!HeapEmpty(hp));
	//不为空,先交换根和最后一片叶子,然后size减1
	HeapSwap(&hp->a[0], &hp->a[hp->size - 1]);
	hp->size--;
	AdjustDown(hp->a, hp->size, 0);
}
//取根部数据
HPDataType HeapTop(HP* hp)
{
	return hp->a[0];
}

test.c(主逻辑)
int main()
{
    HP hp;
    HeapInit(&hp);
    int arr[20] = { 4,5,6,1,2,44,33,25,69,78,3,0,11,22,77,55,88,75,14,8 };
    //找前k个最大的数
    int k = 5;
    for (int i = 0; i < 20; i++)
    {
        HeapPush(&hp, arr[i]);
    }
    for (int i = 0; i < k; i++)
    {
        printf("%d ", HeapTop(&hp));
        HeapPop(&hp);
    }
}

 

 堆的实际应用(topk问题以及堆排序)

缺点:

(1)需要建立一个堆,消耗了额外的空间

(2)如果要排序的数字很多,内存存储不下

【2】还有另一种更常用的方法,这个方法只需要建立一个能够存储k个数据的堆

这里先给结论:

(1)这个方式找较大要建立小堆

(2)这个方式找较小要建立大堆

看到这两个结论大家可能会有点懵逼,因为前面找大我就建立大堆,找小我就建立小堆,为什么这里就反过来了呢?先不用着急,我们先讲解一下为什么找较小数要建立大堆

我们看下面这一组数据:

10  20  58  97  55  66  44  

假设我们要找出这些数据中的前4个较小的数据,我们先将前4个数据存储在大堆中,如下:

堆的实际应用(topk问题以及堆排序)

 然后我们从55(下标为k)开始遍历,如果数据小于根部,就将堆顶删除,然后将这个数据入堆,调整来保持大堆的结构

大堆根部数据是堆中最大的数据,我们遍历插入调整的行为其实是不断淘汰数据中较大的元素,一直到遍历结束还在堆中的元素就是前k个较小的值。

我们从55开始遍历替换:

堆的实际应用(topk问题以及堆排序)

后面的操作一致

 堆的实际应用(topk问题以及堆排序)

最后堆中的元素分别是55,44,10,20,满足了我们的需求。 

相反的,如果要求前k个较大的数,我们就建立小堆,遇到比根部大的数据就将堆顶删除,然后将这个数据入堆,调整来保持小堆的结构

通过遍历插入和调整逐渐淘汰较小的数据,最后堆中的数据就是前k个较大的数据。

为了更好的测试,我们随机生成大量数据求其中的前k个较小数。

代码:

void topk()
{
	HP hp;
	//堆初始化
	HeapInit(&hp);
	//随机生成一万个数
	int n = 10000;
	//找前五个数
	int k = 5;
	int* a = (int*)malloc(sizeof(int) * n);
	if (a == NULL)
	{
		printf("malloc error\n");
		exit(-1);
	}
	srand(time(0));
	for (int i = 0; i < n; i++)
	{
		//随机生成500到1000的数据
		a[i] = rand() % (500) + 500;
	}
	for (int i = 0; i < k; i++)
	{
		//把数组前面几个数拿过来作堆
		HeapPush(&hp, a[i]);
	}
	//为了方便我们观察,我们设置5个小于500的数据
	a[100] = 423;
	a[888] = 55;
	a[999] = 450;
	a[887] = 478;
	a[56] = 256;
	for (int i = k; i < n; i++)
	{
		//如果a[i]比堆顶小,删除堆顶,然后入堆
		if (a[i] < HeapTop(&hp))
		{
			HeapPop(&hp);
			HeapPush(&hp, a[i]);
		}
	}
	//遍历调整结束,最后堆中元素为最小的前5个
	HeapPrint(&hp);
}

堆的实际应用(topk问题以及堆排序)

这个方式的优点:

(1)只需要建立存储k个数据的堆,空间消耗小

(2)因为是一个个数据进行遍历,可以把数据存储在磁盘中,从磁盘中读取数据

二:堆排序

【1】第一种方法(很少用)

(1)我们可以建立一个堆,把数据存储在堆中

(2)堆的物理结构是数组,我们可以把根部节点(最大的数据)和最后一个叶子节点交换,将size(堆中的有效数据)减1。

(3)再进行调整来保持堆的结构,一直到size变成1

图解(以大堆为例,怎么调整的大家可以看前一期):

堆的实际应用(topk问题以及堆排序)

 代码:

void HeapSort1(int* arr, int n)
{
	//建堆
	HP hp;
	HeapInit(&hp);
	for (int i = 0; i < n; i++)
	{
		HeapPush(&hp, arr[i]);
	}
	//排序
	while (hp.size > 1)
	{
		HeapSwap(&hp.a[0], &hp.a[hp.size - 1]);
		hp.size--;
		AdjustDown(hp.a, hp.size, 0);
	}
	for (int i = 0; i < n; i++)
	{
		printf("%d ", hp.a[i]);
	}
}

int main()
{
	int arr[20] = { 78,5,8,9,7,44,55,66,99,458,41,20,0,777,458,994,2,57,7789,956 };
	HeapSort1(arr, sizeof(arr) / sizeof(arr[0]));
}

堆的实际应用(topk问题以及堆排序)

缺点:

(1)消耗了额外的空间来建堆。

(2)这个方式几乎要用到堆的所有功能,再使用前要写大量的接口

【2】第二种方法(很实用)

(1)把数组原地调整成堆

这里有两种调整思路:

①自下而上进行调整(以大堆为例)

图解:

堆的实际应用(topk问题以及堆排序)

 时间复杂度分析:

堆的实际应用(topk问题以及堆排序)

②自上而下调整(以大堆为例)

图解:

堆的实际应用(topk问题以及堆排序)

 时间复杂度分析:

堆的实际应用(topk问题以及堆排序)

(2)进行排序(排序的时间复杂度和自上而下调整一致,计算思路也一致)

排序的思路和第一种方法一致

①可以把根部节点(最大的数据)和最后一个叶子节点交换,将n(堆中的有效数据)减1。

②再进行调整来保持堆的结构,一直到n变成1

代码:

//堆排序
void HeapSort2(int*a,int n)
{
	//调整成堆
	int parent = (n - 1) / 2;
	while (parent>=0)
	{
		AdjustDown(a, n, parent);
		parent--;
	}
	//进行堆排序
	while (n>1)
	{
		HeapSwap(&a[0], &a[n - 1]);
		n--;
		AdjustDown(a, n, 0);
	}
}

int main()
{
	int arr[] = {300,578,65,78,5,8,9,7,44,55,66,99,458,41,20,0,777,458,994,2,57,7789,956 };
	HeapSort2(arr, sizeof(arr) / sizeof(arr[0]));
	for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
	{
		printf("%d ", arr[i]);
	}
}

堆的实际应用(topk问题以及堆排序)

这个方法的优点:

(1)原地调整,不需要消耗额外的空间

(2)只需要用到调整的接口,代码量大大减少。

堆排序的时间复杂度分析

(1)如果调整选择自上而下,整个排序时间复杂度为O(2*N*log2(N))=O(N*log2(N))。

(2)如果调整选择自下而上,整个排序时间复杂度为O(N*log2(N)+N)O(N*log2(N))。

综上所述,堆排序是一个时间复杂度为O(N*log2(N))的算法

这个时间复杂度相较于冒泡是一个很大的提升。

堆的实际应用(topk问题以及堆排序)

堆的实际应用(topk问题以及堆排序)文章来源地址https://www.toymoban.com/news/detail-427828.html

到了这里,关于堆的实际应用(topk问题以及堆排序)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构学习分享之堆的详解以及TopK问题

    💓博主CSDN主页:杭电码农-NEO💓   ⏩专栏分类:数据结构学习分享⏪   🚚代码仓库:NEO的学习日记🚚   🌹关注我🫵带你了解更多数据结构的知识   🔝🔝 本章就给大家带来久违的堆的知识,如果你还不知道数的相关知识,或者什么是完全二叉树,请跳转 树的介绍, 本章的堆结

    2024年02月05日
    浏览(108)
  • 【数据结构】深度剖析最优建堆及堆的经典应用 - 堆排列与topk问题

    🚩 纸上得来终觉浅, 绝知此事要躬行。 🌟主页:June-Frost 🚀专栏:数据结构 🔥该文章分别探讨了向上建堆和向下建堆的复杂度和一些堆的经典应用 - 堆排列与topk问题。 ❗️该文章内的思想需要用到实现堆结构的一些思想(如向上调整和向下调整等),可以在另一篇文章

    2024年02月08日
    浏览(48)
  • 玩转堆排序以及Topk问题——【数据结构】

    W...Y的主页 😊 代码仓库分享  💕 目录 堆排序  建堆  建堆的时间复杂度 Topk问题 学习了二叉树以及堆,今天我们来学习一下什么是堆排序以及经典二叉树问题——topk问题。 在学习开始我们先来回顾一下上篇博客中我们提到的堆,在实现堆时我们要进行向上调整或向下调

    2024年02月07日
    浏览(39)
  • 数据结构:手撕图解堆的实现和TopK的应用

    要讲到堆,先要说两个关于二叉树的概念 满二叉树:一个二叉树如果每一层的节点数都是最大值,那么这个二叉树就是满二叉树 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是满二叉树的变形,对于深度为k的树有n个节点的二叉树,当且仅当其每一个节点都与

    2024年02月15日
    浏览(48)
  • 数据结构---手撕图解堆的实现和TopK的应用

    要讲到堆,先要说两个关于二叉树的概念 满二叉树:一个二叉树如果每一层的节点数都是最大值,那么这个二叉树就是满二叉树 完全二叉树:完全二叉树是效率很高的数据结构,完全二叉树是满二叉树的变形,对于深度为k的树有n个节点的二叉树,当且仅当其每一个节点都与

    2024年02月16日
    浏览(38)
  • 【数据结构】堆的实现,堆排序以及TOP-K问题

    目录 1.堆的概念及结构 2.堆的实现 2.1初始化堆 2.2销毁堆 2.3取堆顶元素 2.4返回堆的大小 2.5判断是否为空 2.6打印堆 2.7插入元素 2.8堆的向上调整 2.9弹出元素 2.10堆的向下调整 3. 建堆时间复杂度 4. 堆的应用 4.1 堆排序 4.2 TOP-K问题 堆是一种数据结构,它是由一组元素组成的,并

    2024年02月12日
    浏览(52)
  • 堆的应用(堆排序、TOP - K问题)

    🍎  时间复杂度: 🥝  堆排序的最坏时间复杂度为 :O(n*lg(n)) 🥝  TOP - K问题的最坏时间复杂度为:O(n*lg(k))     🍁前面我们学习了二叉树、以及堆的结构,也用顺序表的结构成功的把堆的结构一步一步的敲出来了。IT公司的吉祥“树” 二叉树-(堆)C语言创建_硕硕C语言的

    2024年02月06日
    浏览(43)
  • 【数据结构之二叉树简介·顺序存储·应用:堆·堆排序·TOPK问题】

    ​ 🕺作者: 迷茫的启明星 😘欢迎关注:👍点赞🙌收藏✍️留言 🎃相关文章 【数据结构从0到1之树的初识】 【数据结构】带你学会二叉树的链式存储的前中后序遍历,遍历推导及利用队列实现二叉树的层次遍历。 🏇家人们,码字不易,你的👍点赞🙌收藏❤️关注对我

    2024年02月01日
    浏览(37)
  • 数据结构-堆的实现及应用(堆排序和TOP-K问题)

    1.堆的知识点: 下面我们通过一张图片来更加深刻地理解堆 上面我们说过,堆的物理结构是一个数组,逻辑结构是一个完全二叉树,所以堆的实际结构类似于顺序表,只不过我们的处理方式类似于二叉树 那么我们就可以用顺序表那样的结构来表示堆了 于是我们可以写出这样的代码

    2024年02月07日
    浏览(52)
  • 经典TopK问题、优先级队列 与 堆的纠葛一文为你解惑——数据结构

    前言: 本篇文章以 TopK 问题为引,具体阐述了 PriorityQueue 实现的基本逻辑——堆 数据结构,以及PriorityQueue 的常用方法。如有问题欢迎看官朋友指正,如果觉得文章还不错的话,求点赞、收藏、评论 三连。 重点: 堆的基本实现逻辑 PriorityQueue 运用和源码分析 TopK 问题的解法

    2023年04月22日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包