堆排序详解

堆排序

  • 一.前言
  • 二.堆排序思路
  • 三.堆的创建
    • 1.堆的向上调整
    • 2.堆的向下调整
    • 3.向上建堆
    • 4.向下建堆
    • 5.两种建堆方式比较
  • 四.堆排序
  • 五.复杂度分析
  • 六.Topk问题
  • 七.结语

一.前言

堆排序在生活中主要有两大应用场景:一是大数据排序,二是优先队列。其中典型的实例就是解决Topk问题。

二.堆排序思路

请添加图片描述
现在假设我们已经建好了一个有11个元素的大堆(该堆各个元素的编号从上到下从左到右依次为0,1,2,3……10),那么堆顶元素(编号为0)一定比剩下的10个元素(1到10)都大,首先我们把堆顶元素0号与最后一个元素10号交换,那么我们就将最大的元素放到末尾了,接着我们只需要再将0-9号再调整为大堆,再进行一次首尾交换(注意此时的尾是9号元素),那么9,10号元素分别变为次大和最大的了,依此不断进行直至堆没有元素,就成功得到一个升序序列(即升序建大堆,降序建小堆)。
所以堆排序核心在于堆的创建和调整。

三.堆的创建

这里提前说明一下堆是通过堆的调整创建起来的,所以在创建堆之前我们得先知道堆的调整。

1.堆的向上调整

所谓向上调整,其前提是该元素编号前面的元素都是堆(都是大堆或小堆,这里以小堆为例),现在我们要把该元素及其前面的元素都调整为小堆。
请添加图片描述

如上图所示,7号之前的元素都是小堆,但这8个元素却不是小堆,这时我们只需要找到该元素的父亲,如果该元素小于其父亲,就进行交换,直至该元素大于等于其父亲节点该元素已经调整为堆顶。这样就一定可以保证这8个元素都是小堆。

void AdjustUp(HPDataType* a, int child)
{int parent = (child - 1) / 2;//寻找该元素的父亲,注意堆顶元素编号是0while (child > 0){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (parent - 1) / 2;}else{break;}}
}

2.堆的向下调整

所谓向下调整,其前提是该元素的左子堆和右子堆都是堆(都是大堆或小堆,这里以大堆为例),但以该元素为堆顶的堆却不是大堆,只需要把该元素向下面进行调整就可以创建创建出一个大堆来。
请添加图片描述
如上图所示,刚开始堆顶的左子堆和右子堆都是大堆,但整个堆不是大堆,要把0到9号元素进行堆的调整为大堆,只需要从其左右两个孩子中挑选出大数的与其进行一次交换,依此不断进行下去,直到要调整的数均大于其左右孩子要调整的数已经是叶子节点为止。

