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


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

相关文章
|
3月前
|
算法 Python
数据结构算法--4堆排序
堆排序过程概述:建立大根堆,将堆顶最大元素移出并替换为末尾元素,调整保持堆性质,重复此过程直至堆为空,实现排序。时间复杂度为O(nlogn)。Python中可用heapq模块进行堆操作。
|
2月前
|
存储 算法
【数据结构】堆,堆的实现,堆排序,TOP-K问题
数据结构——堆,堆的实现,堆排序,TOP-K问题
31 1
【数据结构】堆,堆的实现,堆排序,TOP-K问题
|
24天前
|
搜索推荐 算法
【初阶数据结构篇】插入、希尔、选择、堆排序介绍(上篇)
堆排序(Heapsort)是指利⽤堆积树(堆)这种数据结构所设计的⼀种排序算法,它是选择排序的⼀ 种。它是通过堆来进⾏选择数据。需要注意的是排升序要建⼤堆,排降序建⼩堆。
|
24天前
|
算法
【初阶数据结构篇】堆的应用(堆排序与Top-K问题)
即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。
|
2月前
|
搜索推荐
【数据结构常见七大排序(二)】—选择排序篇【直接选择排序】And【堆排序】
【数据结构常见七大排序(二)】—选择排序篇【直接选择排序】And【堆排序】
|
3月前
|
搜索推荐 算法 Java
Java中的快速排序、归并排序和堆排序是常见的排序算法。
【6月更文挑战第21天】Java中的快速排序、归并排序和堆排序是常见的排序算法。快速排序采用分治,以基准元素划分数组并递归排序;归并排序同样分治,先分割再合并有序子数组;堆排序通过构建堆来排序,保持堆性质并交换堆顶元素。每种算法各有优劣:快排平均高效,最坏O(n²);归并稳定O(n log n)但需额外空间;堆排序O(n log n)且原地排序,但不稳定。
34 3
|
3月前
|
搜索推荐 算法
【C/排序算法】:堆排序和选择排序
【C/排序算法】:堆排序和选择排序
24 0
|
3月前
|
存储 算法 C语言
数据结构和算法——堆排序(选择排序、思路图解、代码、时间复杂度、堆排序及代码)
数据结构和算法——堆排序(选择排序、思路图解、代码、时间复杂度、堆排序及代码)
28 0
|
2天前
|
存储 C语言
数据结构基础详解(C语言): 栈与队列的详解附完整代码
栈是一种仅允许在一端进行插入和删除操作的线性表,常用于解决括号匹配、函数调用等问题。栈分为顺序栈和链栈,顺序栈使用数组存储,链栈基于单链表实现。栈的主要操作包括初始化、销毁、入栈、出栈等。栈的应用广泛,如表达式求值、递归等场景。栈的顺序存储结构由数组和栈顶指针构成,链栈则基于单链表的头插法实现。
|
3天前
|
Java
【数据结构】栈和队列的深度探索,从实现到应用详解
本文介绍了栈和队列这两种数据结构。栈是一种后进先出(LIFO)的数据结构,元素只能从栈顶进行插入和删除。栈的基本操作包括压栈、出栈、获取栈顶元素、判断是否为空及获取栈的大小。栈可以通过数组或链表实现,并可用于将递归转化为循环。队列则是一种先进先出(FIFO)的数据结构,元素只能从队尾插入,从队首移除。队列的基本操作包括入队、出队、获取队首元素、判断是否为空及获取队列大小。队列可通过双向链表或数组实现。此外,双端队列(Deque)支持两端插入和删除元素,提供了更丰富的操作。
10 0
【数据结构】栈和队列的深度探索,从实现到应用详解