堆排序讲解

简介: 堆排序讲解

前言

在讲堆的删除时,我们发现一步一步删除堆顶的数据,排列起来呈现出排序的规律,所以本节小编将带领大家进一步理解堆排序。

1.堆排序概念

那么什么是堆排序

堆排序(Heap Sort)是一种基于堆数据结构的排序算法。它利用堆的性质(大堆或小堆)进行排序操作。堆排序的基本思想是通过构建堆,将待排序的数组转化为一个符合堆性质的堆结构,然后不断将堆顶元素与堆的最后一个元素进行交换,并调整堆,使剩余元素继续满足堆的性质。重复这个过程,直到整个数组有序。

堆排序的步骤如下:

  1. 构建大堆或小堆:将待排序的数组视为一个完全二叉树,通过从最后一个非叶子节点开始,依次对每个节点进行向下调整(Adjustdown)操作,构建出一个大堆或小堆。这个过程确保了堆的性质:对于大堆,父节点的值大于等于其子节点的值;对于小堆,父节点的值小于等于其子节点的值。
  2. 排序:交换堆顶元素(最大值或最小值)与堆的最后一个元素,并将堆的大小减一。然后对堆顶元素进行向下调整,使剩余元素继续满足堆的性质。重复这个过程,直到堆的大小为1,即所有元素都已经排好序。(运用堆删除的思想
  3. 得到排序结果:经过上述步骤,数组中的元素就按照升序(从小到大)或降序(从大到小)排列了。

堆排序的时间复杂度为 O(nlogn),其中 n 是待排序数组的大小。它具有原地排序的特点,不需要额外的存储空间。

堆排序的优点是稳定性较好,适用于大规模数据的排序。然而,堆排序的缺点是相对较慢,尤其在快速排序等其他排序算法的应用场景中,堆排序的性能可能不如其他算法。

2.堆的建立方法

2.1向下调整建立堆(补充)

在这里,堆的建立有两种,在二叉树的顺序结构中提到一种建堆的方法,通过尾插再进行向上调整,不过时间复杂度为O(N*logN),这里提供新的建堆方法,通过向下调整法,时间复杂度为O(N),不过再用此调整方法时,左右子树要是堆的结构。即从倒数的第一个非叶子结点的子树开始调整,一直调整到根结点的树,就可以调整成堆。

假设给一个数组 int a[]={4,2,8,1,5,6,9,7,2,7,9},通过向下调整法制造大堆。

2.2向上调整法

通过比较新插入元素与其父节点的值来判断是否需要进行交换。如果新插入元素的值大于父节点的值,就将它们进行交换,并更新索引值。这样,逐步向上调整,直到新插入元素找到了合适的位置,保证了堆的性质。


//向上调整
void Adjustup(Datatype* a,int child) {
  int parent = (child - 1) / 2;
  while (child > 0) {
    if (a[child] > a[parent]) {
      Swap(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
      break;
  }
}

2.3建堆时间复杂度分析

1.向下调整法


void Adjustdown(Datatype* a, int n, int parent) {
  //假设法,假设左孩子大
  int child = parent * 2 + 1;
  
  while (child < n ) {
    if (child + 1 < n && a[child + 1] > a[child])
      child = child + 1;
    if (a[child] > a[parent]) {
      Swap(&a[child], &a[parent]);
        parent = child;
        child = parent * 2 + 1;
  }
    else break;
    
  }
}

2.向上调整法

向上调整法每层节点向上调整次数就是乘以层数


//向上调整
void Adjustup(Datatype* a,int child) {
  int parent = (child - 1) / 2;
  while (child > 0) {
    if (a[child] > a[parent]) {
      Swap(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
      break;
  }
}

注意:上述代码都是采用建大堆的代码,建小堆把部分大于符号改成小于。

因此向下调整法实质上是节点数少的层,调整次数越多,向上调整是节点数越多,调整次数越多。

3.排序建堆选择

升序:建大堆

降序:建小堆

每次将堆首元素与尾元素交换,然后向下调整,每交换一次,堆的大小要减一,因为我们是每次将最大或者最小的元素依次交换堆后面。

例如升序的一个过程如下图:

void HeapSort(int* a, int n)
{
  //降序,建小堆
  // 升序,建大堆
  //for (int i = 1; i < n; i++)
  //{
  //  Adjustup(a, i);
  //}
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    Adjustdown(a, n, i);
  }
 
  int end = n - 1;
  while (end > 0)
  {
    Swap(&a[0], &a[end]);
    Adjustdown(a, end, 0);
    --end;
  }
}
 
void TestHeap2()
{
  int a[] = {20,17,16,5,3,4 };
  HeapSort(a, sizeof(a) / sizeof(int));
  for (int i = 0; i < sizeof(a) / sizeof(int); i++) {
    printf("%d ", a[i]);
  }
}

4.TOP-K问题

TOP-K 问题:即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大

比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等。

对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能数据都不能一下子全部加载到内存中) 。最佳的方式就是用堆来解决,基本思路如下:

若数据量小,可以直接建立相应的堆,在pop。

数据量很大的时候,可以采用以下方法:

1. 用数据集合中前 K 个元素来建堆

前k个最大的元素,则建小堆

前k个最小的元素,则建大堆

2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素

将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素。

假如求一堆数中的前k个最小的数,则建大堆。

这里我们采用随机数来生成100个随机数,然后存入一个动态数组中,然后选出前10个最小的数。


void PrintTopK(int* a, int n, int k)
{
  // 1. 建堆--用a中前k个元素建堆
  for (int i = (k - 1 - 1) / 2; i >= 0; i--) {
    Adjustdown(a, k, i);
  }
  // 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
  for (int j = n - k; j < n; j++) {
    if (a[j] < a[0]) {
      a[0] = a[j];
      Adjustdown(a, k, 0);
    }
  }
  printf("最小前%d个数:", k);
  for (int i = 0; i < k; i++) {
    printf("%d ", a[i]);
  }
}
void TestTopk()
{
  int n = 100;
  int* a = (int*)malloc(sizeof(int) * n);
  srand(time(0));
  for (size_t i = 0; i < n; ++i)
  {
    a[i] = rand() % 100;
  }
  PrintTopK(a, n, 10);
  }

本节内容到此结束,谢谢各位友友的捧场,下节小编将带领大家继续了解二叉树的链式存储结构!!!

留下三连和评论吧!!!

目录
相关文章
|
存储 算法 PyTorch
FlashAttention2原理解析以及面向AIGC的加速实践
FlashAttention2原理解析以及面向AIGC的加速实践
1972 0
|
11月前
|
存储 算法 索引
二叉树的顺序结构(堆的实现)
二叉树的顺序结构(堆的实现)
79 1
|
11月前
|
测试技术
自动化测试项目实战笔记(三):测试用户注册(验证码错误,成功,出现弹框时处理)
本文是关于自动化测试项目实战笔记,主要介绍了如何测试用户注册功能,包括验证码错误、注册成功以及弹框处理的测试步骤和代码实现。
374 2
自动化测试项目实战笔记(三):测试用户注册(验证码错误,成功,出现弹框时处理)
|
8月前
|
算法 Java 编译器
深入理解 Java JDK —— 让我们从基础到进阶
JDK(Java Development Kit)是 Java 开发的核心工具包,包含编译、运行和调试 Java 程序所需的所有工具和库。它主要由 JVM(Java 虚拟机)、JRE(Java 运行时环境)和 Java 核心类库组成。JVM 是跨平台运行的基础,负责字节码的加载、执行和内存管理;JRE 提供运行 Java 应用的环境;核心类库则提供了丰富的 API 支持。通过编写、编译和运行一个简单的 Java 程序,可以深入理解 JDK 的工作原理。此外,JDK 还提供了 JIT 编译、垃圾回收优化和并发工具包等高级功能,帮助开发者提高程序性能和稳定性。
772 10
|
11月前
|
算法 搜索推荐
数据结构与算法学习十八:堆排序
这篇文章介绍了堆排序是一种通过构建堆数据结构来实现的高效排序算法,具有平均和最坏时间复杂度为O(nlogn)的特点。
265 0
数据结构与算法学习十八:堆排序
|
存储 缓存 编解码
Android经典面试题之图片Bitmap怎么做优化
本文介绍了图片相关的内存优化方法,包括分辨率适配、图片压缩与缓存。文中详细讲解了如何根据不同分辨率放置图片资源,避免图片拉伸变形;并通过示例代码展示了使用`BitmapFactory.Options`进行图片压缩的具体步骤。此外,还介绍了Glide等第三方库如何利用LRU算法实现高效图片缓存。
161 20
Android经典面试题之图片Bitmap怎么做优化
|
11月前
|
机器学习/深度学习 人工智能 自然语言处理
[大语言模型-论文精读] ACL2024-长尾知识在检索增强型大型语言模型中的作用
[大语言模型-论文精读] ACL2024-长尾知识在检索增强型大型语言模型中的作用
273 0
|
SQL 自然语言处理 安全
2024 年 8 月暨 ACL 2024 57篇代码大模型论文精选
2024年8月中旬,国际计算语言学大会ACL在泰国曼谷举行,展示了48篇代码大模型相关论文,包括24篇主会论文和24篇findings论文。主会论文涵盖XFT、WaveCoder、DolphCoder等创新方法,findings论文则探讨了代码注释增强、自动化程序修复等主题。此外,还额外整理了9篇8月最新代码大模型论文,涉及数据集合成、安全代码生成等多个前沿方向。欲了解更多,请访问我们的综述和GitHub项目。
1198 4
|
缓存 运维 前端开发
前端必备的运维知识点
【8月更文挑战第25天】前端必备的运维知识点
369 1
|
SQL 安全 数据挖掘
Elasticsearch如何聚合查询多个统计值,如何嵌套聚合?并相互引用,统计索引中某一个字段的空值率?语法是怎么样的?
Elasticsearch聚合查询用于复杂数据分析,包括统计空值率。示例展示了如何计算字段`my_field`非空非零文档的百分比。查询分为三步:总文档数计数、符合条件文档数计数及计算百分比。聚合概念涵盖度量、桶和管道聚合。脚本在聚合中用于动态计算。常见聚合类型如`sum`、`avg`、`date_histogram`等。组合使用可实现多值统计、嵌套聚合和空值率计算。[阅读更多](https://zhangfeidezhu.com/?p=515)
464 0
Elasticsearch如何聚合查询多个统计值,如何嵌套聚合?并相互引用,统计索引中某一个字段的空值率?语法是怎么样的?