void HeapAdjustDown(int* a, int size, int parent)
{assert(a);int leftChild = parent * 2 + 1;int rightChild = leftChild + 1;while (leftChild < size){//找较大的孩子int maxChild = leftChild;if (rightChild<size && a[leftChild] < a[rightChild]){maxChild = rightChild;}if (a[parent] < a[maxChild]){Swap(&a[parent], &a[maxChild]);//交换两数}else{break;}parent = maxChild;leftChild = parent * 2 + 1;rightChild = leftChild + 1;}
}

3.向上建堆

现在任意给我们一个整型数组,在我们看来这就是一个乱堆,但我们想要将其建成一个大堆,我们可以把第一个元素(编号为0)看成一个堆,该堆只有一个元素,那么1号元素之前的元素都是大堆,现在我们只需要对1号元素进行一次向上调整那么0号和1号元素就构成一个新的大堆了,接着又可以对2号元素进行向上调整,依此进行下去,直到最后一个元素也进行向上调整,至此,一个大堆就建立好了。

void HeapCreate(int* hp, int size)
{assert(hp);for (int i = 1; i < size; ++i){HeapAdjustUp(hp, i);}
}

4.向下建堆

现在还是任意给我们一个整型数组,在我们看来他还是一个乱堆,但我们也希望将其建成一个大堆,那么我们可以把每一个叶子节点都看做是大堆,从最后一个非叶子节点开始,向下建堆,直至到根节点,至此,一个大堆就建成了。
在这里插入图片描述
如上图:先将4号建成大堆,再将3号建成大堆,依此类推,直至0号。

void HeapCreate(int*hp,int size)
{assert(hp);for (int i = (size - 1) / 2; i >= 0; i--){HeapAdjustDown(hp, size, i);}
}

5.两种建堆方式比较

两种建堆方式空间复杂度度均为O(1),下面我们以满二叉树来看时间复杂度:
向上调整:

在这里插入图片描述

向下调整:
在这里插入图片描述
因此我们优先采用向下调整的方式建堆

四.堆排序

现在我们已经建好了一个11个元素的大堆,接下来要考虑如何排序了。
我们可以将堆的首尾互换,即先把堆顶元素0号与最后一个元素10号交换,那么我们就将最大的元素放到末尾了,接着我们只需要用向下调整的方法再把0-9号元素调整为大堆,再进行一次首尾交换(注意此时的尾是9号元素),同样再把0到8号进行向下调整,那么9,10号元素分别变为次大和最大的了,依此不断进行直至需要进行向下调整的元素个数为0,就成功得到一个升序序列。

void HeapSort(int* hp, int size)
{HeapCreate(hp, size);//创建堆while (size--){Swap(&hp[0], &hp[size]);//首尾交换HeapAdjustDown(hp, size, 0);}
}

至此,堆排序就算完成了。

下面给出以上完整代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<assert.h>//交换两个数
void Swap(int* num1, int* num2)
{int temp = *num1;*num1 = *num2;*num2 = temp;
}void HeapAdjustUp(int * a, int child)
{int parent = (child - 1) / 2;//寻找该元素的父亲,注意堆顶元素编号是0while (child > 0){if (a[child] > a[parent]){Swap(&a[child], &a[parent]);child = parent;parent = (parent - 1) / 2;}else{break;}}
}void HeapAdjustDown(int* a, int size, int parent)
{assert(a);int leftChild = parent * 2 + 1;int rightChild = leftChild + 1;while (leftChild < size){int maxChild = leftChild;if (rightChild<size && a[leftChild] < a[rightChild]){maxChild = rightChild;}if (a[parent] < a[maxChild]){Swap(&a[parent], &a[maxChild]);}else{break;}parent = maxChild;leftChild = parent * 2 + 1;rightChild = leftChild + 1;}
}//向下调整建堆
void HeapCreate(int*hp,int size)
{assert(hp);for (int i = (size - 1) / 2; i >= 0; i--){HeapAdjustDown(hp, size, i);}
}//向上调整建堆
//void HeapCreate(int* hp, int size)
//{
//	assert(hp);
//	for (int i = 1; i < size; ++i)
//	{
//		HeapAdjustUp(hp, i);
//	}
//}void HeapSort(int* hp, int size)
{HeapCreate(hp, size);while (size--){Swap(&hp[0], &hp[size]);HeapAdjustDown(hp, size, 0);}
}void Print(int* hp, int size)
{int i = 0;for (i = 0; i < size; ++i){printf("%d ", hp[i]);}
}int main()
{int hp[10] = { 9,5,4,1,3,2,8,7,6,10 };HeapSort(hp, 10);Print(hp, 10);return 0;
}

五.复杂度分析

在这里插入图片描述
故堆排序
时间复杂度为为O(NlogN)
空间复杂度为O(1)

六.Topk问题

假设现在我有几亿条数据,他们存放在磁盘中,我需要找出他们前K个数据(这K条数据可以放在内存中排序),由于内存大小有限,无法在内存中直接对这几亿条数据进行排序,这个问题就会变得非常棘手,但堆排序却很好的解决了这个问题。
假设我现在想要从存放在磁盘里的三亿个整数中找出最大的前100个数,我可以在内存中先建立一个100个元素的小堆,然后依此从磁盘里面读取数据,如果读取的整数比堆顶元素大,就将堆顶元素替换成读取的数据,再进行一次向下调整,以此类推,直至数据读取完毕,那留在堆里面的元素就是最大的前100个数。

一个数据最坏情况下要进行logK次调整,最坏情况下N个数据都要进行调整,故:
时间复杂度:O(NlogK)
空间复杂度:O(K)

下面是代码:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<time.h>void CreateData()
{// 造数据int n = 10000;srand(time(0));const char* file = "data.txt";FILE* fin = fopen(file, "w");if (fin == NULL){perror("fopen error");return;}for (size_t i = 0; i < n; ++i){int x = rand() % 1000000;fprintf(fin, "%d\n", x);}fclose(fin);
}void Swap(int* num1, int* num2)
{int temp = *num1;*num1 = *num2;*num2 = temp;
}void HeapAdjustDown(int* a, int size, int parent)
{assert(a);int leftChild = parent * 2 + 1;int rightChild = leftChild + 1;while (leftChild < size){int minChild = leftChild;if (rightChild<size && a[leftChild] > a[rightChild]){minChild = rightChild;}if (a[parent] > a[minChild]){Swap(&a[parent], &a[minChild]);}else{break;}parent = minChild;leftChild = parent * 2 + 1;rightChild = leftChild + 1;}
}int* HeapCreate(FILE*fout,int k)
{assert(fout);int* hp = (int*)malloc(sizeof(int) * k);if (NULL == hp){perror("HeapCreat");exit(-1);}int i = 0;for (i = 0; i < k; ++i){fscanf(fout, "%d", &hp[i]);}for (i = (k - 1) / 2; i >= 0; i--){HeapAdjustDown(hp, k, i);}return hp;
}void PrintTopK(int k)
{FILE* fout = fopen("data.txt", "r");if (NULL == fout){perror("fopen");exit(-1);}int* hp = HeapCreate(fout, k);int x = 0;while (EOF!=fscanf(fout, "%d", &x)){if (x > hp[0]){hp[0] = x;HeapAdjustDown(hp, k, 0);}}while (k--){printf("%d ", hp[k]);}free(hp);fclose(fout);
}int main()
{//CreateData();PrintTopK(5);return 0;
}

七.结语

堆的创建优先选用向下调整建堆,无论是从空间复杂度还是时间复杂度来说,堆排序的性能都是非常不错的。以上就是本文的全部内容了,如果本文有什么不对的地方,恳请指正,当然了,如果你觉得本文对你有所帮助,记得点赞哟!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/149424.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

【算法与数据结构】归并排序的代码实现(详细图解)以及master公式的讲解

目录 1、归并排序 1.1、算法描述 1.2、图解说明 2、代码实现 3、master公式 3.1、公式以及结论 3.2、适用于某些特殊的递归 3.3、计算归并排序的时间复杂度 1、归并排序 归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用递归或者说是分治法&#xff08;Di…

JAVA学习(5)-全网最详细~

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

第八章 Linux文件系统权限

目录 8.1 文件的一般权限 1.修改文件或目录的权限---chmod命令 2.对于文件和目录&#xff0c;r&#xff0c;w&#xff0c;x有不同的作用&#xff1a; 3.修改文件或目录的所属主和组---chown,chgrp 8.2 文件和目录的特殊权限 三种通过字符描述文件权限 8.3 ACL 权限 1.A…

redis高可用(主从复制,哨兵,集群)

目录 一、主从复制&#xff1a; 1.主从复制介绍&#xff1a; 2.主从复制的作用&#xff1a; 3.主从复制流程&#xff1a; 4.搭建Redis 主从复制&#xff1a; 4.1 环境准备&#xff1a; 4.2 安装redis&#xff1a; 4.3 master节点修改 Redis 配置文件&#xff1a; 4.4 slave节点…

JAVA面经整理(7)

一)什么是AQS&#xff1f; 1)AQS也被称之为是抽象同步队列&#xff0c;它是JUC包底下的多个组件的底层实现&#xff0c;Lock&#xff0c;CountDownLatch和Semphore底层都使用到了AQS AQS的核心思想就是给予一个等待队列和同步状态来实现的&#xff0c;它的内部使用一个先进先出…

