【数据结构与算法】堆排序(向下和向上调整)、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;
}


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

相关文章
|
1月前
|
算法 搜索推荐
数据结构与算法学习十八:堆排序
这篇文章介绍了堆排序是一种通过构建堆数据结构来实现的高效排序算法,具有平均和最坏时间复杂度为O(nlogn)的特点。
70 0
数据结构与算法学习十八:堆排序
|
1月前
|
算法 搜索推荐
算法之堆排序
本文介绍了堆排序算法的原理和实现,通过构建最大堆或最小堆,利用堆的性质进行高效的排序,并提供了具体的编程实现细节和示例。
20 0
算法之堆排序
|
1月前
|
算法 Java Go
深入了解堆排序算法
深入了解堆排序算法
22 1
|
4月前
|
存储 算法
【数据结构】堆,堆的实现,堆排序,TOP-K问题
数据结构——堆,堆的实现,堆排序,TOP-K问题
48 1
【数据结构】堆,堆的实现,堆排序,TOP-K问题
|
3月前
|
搜索推荐 算法
【初阶数据结构篇】插入、希尔、选择、堆排序介绍(上篇)
堆排序(Heapsort)是指利⽤堆积树(堆)这种数据结构所设计的⼀种排序算法,它是选择排序的⼀ 种。它是通过堆来进⾏选择数据。需要注意的是排升序要建⼤堆,排降序建⼩堆。
19 0
|
3月前
|
算法
【初阶数据结构篇】堆的应用(堆排序与Top-K问题)
即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。
43 0
|
4月前
|
搜索推荐
【数据结构常见七大排序(二)】—选择排序篇【直接选择排序】And【堆排序】
【数据结构常见七大排序(二)】—选择排序篇【直接选择排序】And【堆排序】
|
15天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
90 9
|
6天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
15 1
|
9天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。