【初阶数据结构】堆排序和TopK问题(下)

简介: 【初阶数据结构】堆排序和TopK问题

2-4完整代码

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef struct Heap
{
  int* a;
  int size;
  int capacity;
}HP;
void HeapInit(HP* php)
{
  assert(php);
  php->a = (int*)malloc(sizeof(HP) * 4);
  php->size = 0;
  php->capacity = 4;
}
void HeapDestory(HP* php)
{
  assert(php);
  php->a = NULL;
  php->size = php->capacity = 0;
}
void Swap(int* a, int* b)
{
  int temp = *a;
  *a = *b;
  *b = temp;
}
void AdjustUp(int* a, int child)
{ 
  assert(a);
  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;
    }
  }
}
//插入X后继续保持堆形态
void HeapPush(HP* php, int x)
{
  assert(php);
  if (php->size == php->capacity)
  {
    int newcapacity = 2 * php->capacity;
    int* temp = (int*)realloc(php->a, sizeof(int) * newcapacity);
    if (temp == NULL)
    {
      perror("realloc fail.\n");
      exit(-1);
    }
    php->a = temp;
    php->capacity = newcapacity;
  }
  php->a[php->size] = x;
  php->size++;
  //向上调整算法,传要调整的数组和从哪个下标child开始调
  AdjustUp(php->a, php->size - 1);
}
void HeapAdjustDown(int* a, int n, int parent)
{
  int minchild = 2 * parent + 1;
  while (minchild < n)
  {
    if (minchild + 1 < n && a[minchild] > a[minchild + 1])
    {
      minchild++;
    }
    if (a[minchild] < a[parent])
    {
      Swap(&a[minchild], &a[parent]);
      parent = minchild;
      minchild = 2 * parent + 1;
    }
    else
    {
      break;
    }
  }
}
//删除堆顶元素,找到次小或次大的元素
//删除之后仍要尽量保持堆的形态
void HeapPop(HP* php)
{
  assert(php);
  Swap(&php->a[0], &php->a[php->size - 1]);
  php->size--;
  HeapAdjustDown(php->a, php->size-1,0);
}
bool HeapEmpty(HP* php);
int 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;
}
void HeapPrint(HP* php)
{
  assert(php);
  for (int i = 0; i < php->size; i++)
  {
    printf("%d  ", php->a[i]);
  }
}
int main()
{
  int a[] = { 10,2,20,4,12,67,56,1 };
  int size = sizeof(a) / sizeof(a[0]);
  HP heap;
  HeapInit(&heap);
  for (int i = 0; i < size; i++)
  {
    HeapPush(&heap, a[i]);
    }
  HeapPop(&heap);
  HeapPrint(&heap);
  HeapDestory(&heap);
  return 0;
 }

3.两种方法建堆:


从上面我们可以知道:我们已经学会了建堆以及堆的插入删除数据。

但是我们知道我们建好的堆并不是有序的,而且堆中的数组和待的数组还不是同一个数组,这就意味着如果要使待排序的数组有序的话,还得将堆中的数据通过heapTop函数和HeapPop函数不断先取出堆顶元素插入到待排序数组,后删除堆顶元素(向下调整法)....


739683730a58417cb60b744691b92989.png


最重要的话这样的话还会导致我们使用额外的空间来拷贝待排序的数组来建堆


因此问题来了:怎么将数组本身建立成一个堆,从而减少额外空间的开辟

如果随便给你一个数组,元素向后顺序随机,要你把这个数组建成一个小根堆.(比如 14, 12, 4, 3, 6, 68, 21, 2 )


3-1向上调整法建堆

向上调整法的使用前提:每插入一个元素前,原数组的逻辑二叉树必须是一个小根堆(大根堆).

那么我们可以把14默认为是一个符合前提的堆,然后从12往后不断向数组中插入元素,并不断向上调整,直至把整个数组元素全部插完,即完成堆的建立.



27ce2ac54f2c487d8700bf97dac5da96.png

  //向上调整法建堆
  for (int i = 0; i < n; i++)
  {
    AdjustUp(a, i);
  }

8238af5cd7514a88a15daa17be1d299e.png