【C语言】循环结构程序设计(第二部分 -- 习题讲解)

前言:昨天我们学习了C语言中循环结构程序设计&#xff0c;并分析了循环结构的特点和实现方法&#xff0c;有了初步编写循环程序的能力&#xff0c;那么今天我们通过一些例子来进一步掌握循环程序的编写和应用。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &am…

提示msvcp140.dll丢失的5个解决方法,msvcp140.dll丢失问题全面分析

在我们的日常生活和工作中&#xff0c;电脑已经成为不可或缺的工具。然而&#xff0c;在使用电脑的过程中&#xff0c;我们经常会遇到各种问题&#xff0c;其中就包括提示 msvcp140.dll 丢失的问题。msvcp140.dll 是 Visual C Redistributable for Visual Studio 2015 的运行时…

动态内存管理<C语言>

✨Blog&#xff1a;&#x1f970;不会敲代码的小张:)&#x1f970; &#x1f251;推荐专栏&#xff1a;C语言&#x1f92a;、Cpp&#x1f636;‍&#x1f32b;️、数据结构初阶&#x1f480; &#x1f4bd;座右铭&#xff1a;“記住&#xff0c;每一天都是一個新的開始&#x1…

微信小程序代驾系统源码(含未编译前端,二开无忧) v2.5

