【数据结构与算法】堆排序(向下和向上调整)、TOP-K问题(超详细解读)(下)

简介: 【数据结构与算法】堆排序(向下和向上调整)、TOP-K问题(超详细解读)(下)

4.堆的应用


4.1堆排序

       这里前提说一下:当我们用向上调整或者向下调整算法建成一个小堆或者大堆时,这时候的小堆和大堆,不一定是有序的,因为堆跟有序之间还存在明显的界限。

以小堆为例子:

就比如说,要将 7,5,3,1,1,9,5,4 ,变成小堆的结果是: 1,1,5,4,3,9,5,7  , 并不是有序的

e54db5e6e23745a48222abbef7f36624.png

3caac0e0ff8c4320bb22ef119a7d62a0.png

那么堆排序,说到底还是一个排序,那么排序肯定是要将数据排成升序 / 降序,那么建小堆,要排成升序还是降序呢?

先来看排成升序的情况:1,1,5,4,3,9,5,7 -> 1,1,3,4,5,5,7,9

954713df016c40a7880be543701d5a29.png

所以小堆是要排成降序的


4.1.1堆排序的本质

堆排序正确思路是:

①先用向上调整或者向下调整,弄出一个小堆或者大堆。

②假定前面弄的是小堆,那么进入while循环,通过向下调整,那么这时候的小堆就会逐渐排成倒序。

如果这时候为大堆,通过向下调整,就会排成升序。

③依据题目的意图,可以轻易地选出最大或者最小的元素。


4.1.2向上调整建堆

那这时候我们就来看一下,先通过一次向上调整,

c8b795d9e93340e48b794e20f210d75b.png

排序:再通过向下调整,变成降序的例子(只演示了一遍的过程,因为篇幅太长了)

3002f20fa4c64d5ab7ca140b48a1c546.png

向上调整算法建堆的时间复杂度:O(N):F(N)= (N+1)*(log(N+1)-2)+ 2

85014f04ee584f4b9918f946a7ba2125.png

特别注意:向下调整(父节点下标是0)

a62833607f4043e9989b8e1cc67c15ed.png


4.1.3向下调整建堆(动图)

动图解析:

image.gif

向下调整的最终结果:

6e331b8c3bea4f97bfaae2018459dd6a.png

排序:

这个向下调整的排序结果,跟上面先向上调整,再经过向下调整的排序结果是一样的,跟上面的向下调整的排序思路也是一样的,只是刚开始数据的顺序不一样。

cad5a87ffc9d414182f406c0f5ae75ca.png

向下调整算法建堆的时间复杂度: O(N)=N - log(N+1)

b27b410f8b2b41a78f2bb6b52274f20a.png

     因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明( 时间复杂度本来看的是近似值,多几个节点不影响最终结果) 

f6819fbd89cd42498ec87036acc2b2d4.png

因此:建堆的时间复杂度为O(N)


4.2TOP-K问题

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

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

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

数值间的转换关系:

1G= 1024MB

1024MB = 1024*1024KB

1024*1024KB= 1024*1024*1024Byte 约等于10亿Byte

解决思路:

      把这个N建成大堆,PopK次,即可找出最大的前K个有些场景,但是有特殊情况上面的思路解决不了,比如N非常大,假设N是10亿,K是100,解决方法:数据多,数据存在磁盘文件中

具体步骤:

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

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

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

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


4.2.1生成随机数并写入文件

这段代码的目的是生成10000个0到999999之间的随机数,并将它们写入"data.txt"文件中,每个数占一行

void CreateNDate()
{
  // 造数据
  int n = 10000;//并将其赋值为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)
  {//rand()函数用于生成随机数,%操作符用于限制随机数的范围。
    int x = rand() % 1000000;//循环从0到n-1,每次迭代生成一个随机数x,范围在0到999999之间。
    fprintf(fin, "%d\n", x);//使用fprintf(fin, "%d\n", x)将随机数写入文件。
                  //fprintf()函数用于格式化输出,将随机数写入文件的新行。
  }
  fclose(fin);//使用fclose(fin)关闭文件,确保数据写入完成并释放相关资源。
}


执行:

生成10000个随机数并且范围在0~999999之间

1e723ea23ee6449f8094d3d35363af48.png

4.2.2建立小堆并比较元素进行合理替换

该函数的目的是从"data.txt"文件中读取数据,并按照从大到小的顺序打印出前k个最大的数。