3-2向下调整法建堆

向下调整法使用的前提:左子树和右子树必须是小根堆(大根堆)


由于排序的数组是随机给的,所以对于堆顶元素来说,其左子树和右子树大大部分都不是小根堆(大根堆),所以不能从第一个数组元素(堆顶)开始向下调整;同时,叶子节点不需要向下调整,所以我们采用从倒数第一个非叶子节点开始向下调整(当然如果代码中写的是从叶子节点开始向下调整,结果也没有问题,但是就是多次一举而已);


06f02004b2984c65b7f98db5eb314dc2.png

  向下调整法建堆
  //for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  //{
  //  HeapAdjustDown(a, n, i);
  //}

d3629909ba8a4aea8cbade0bedbfcbd8.png

3-3.完整代码

void HeapSort(int* a, int n)
{
  //向上调整法建堆
  for (int i = 0; i < n; i++)
  {
    AdjustUp(a, i);
  }
  向下调整法建堆
  //for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  //{
  //  HeapAdjustDown(a, n, i);
  //}
}
void HeapPrint2(int* a, int n)
{
  assert(a);
  for (int i = 0; i < n; i++)
  {
    printf("%d  ", a[i]);
  }
}
int main()
{
  int a[]={ 14, 12, 4, 3, 6, 68, 21, 2 };
  int sz = sizeof(a) / sizeof(a[0]);
  HeapSort(a, sz);
  HeapPrint2(a, sz);
  return 0;
}

3-4.两种建堆方式的时间复杂度

时间复杂度=总调整次数=每一层节点个数*该层需要调整的次数


向下调整法建堆:LogN

ca16e46141f4419c94d6ccfeb148d8cd.png


0936bbed83a744ab9d2988c1edd2f0e5.png

57caff74061a4b3f81ba27379d102044.png


向上调整法建立堆:NLogN


a92cce2b6e5943e79d30b22249d54125.png

分析向上调整法和向下调整法建堆时间复杂度相差这么大的原因:

因为向下调整法的节点数量多的时候,需要调整的次数就少;

而向上调整法的节点数量多的时候,需要调整的次数也越多;

4.堆排序

前面我们学会了如何去高效的建立堆,其中我们优先采用时间复杂度更小的向下调整法建堆

我们直接在数组上建立了堆,那我们就可以接着通过选数,把数组进行排序,从而完成堆排序


那么问题又来了:如果我要排升序,我们应该建大堆还是小堆呐?


让我们想一想,如果要排升序,如果我们建立的是小堆的话,我们的确可以轻松的选出最小的数,但是如果我们在选次小的数的时候,就不得不破坏整个堆的结构,父子关系全乱了(和堆的插入和删除那里一样),这样下来重新建堆的话就是O(N)的时间复杂度;要选N个数,选一次数就要重新建一次堆的话,时间复杂度总体上就是O(N*N),那还不如直接遍历数组n次,也是N方,这样简直是拿着一手好牌,却打的稀烂!


所以我们升序的话采用建大堆的方式,那又有一个问题,建大堆后又是如何选出次小的呐?请往下看~~

4e467c9e79344450a341fd9cb2311894.png

代码如下:(在原来的基础上增加的选数的代码完成堆排序)

void HeapSort(int* a, int n)
{
  //向下调整法建堆,时间复杂度:O(N)
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    HeapAdjustDown(a, n, i);
  }
  //升序-建小堆
  //降序-建大堆
  //选数:堆排序
  int i = 1;
  //时间复杂度:O(NLogN)
  while (i < n)
  {
    Swap(&a[0], &a[n - i]);
    HeapAdjustDown(a, n - i, 0);
    ++i;
  }
}

5.TopK问题

在一堆数中,我们如果要找前K个最大的数,该怎么做?


或许你脑海里最先想到的是用快排先排序,然后直接选择前K个数据,那代价有点大.


这里鉴于选择排序中的堆排序的选数的经验,我们考虑采用堆的选数的思想解决这个问题.


(这里因为数据量过大,担心内存空间不够大,我们选择在磁盘上存储这些数据)


ea786488661747bf99a665354b9fb0cb.png