简介&#xff1a; 如今有越来越多的人在网上做代驾&#xff0c;打造一个代驾平台&#xff0c;既可以让司机增加一笔额外的收入&#xff0c;也解决了车主酒后不能开发的问题&#xff0c;代驾系统基于微信小程序开发的代驾系统支持一键下单叫代驾&#xff0c;支持代驾人员保证金…

Python的NumPy库(一)基础用法

NumPy库并不是Python的标准库&#xff0c;但其在机器学习、大数据等很多领域有非常广泛的应用&#xff0c;NumPy本身就有比较多的内容&#xff0c;全部的学习可能涉及许多的内容&#xff0c;但我们在这里仅学习常见的使用&#xff0c;这些内容对于我们日常使用NumPy是足够的。 …

2023.10.5 文件操作IO 经典例题

目录 例题一 例题二 例题一 扫描指定目录&#xff0c;并找到名称中包含指定字符的所有普通文件&#xff08;不包含目录&#xff09;&#xff0c;并且后续询问用户是否删除该文件 代码如下&#xff1a; package io;import java.io.File; import java.util.Scanner;//扫描指定目…

RSA攻击:模数分解

目录 一、模数分解总览 1.1直接分解法 1.2费马分解与Pollard_rho分解 1.3公约数分解 1.4其他模数分解 二、实战特训 2.1[黑盾杯 2020]Factor 2.2[GWCTF 2019]babyRSA 2.3[LitCTF 2023]yafu (中级) 2.4[RoarCTF 2019]RSA 2.5[CISCN 2022 西南]rsa 三、总结 一、模数分解总览 …

使用idea 中的rest 将 git 合并部分分支代码到主分支

需求&#xff1a;当要将dev的分支中的部分代码合并到test分支时&#xff0c;又不想把dev的全部代码合并到test分支 例如dev分支已经提交了 demo1到4&#xff0c;到想把demo1-3的代码合并到test分支&#xff0c;demo4暂时不合并 可以使用idea的reset 功能满足以上需求 1首先切…

Seata 源码篇之AT模式启动流程 - 中 - 03

Seata 源码篇之AT模式启动流程 - 中 - 03 数据源代理会话代理锁定查询执行器本地事务提交本地事务回滚 更新执行器删除执行器插入执行器 小节 本系列文章: Seata 源码篇之核心思想 - 01Seata 源码篇之AT模式启动流程 - 上 - 02 数据源代理 当我们的数据源被代理后&#xff0c…

.Net开源迁移框架FluentMigrator的使用。

在实际的开发过程中&#xff0c;经常会遇到数据库结构变动&#xff0c;比如新增表、删除表&#xff1b;已有的表新增字段&#xff0c;删除字段&#xff1b;修改字段属性等等。而且需要开发环境、测试环境和生产环境进行同步。如果使用的是EF&#xff0c;还是挺方便的。而非EF环…

PyTorch入门之【tensor】

目录 tensor的创建tensor的相关信息tensor的运算 tensor的创建 1.手动创建 import torch test1torch.tensor([1,2,3])#一维时为向量 test2torch.tensor([[1,2,3]])#二维时为矩阵 test3torch.tensor([[[1,2,3]]])#三维及以上统称为tensor print(test1) print(test2) print(tes…

【Linux】线程详解完结篇——信号量 + 线程池 + 单例模式 + 读写锁

线程详解第四篇 前言正式开始信号量引例信号量的本质信号量相关的四个核心接口生产消费者模型用环形队列实现生产者消费者模型基于环形队列的生产消费模型的原理代码演示单生产者单消费者多生产者多消费者 计数器的意义 线程池基本概念代码 单例模式STL,智能指针和线程安全STL中…

文心一言 VS 讯飞星火 VS chatgpt (106)-- 算法导论10.1 4题

四、用go语言&#xff0c;重写 ENQUEUE 和 DEQUEUE 的代码&#xff0c;使之能处理队列的下溢和上溢。 文心一言&#xff1a; 在 Go 语言中&#xff0c;我们可以使用切片来实现队列。下面是如何实现 ENQUEUE 和 DEQUEUE 操作&#xff0c;同时处理队列的下溢和上溢的情况&#…

git提交代码的流程

1.拉取代码 当你进入了一家公司就需要拉去公司的代码进行开发,此时你的项目小组长会给你个地址拉代码, git clone 公司项目的地址 此时如果不使用了这个方式拉去代码,拉去的是master分支上的代码,但是很多数的情况下&#xff0c;公司的项目可能会在其它的分支上,因此到公…