void PrintTopK(int k)
{
  const char* file = "data.txt";//声明一个指向常量字符的指针file,并将其赋值为"data.txt"。这个变量表示要读取的文件名。
  FILE* fout = fopen(file, "r");//使用fopen(file, "r")函数以读取模式打开文件。如果文件打开失败,会输出错误信息并返回。
  if (fout == NULL)
  {
    perror("fopen error");
    return;
  }
  //使用malloc(sizeof(int) * k)函数动态分配一个能容纳k个整数的内存空间,
  int* kminheap = (int*)malloc(sizeof(int) * k);
  //返回的指针赋值给kminheap。如果内存分配失败,会输出错误信息并返回。
  if (kminheap == NULL)
  {
    perror("malloc error");
    return;
  }
  for (int i = 0; i < k; i++)
  {//使用循环从文件中读取前k个整数,并将它们存储在kminheap数组中。fscanf()函数用于从文件中读取格式化输入。
    fscanf(fout, "%d", &kminheap[i]);
  }
  // 建小堆
  for (int i = (k - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(kminheap, k, i);
  }
  int val = 0;//声明一个整数变量val,用来存储从文件中读取的下一个整数
  while (!feof(fout))//使用循环从文件中读取剩余的整数,并与小堆的根节点比较。
    //如果读取的整数大于小堆的根节点,则将其替换为根节点,并重新调整小堆。
  {
    fscanf(fout, "%d", &val);
    if (val > kminheap[0])
    {
      kminheap[0] = val;
      AdjustDown(kminheap, k, 0);
    }
  }
  //使用循环打印小堆中的元素,即前k个最大的数
  for (int i = 0; i < k; i++)
  {
    printf("%d ", kminheap[i]);
  }
  //最后,在打印完所有元素后,输出一个换行符
  printf("\n");
}


我们执行一下,看看情况如何:

a79301654485469f9b5ba67ce9aeac0c.png

可以看到,这些数据并不好一眼看出建的是小堆的数据,我们可以手动来验证一下,打开文本文件:

fdfb1d03292d4ec58392c4a55dcc790c.png

修改的数据明显一点,一眼就可以看出数据大小。修改的数据明显一点,一眼就可以看出数据大小。

6c591c3e384848a8b8ab735d3d4a725d.png

排序执行:

7499f190f7254cb4ae8ad338ca3baa3d.png


5.总代码


test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
#include<time.h>
//int main()
//{
//  HP hp;
//  HeapInit(&hp);
//  //int a[] = { 65,100,70,32,50,60 };
//  int b[] = { 100,90,80,70,60,50 };
//  for (int i = 0; i < sizeof(b) / sizeof(int); ++i)
//  {
//    HeapPush(&hp, b[i]);
//  }
//  while (!HeapEmpty(&hp))
//  {
//    int top = HeapTop(&hp);
//    printf("%d\n", top);
//    HeapPop(&hp);
//  }
//  return 0;
//}
//弊端:1.先有一个堆,太麻烦。2.空间复杂度+拷贝数据
//void HeapSort(int* a, int n)
//{
//  HP hp;
//  HeapInit(&hp);
//  //N * logN
//  for (int i = 0; i < n; i++)
//  {
//    HeapPush(&hp,a[i]);
//  }
//  //N * logN
//  int i = 0;
//  while (!HeapEmpty(&hp))
//  {
//    int top = HeapTop(&hp);
//    a[i++] = top;
//    HeapPop(&hp);
//  }
//  
//  HeapDestroy(&hp);
//}
//
//
//int main()
//{
//  int a[] = { 7,8,3,5,1,9,5,4 };
//  HeapSort(a, sizeof(a) / sizeof(int));
//
//  return 0;
//}
//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;
//  }
//}
//int main()
//{
//  int a[] = { 7,5,3,1,1,9,5,4 };
//  HeapSort(a, sizeof(a) / sizeof(int));
//
//  return 0;
//}
//
//
//这段代码的目的是生成10000个0到999999之间的随机数,并将它们写入"data.txt"文件中,每个数占一行
void CreateNDate()
{
  // 造数据
  int n = 10000;//并将其赋值为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)
  {//rand()函数用于生成随机数,%操作符用于限制随机数的范围。
    int x = rand() % 1000000;//循环从0到n-1,每次迭代生成一个随机数x,范围在0到999999之间。
    fprintf(fin, "%d\n", x);//使用fprintf(fin, "%d\n", x)将随机数写入文件。
                  //fprintf()函数用于格式化输出,将随机数写入文件的新行。
  }
  fclose(fin);//使用fclose(fin)关闭文件,确保数据写入完成并释放相关资源。
}
//该函数的目的是从"data.txt"文件中读取数据,并按照从大到小的顺序打印出前k个最大的数。
void PrintTopK(int k)
{
  const char* file = "data.txt";//声明一个指向常量字符的指针file,并将其赋值为"data.txt"。这个变量表示要读取的文件名。
  FILE* fout = fopen(file, "r");//使用fopen(file, "r")函数以读取模式打开文件。如果文件打开失败,会输出错误信息并返回。
  if (fout == NULL)
  {
    perror("fopen error");
    return;
  }
  //使用malloc(sizeof(int) * k)函数动态分配一个能容纳k个整数的内存空间,
  int* kminheap = (int*)malloc(sizeof(int) * k);
  //返回的指针赋值给kminheap。如果内存分配失败,会输出错误信息并返回。
  if (kminheap == NULL)
  {
    perror("malloc error");
    return;
  }
  for (int i = 0; i < k; i++)
  {//使用循环从文件中读取前k个整数,并将它们存储在kminheap数组中。fscanf()函数用于从文件中读取格式化输入。
    fscanf(fout, "%d", &kminheap[i]);
  }
  // 建小堆
  for (int i = (k - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(kminheap, k, i);
  }
  int val = 0;//声明一个整数变量val,用来存储从文件中读取的下一个整数
  while (!feof(fout))//使用循环从文件中读取剩余的整数,并与小堆的根节点比较。
    //如果读取的整数大于小堆的根节点,则将其替换为根节点,并重新调整小堆。
  {
    fscanf(fout, "%d", &val);
    if (val > kminheap[0])
    {
      kminheap[0] = val;
      AdjustDown(kminheap, k, 0);
    }
  }
  //使用循环打印小堆中的元素,即前k个最大的数
  for (int i = 0; i < k; i++)
  {
    printf("%d ", kminheap[i]);
  }
  //最后,在打印完所有元素后,输出一个换行符
  printf("\n");
}
int main()
{
  //CreateNDate();
  PrintTopK(5);
  return 0;
}


Heap.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
typedef int HPDataType;
typedef struct Heap
{
  HPDataType* a;
  int size;//有效数据个数
  int capacity;//容量
}HP;
//向上调整
void AdjustUp(HPDataType* a, int child);
//向下调整
void AdjustDown(int* a, int n, int parent);
//堆的初始化s
void HeapInit(HP* php);
// 堆的销毁
void HeapDestroy(HP* php);
// 堆的插入
void HeapPush(HP* PHP, HPDataType x);
// 堆的删除
void HeapPop(HP* php);
// 取堆顶的数据
HPDataType HeapTop(HP* php);
// 堆的判空
bool HeapEmpty(HP* php);
// 堆的数据个数
int HeapSize(HP* php);


Heap.c

void HeapInit(HP* php)
{
  assert(php);
  php->a = NULL;
  php->capacity = php->size = 0;
}
void HeapDestroy(HP* php)
{
  assert(php);
  free(php->a);
  php->a = NULL;
  php->capacity = php->size = 0;
}
void Swap(HPDataType* a1, HPDataType* a2)
{
  HPDataType tmp = *a1;
  *a1 = *a2;
  *a2 = tmp;
}
void Swap1(HPDataType* n1, HPDataType* n2)
{
  HPDataType tmp = *n1;
  *n1 = *n2;
  *n2 = tmp;
}
void Swap2(HPDataType* x1, HPDataType* x2)
{
  HPDataType tmp = *x1;
  *x1 = *x2;
  *x2 = tmp;
}
void AdjustUp(int* a, int child)//AdjustUp
{
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[child] < a[parent])//小堆< /大堆 >
    {
      Swap1(&a[child], &a[parent]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
void AdjustDown(int* a, int n, int parent)
{
  int child = parent * 2 + 1;
  while (child < n)
  {   //先判断是否越界的情况下,再判断两个孩子的大小;
    if (child + 1 < n && a[child] > a[child + 1])//假设左孩子小
    {
      child++;
    }
    if (a[child] < a[parent])
    {
      Swap2(&a[parent], &a[child]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
void HeapPush(HP* php, HPDataType x)
{
  assert(php);
  if (php->size == php->capacity)
  { //如果空间不够则扩容
    int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
    HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
    if (tmp == NULL)
    {
      perror("malloc fail\n");
      return;
    }
    php->a = tmp;
    php->capacity = newCapacity;
  }
  php->a[php->size] = x;
  php->size++;
  AdjustUp(php->a, php->size - 1);
}
void HeapPop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  Swap(&php->a[0], &php->a[php->size - 1]);
  php->size--;
  AdjustDown(php->a, php->size, 0);
}
HPDataType HeapTop(HP* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  return php->a[0];
}
bool HeapEmpty(HP* php)
{
  assert(php);
  return php->size == 0;
}
int HeapSize(HP* php)
{
  assert(php);
  return php->size;
}


   本篇文章到此结束,如有错误,欢迎更正,感谢来访!

相关文章
|
15小时前
|
移动开发 算法 前端开发
前端算法之堆排序
前端算法之堆排序
14 1
|
15小时前
【数据结构】二叉树-堆(top-k问题,堆排序,时间复杂度)
【数据结构】二叉树-堆(top-k问题,堆排序,时间复杂度)
17 4
|
15小时前
|
人工智能 搜索推荐 算法
sort-04-heap sort 堆排序算法详解
这是一个关于排序算法的系列文章摘要,包括了10篇关于不同排序算法的链接,如冒泡排序、快速排序、选择排序、堆排序等。堆排序是一种基于比较的排序算法,利用了近似完全二叉树的结构并满足最大堆或最小堆的性质。最大堆中,每个节点的值都大于或等于其子节点。文章详细解释了最大堆的概念、节点访问方式以及堆的操作,包括堆调整和堆排序的过程,并通过图解展示了堆排序的具体步骤。此外,还提供了一个Java实现的堆排序代码示例。整个排序系列来源于一个开源项目,读者可以通过链接查看完整内容。
sort-04-heap sort 堆排序算法详解
|
15小时前
|
人工智能 算法 搜索推荐
直接插入排序、希尔排序、直接选择排序、堆排序、冒泡排序——“数据结构与算法”
直接插入排序、希尔排序、直接选择排序、堆排序、冒泡排序——“数据结构与算法”
|
16小时前
|
算法
堆排序+TopK问题——“数据结构与算法”
堆排序+TopK问题——“数据结构与算法”
|
16小时前
|
存储 机器学习/深度学习 算法
初阶数据结构之---堆的应用(堆排序和topk问题)
初阶数据结构之---堆的应用(堆排序和topk问题)
|
16小时前
|
算法 搜索推荐 数据挖掘
【算法与数据结构】堆排序&&TOP-K问题
【算法与数据结构】堆排序&&TOP-K问题
|
16小时前
|
算法 数据安全/隐私保护 计算机视觉
基于二维CS-SCHT变换和LABS方法的水印嵌入和提取算法matlab仿真
该内容包括一个算法的运行展示和详细步骤,使用了MATLAB2022a。算法涉及水印嵌入和提取,利用LAB色彩空间可能用于隐藏水印。水印通过二维CS-SCHT变换、低频系数处理和特定解码策略来提取。代码段展示了水印置乱、图像处理(如噪声、旋转、剪切等攻击)以及水印的逆置乱和提取过程。最后,计算并保存了比特率,用于评估水印的稳健性。
|
15小时前
|
算法 计算机视觉
基于高斯混合模型的视频背景提取和人员跟踪算法matlab仿真
该内容是关于使用MATLAB2013B实现基于高斯混合模型(GMM)的视频背景提取和人员跟踪算法。算法通过GMM建立背景模型,新帧与模型比较,提取前景并进行人员跟踪。文章附有程序代码示例,展示从读取视频到结果显示的流程。最后,结果保存在Result.mat文件中。
|
15小时前
|
资源调度 算法 块存储
m基于遗传优化的LDPC码OMS译码算法最优偏移参数计算和误码率matlab仿真
MATLAB2022a仿真实现了遗传优化的LDPC码OSD译码算法,通过自动搜索最佳偏移参数ΔΔ以提升纠错性能。该算法结合了低密度奇偶校验码和有序统计译码理论,利用遗传算法进行全局优化,避免手动调整,提高译码效率。核心程序包括编码、调制、AWGN信道模拟及软输入软输出译码等步骤,通过仿真曲线展示了不同SNR下的误码率性能。
8 1