这时我们优先选择建小堆,我们建一个K个数的小堆,然后将后N-K个数和堆顶元素比较,如果堆顶元素小于后N-K个树数,就交换,然后向下调整,以此类推。

#include<time.h>
void CreateFileName(const char* filename, int N)
{
  FILE* pf = fopen(filename, "w");
  if (pf == NULL)
  {
    perror("fopen\n");
    exit(-1);
  }
  //生成随机数
  srand(time(0));
  for (int i = 0; i < N; i++)
  {
    fprintf(pf, "%d ", rand() % 10000 + 1);
  }
  fclose(pf);
  pf = NULL;
}
void PrintTopK(const char* filename, int k)
{
  FILE* pf = fopen(filename, "r");
  if (pf == NULL)
  {
    perror("fopen\n");
    exit(-1);
  }
  //文件读取前K个数
  int* minHeap = (int*)malloc(sizeof(int) * k);
  if (minHeap == NULL)
  {
    perror("malloc");
    exit(-1);
  }
  for (int i = 0; i < k; i++)
  {
    fscanf(pf,"%d", &minHeap[i]);
  }
  //建小堆
  for (int i = (k-1-1)/2; i>=0;--i)
  {
    HeapAdjustDown(minHeap, k, i);
  }
  //后N-k个数和堆顶元素比较
  int val = 0;
  while (fscanf(pf, "%d", &val)!=EOF)
  {
    if (val > minHeap[0])
    {
      minHeap[0] = val;
      HeapAdjustDown(minHeap, k, 0);
    }
  }
  //打印出前k大的数
  for (int i = 0; i < k; i++)
  {
    printf("%d ", minHeap[i]);
  }
  fclose(pf);
  pf = NULL;
}
int main()
{
  const char* filename = "test.txt";
  int N = 100;
  int k = 10;
  //给文件内随机生成N个数
  CreateFileName(filename, N);
  //从文件中选出N个数中前K大的几个数字,并且打印
  PrintTopK(filename, k);
  return 0;
}

59fb0d7f3d174e5ea59c205c17cbf27c.png

TopK问题的时间复杂度分析:

c04654fa24fc438e95e85299b4892098.png


目录
相关文章
|
6月前
|
算法 Python
数据结构算法--4堆排序
堆排序过程概述:建立大根堆,将堆顶最大元素移出并替换为末尾元素,调整保持堆性质,重复此过程直至堆为空,实现排序。时间复杂度为O(nlogn)。Python中可用heapq模块进行堆操作。
|
2月前
|
算法 搜索推荐
数据结构与算法学习十八:堆排序
这篇文章介绍了堆排序是一种通过构建堆数据结构来实现的高效排序算法,具有平均和最坏时间复杂度为O(nlogn)的特点。
75 0
数据结构与算法学习十八:堆排序
|
5月前
|
存储 算法
【数据结构】堆,堆的实现,堆排序,TOP-K问题
数据结构——堆,堆的实现,堆排序,TOP-K问题
51 1
【数据结构】堆,堆的实现,堆排序,TOP-K问题
|
4月前
|
搜索推荐 算法
【初阶数据结构篇】插入、希尔、选择、堆排序介绍(上篇)
堆排序(Heapsort)是指利⽤堆积树(堆)这种数据结构所设计的⼀种排序算法,它是选择排序的⼀ 种。它是通过堆来进⾏选择数据。需要注意的是排升序要建⼤堆,排降序建⼩堆。
21 0
|
4月前
|
算法
【初阶数据结构篇】堆的应用(堆排序与Top-K问题)
即求数据结合中前K个最⼤的元素或者最⼩的元素,⼀般情况下数据量都⽐较⼤。
51 0
|
5月前
|
搜索推荐
【数据结构常见七大排序(二)】—选择排序篇【直接选择排序】And【堆排序】
【数据结构常见七大排序(二)】—选择排序篇【直接选择排序】And【堆排序】
|
6月前
|
存储
数据结构初阶 堆(一)
数据结构初阶 堆(一)
34 1
|
6月前
数据结构初阶 堆(二)
数据结构初阶 堆(二)
30 0
|
1月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
161 9
|
1月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
30 1