【数据结构】第十站:堆与堆排序

简介: 【数据结构】第十站:堆与堆排序

一、二叉树的顺序结构

二叉树有顺序结构和链式结构,分别使用顺序表和链表来实现

普通的二叉树是不适合用数组来存储的,因为可能会存在大量的空间浪费。而完全二叉树更适合使用顺序结构存储。现实中我们通常把堆(一种二叉树)使用顺序结构的数组来存储,需要注意的是这里的堆和操作系统

虚拟进程地址空间中的堆是两回事,一个是数据结构,一个是操作系统中管理内存的一块区域分段。

如下图所示,是二叉树的顺序结构的逻辑结构与物理结构。逻辑结构就是我们想象出来的,而物理结构就是实实在在在内存中是如何存储的。

不难发现,将逻辑结构转化为物理结构就是只需要按照一层一层的往数组中插入即可

并且这样转化好之后,他们的父子之间的下标是有规律的

parent=(child-1)/2

leftchild=parent*2+1

rightchild=parent*2+2

但是这种顺序存储是适合满二叉树或者完全二叉树的,如果不是的话,会有很多空的元素,这样就存在一定的浪费了。

由此可见,堆只适合存储完全二叉树

二、堆的概念及结构

堆的性质:

堆中某个节点的值总是不大于或不小于其父节点的值;

堆总是一棵完全二叉树。

上面的概念或许比较抽象,简单来说就是堆是一颗完全二叉树,在这颗二叉树中,如果每一个父节点都大于或等于子节点,那么称作最大堆,如果每一个父节点都小于或等于子节点,那么称作最小堆。

三、堆的实现

1.堆的创建

由于堆是一颗完全二叉树,并且存储在顺序表中,所以它的定义就很简单了

typedef int HPDateType;
typedef struct Heap
{
  HPDateType* a;
  int size;
  int capacity;
}Heap;

2.堆的各接口实现

1.堆的初始化

//堆的初始化
void HeapInit(Heap* php)
{
  assert(php);
  php->a = (HPDateType*)malloc(sizeof(HPDateType) * 4);
  if (php->a == NULL)
  {
    perror("malloc fail");
    return;
  }
  php->size = 0;
  php->capacity = 4;
}

如上代码所示,与顺序表的初始化是一样的操作

2.堆的插入

void Swap(HPDateType* a, HPDateType* b)
{
  HPDateType tmp = *a;
  *a = *b;
  *b = tmp;
}
void AdjustUp(HPDateType* a, int child)
{
  assert(a);
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[parent] < a[child])
    {
      Swap(&a[parent], &a[child]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
//堆的插入
void HeapPush(Heap* php, HPDateType x)
{
  assert(php);
  if (php->size == php->capacity)
  {
    HPDateType* tmp = (HPDateType*)realloc(php->a, sizeof(HPDateType) * php->capacity * 2);
    if (tmp == NULL)
    {
      perror("realloc fail");
      return;
    }
    php->a = tmp;
    php->capacity *= 2;
  }
  php->a[php->size++] = x;
  AdjustUp(php->a, php->size - 1);
}

如上代码所示,对于堆的插入,首先是现在顺序表中进行尾插,之后,我们要将这个刚刚插入的数据进行调整,假设我们需要的是大堆,那么也就是说,如果刚插入节点比父节点大,那么就需要进行交换,然后一层一层网上调整,直到遇到一个祖先比他大。一旦当刚插入的数据到达了最顶端,那么也可以停止了。

3.堆的删除

void AdjustDown(HPDateType* a, int n, int parent)
{
  assert(a);
  int child = parent * 2 + 1;
  while (child < n)
  {
    if ((child + 1 < n) && (a[child] < a[child + 1]))
    {
      child++;
    }
    if (a[child] > a[parent])
    {
      Swap(&a[child], &a[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
//堆的删除
void HeapPop(Heap* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  Swap(&(php->a[0]), &(php->a[php->size-1]));
  php->size--;
  AdjustDown(php->a, php->size, 0);
}

如上代码所示,对于堆而言它一般删除的是堆顶的元素,因为对于其他的元素没有意义,不仅破坏了堆的结构,扰乱了父子关系,而且效率低下。

所以我们一般删除堆顶元素,为了删除堆顶元素,我们可以交换堆顶的尾元素,然后删除掉最后一个元素即可。此时我们堆顶的元素需要往下调整,即如果孩子节点大比父亲节点大的化,则交换,一直循环往复。原来的元素调整到叶子节点的时候,就可以停止了。要注意,每一次向下调整需要与最大的孩子进行交换,我们可以假设左孩子最大, 然后进行比较从而得出大孩子的下标。

在向下调整中,我们需要传三个参数,一个是数组,一个是堆顶下标,一个是数组的元素个数。要传数组的元素个数是为了方便判断叶子节点的个数。

与向上调整不同的是,向上调整不需要传数组元素个数,因为它并不关心数组有多少元素,只需要确保孩子到达堆顶即可。而堆顶的下标很显然就是0。

也就是说,是目标的不同而导致的参数有所变化,向上是从某个位置到0下标处,向下某个是从某个位置到叶子节点处。叶子节点的位置需要根据size来判断

4.取堆顶的数据

//取堆顶的数据
HPDateType HeapTop(Heap* php)
{
  assert(php);
  return php->a[0];
}

这个代码很简单,返回堆顶元素即可

5.判断堆是否为空

//堆是否为空
bool HeapEmpty(Heap* php)
{
  assert(php);
  return php->size == 0;
}

与顺序表和栈判断为空方法一样

6.判断堆的数据个数

//堆的数据个数
int HeapSize(Heap* php)
{
  assert(php);
  return php->size;
}

同样的,与栈的方法一样

7.堆的销毁

// 堆的销毁
void HeapDestory(Heap* php)
{
  assert(php);
  free(php->a);
  php->a = NULL;
  php->size = 0;
  php->capacity = 0;
}

由于堆是使用顺序表实现的,所以其实就是顺序表或栈的销毁

8.利用一个数组来初始化堆

//利用数组初始化堆
void HeapInitArray(Heap* php, int* a, int n)
{
  assert(php);
  php->a = (HPDateType*)malloc(sizeof(HPDateType) * n);
  if (php->a == NULL)
  {
    perror("malloc fail");
    return;
  }
  php->size = n;
  php->capacity = n;
  int i = 0;
  for (i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(php->a, php->size, i);
  }
}

有时候我们也会遇到这样的接口,多给了一个数组,目的是讲这个数组中的数据给建成堆。我们直接使用向下调整法即可。向下调整在下文中详细给出。

四、堆排序

1.堆排序的基本思想

堆排序的思想是这样的,首先先将传入的数组的元素给建成堆,建堆的思想可以使用向上调整法,也可以使用向下调整法。然后当堆建成以后,如果我们要排的是升序,那么我们需要建大堆,这是因为大堆的堆顶元素就是最大的元素,为了不破坏堆,我们将堆顶元素和最后一个元素进行交换,这样一来,最后一个元素就是最大的元素了,然后将前n-1个元素看作一个新的数组,先将第一个元素向下调整,然后继续让堆顶元素和最后一个元素交换。这样就实现了堆排序。

注意:

1.排升序一定要建大堆,排降序建小堆。

如果升序建了小堆的话,那么第一个元素确实是最小的元素,但是后面的n-1个元素的堆关系全部混乱了。我们需要重新建堆,时间复杂度大幅度提升。

2.在建堆的时候,如果采用向上调整法建堆,由于向上调整法必须保证原来的数组就是一个大堆或小堆,所以那么我们是从第二个元素开始,每一个元素依次向上调整即可,类似于堆的插入,这样我们可以保证我们前面的元素时刻为大堆或者小堆

3.在建堆的时候,如果采用向下调整法建堆,由于向下调整法需要保证,这个节点的左右子树都是大堆或者小堆,所以,我们就得从后面的元素开始建堆,由于叶子节点本身可以视作一个堆,所以我们从最后一个父节点开始建堆,然后一步一步往前面的父节点进行调整,这样可以保证每一个节点的左右子树都是大堆或小堆。

2.堆排序的实现

//堆排序
void HeapSort(int* a, int n)
{
  //向上调整建堆
  int i = 0;
  //for (i = 1; i < n; i++)
  //{
  //  AdjustUp(a, i);
  //}
  //向下调整建堆
  for (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--;
  }
}

如上代码所示,是堆排序的代码,建堆的时候,我们将向下调整和向上调整建堆的方法都给出,当堆建好以后,我们开始让堆顶元素与最后一个元素交换,然后向下调整,然后继续从前n-1个元素的数组中,让堆顶元素继续与最后最后一个元素交换,向下调整,如此循环往复即可。

3.堆排序时间复杂度

先说结论:向上调整建堆的时间复杂度为O(N*logN),向下调整建堆的时间复杂度为O(N),在选大数排序的过程时间复杂度也是O(N*logN)

总时间复杂度为O(N*logN)

向下调整建堆的时间复杂度分析

我们直接讨论最坏情况

如上图所示的满二叉树。我们不难得知每一层的节点个数

向上调整建堆的时间复杂度

事实上,我们也可以直观的看出来

向上调整建堆,由于二叉树下层的节点个数多,而下层向上层调整的次数也多。所以最终的次数必然是较多的

向下调整建堆,由于二叉树的下层节点个数多,但是下层的向下调整次数少,上层的个数少,次数多。所以向下调整建堆是优于向上调整建堆的

3.利用堆排序的时间复杂度计算

对于这一段代码,我们是这样思考的

先看最坏情况,最后一层的最坏情况是:由2^(h-1)次方个节点,每个节点交换后,又需要向下调整到最后一层也就是h-1次

不难发现,最后一层的时间复杂度已经达到了O(N*logN)

四、TOP-K问题

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

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

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

1.前k个数据建立一个小堆

2.遍历剩余的数据,如果存在一个数据比堆顶元素大,那么让这个大的元素替换掉堆顶元素。最后向下调整,再次变成小堆。这样就可以解决问题。最终堆里面的数据就是前k个最大的元素

如下代码是随机生成100000个数据,然后求出前k个的最大值的代码

void CreatDate()
{
  int n = 100000;
  srand(time(0));
  const char* file = "date.txt";
  FILE* pf = fopen(file, "w");
  if (pf == NULL)
  {
    perror("fopen");
    return;
  }
  while (n--)
  {
    int x = rand() % 10000;
    fprintf(pf, "%d\n", x);
  }
  fclose(pf);
}
void PrintTopK(const char* file,int k)
{
  int* topk = (int*)malloc(sizeof(int) * k);
  if (topk == NULL)
  {
    perror("malloc\n");
    return;
  }
  FILE* pf = fopen(file, "r");
  if (pf == NULL)
  {
    perror("fopen\n");
    return;
  }
  for (int i = 0; i < k; i++)
  {
    fscanf(pf, "%d", &topk[i]);
  }
  for (int i = (k - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(topk, k, i);
  }
  int x = 0;
  int ret = fscanf(pf, "%d", &x);
  while (ret != EOF)
  {
    if (x > topk[0])
    {
      Swap(&x, &topk[0]);
      AdjustDown(topk, k, 0);
    }
    ret = fscanf(pf, "%d", &x);
  }
  for (int i = 0; i < k; i++)
  {
    printf("%d ", topk[i]);
  }
  free(topk);
  fclose(pf);
}

五、堆的完整代码

Heap.h

#pragma once
#include<stdio.h>
#include<malloc.h>
#include<assert.h>
#include<stdbool.h>
#include<time.h>
#include<stdlib.h>
typedef int HPDateType;
typedef struct Heap
{
  HPDateType* a;
  int size;
  int capacity;
}Heap;
//堆的初始化
void HeapInit(Heap* php);
//利用数组初始化堆
void HeapInitArray(Heap* php, int* a, int n);
//堆的插入
void HeapPush(Heap* php, HPDateType x);
//堆的删除
void HeapPop(Heap* php);
//取堆顶的数据
HPDateType HeapTop(Heap* php);
//堆是否为空
bool HeapEmpty(Heap* php);
//堆的数据个数
int HeapSize(Heap* php);
// 堆的销毁
void HeapDestory(Heap* php);
//交换两个元素
void Swap(HPDateType* a, HPDateType* b);
//向上调整
void AdjustUp(HPDateType* a, int child);
//向下调整
void AdjustDown(HPDateType* a, int n, int parent);
//堆排序
void HeapSort(int* a, int n);

Heap.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
//堆的初始化
void HeapInit(Heap* php)
{
  assert(php);
  php->a = (HPDateType*)malloc(sizeof(HPDateType) * 4);
  if (php->a == NULL)
  {
    perror("malloc fail");
    return;
  }
  php->size = 0;
  php->capacity = 4;
}
void Swap(HPDateType* a, HPDateType* b)
{
  HPDateType tmp = *a;
  *a = *b;
  *b = tmp;
}
//向上调整
void AdjustUp(HPDateType* a, int child)
{
  assert(a);
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[parent] < a[child])
    {
      Swap(&a[parent], &a[child]);
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
//堆的插入
void HeapPush(Heap* php, HPDateType x)
{
  assert(php);
  if (php->size == php->capacity)
  {
    HPDateType* tmp = (HPDateType*)realloc(php->a, sizeof(HPDateType) * php->capacity * 2);
    if (tmp == NULL)
    {
      perror("realloc fail");
      return;
    }
    php->a = tmp;
    php->capacity *= 2;
  }
  php->a[php->size++] = x;
  AdjustUp(php->a, php->size - 1);
}
//向下调整
void AdjustDown(HPDateType* a, int n, int parent)
{
  assert(a);
  int child = parent * 2 + 1;
  while (child < n)
  {
    if ((child + 1 < n) && (a[child] > a[child + 1]))
    {
      child++;
    }
    if (a[child] < a[parent])
    {
      Swap(&a[child], &a[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
//堆的删除
void HeapPop(Heap* php)
{
  assert(php);
  assert(!HeapEmpty(php));
  Swap(&(php->a[0]), &(php->a[php->size-1]));
  php->size--;
  AdjustDown(php->a, php->size, 0);
}
//取堆顶的数据
HPDateType HeapTop(Heap* php)
{
  assert(php);
  return php->a[0];
}
//堆是否为空
bool HeapEmpty(Heap* php)
{
  assert(php);
  return php->size == 0;
}
//堆的数据个数
int HeapSize(Heap* php)
{
  assert(php);
  return php->size;
}
// 堆的销毁
void HeapDestory(Heap* php)
{
  assert(php);
  free(php->a);
  php->a = NULL;
  php->size = 0;
  php->capacity = 0;
}
//堆排序
void HeapSort(int* a, int n)
{
  //向上调整建堆
  int i = 0;
  //for (i = 1; i < n; i++)
  //{
  //  AdjustUp(a, i);
  //}
  //向下调整建堆
  for (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 HeapInitArray(Heap* php, int* a, int n)
{
  assert(php);
  php->a = (HPDateType*)malloc(sizeof(HPDateType) * n);
  if (php->a == NULL)
  {
    perror("malloc fail");
    return;
  }
  php->size = n;
  php->capacity = n;
  int i = 0;
  for (i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(php->a, php->size, i);
  }
}

Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"Heap.h"
void TestHeap1()
{
  Heap hp;
  HeapInit(&hp);
  HeapPush(&hp, 5);
  HeapPush(&hp, 10);
  HeapPush(&hp, 60);
  HeapPush(&hp, 100);
  HeapPush(&hp, 99);
  HeapPush(&hp, 12);
  HeapPush(&hp, 16);
  while (!HeapEmpty(&hp))
  {
    printf("%d ", HeapTop(&hp));
    HeapPop(&hp);
  }
  printf("\n");
  HeapDestory(&hp);
}
void TestHeapSort()
{
  int a[] = { 1,2,5,4,100,22,165,12,167,123,111,0 };
  int sz = sizeof(a) / sizeof(a[0]);
  HeapSort(a, sz);
}
void CreatDate()
{
  int n = 100000;
  srand(time(0));
  const char* file = "date.txt";
  FILE* pf = fopen(file, "w");
  if (pf == NULL)
  {
    perror("fopen");
    return;
  }
  while (n--)
  {
    int x = rand() % 10000;
    fprintf(pf, "%d\n", x);
  }
  fclose(pf);
}
void PrintTopK(const char* file,int k)
{
  int* topk = (int*)malloc(sizeof(int) * k);
  if (topk == NULL)
  {
    perror("malloc\n");
    return;
  }
  FILE* pf = fopen(file, "r");
  if (pf == NULL)
  {
    perror("fopen\n");
    return;
  }
  for (int i = 0; i < k; i++)
  {
    fscanf(pf, "%d", &topk[i]);
  }
  for (int i = (k - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(topk, k, i);
  }
  int x = 0;
  int ret = fscanf(pf, "%d", &x);
  while (ret != EOF)
  {
    if (x > topk[0])
    {
      Swap(&x, &topk[0]);
      AdjustDown(topk, k, 0);
    }
    ret = fscanf(pf, "%d", &x);
  }
  for (int i = 0; i < k; i++)
  {
    printf("%d ", topk[i]);
  }
  free(topk);
  fclose(pf);
}
int main()
{
  //TestHeap1();
  //TestHeapSort();
  //CreatDate();
  PrintTopK("date.txt", 10);
  return 0;
}

本期内容就到这里了

如果对你有帮助的话,不要忘记点赞加收藏哦!!!

相关文章
|
1月前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
38 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
68 16
|
2月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
85 1
|
2月前
|
算法 搜索推荐
数据结构与算法学习十八:堆排序
这篇文章介绍了堆排序是一种通过构建堆数据结构来实现的高效排序算法,具有平均和最坏时间复杂度为O(nlogn)的特点。
75 0
数据结构与算法学习十八:堆排序
|
2月前
|
存储 算法 调度
数据结构--二叉树的顺序实现(堆实现)
数据结构--二叉树的顺序实现(堆实现)
|
2月前
|
存储 算法 分布式数据库
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
【初阶数据结构】理解堆的特性与应用:深入探索完全二叉树的独特魅力
|
2月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
2月前
|
存储 算法 Java
【用Java学习数据结构系列】用堆实现优先级队列
【用Java学习数据结构系列】用堆实现优先级队列
36 0
|
2月前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现
|
2月前
【数据结构】大根堆和小根堆
【数据结构】大根堆和小根堆
38 0