[数据结构]堆详解

目录

一、堆的概念及结构

二、堆的实现

1.堆的定义

2堆的初始化

3堆的插入

[​编辑 4.堆的删除](#编辑 4.堆的删除)

5堆的其他操作

6代码合集

三、堆的应用

(一)堆排序(重点)

(二)TOP-K问题


一、堆的概念及结构

堆的性质:

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

  • 堆总是一棵完全二叉树(但实现起来是线性表)。

二、堆的实现

1.堆的定义

//大堆
typedef int HPDataType;
typedef struct Heap
{
       HPDataType* a;
       int size;
       int capacity;
}HP;

2堆的初始化

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

3堆的插入

//从孩子的位置向上调整函数
void AdjustUp(HPDataType* a, int child)//child是向上调整数的下标
{
       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;
              }
       }
}
//堆的插入
void HeapPush(HP* php, HPDataType x)
{
       assert(php);
       if (php->size == php->capacity)
       {
              HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) *  php->capacity*2);
              if (php->a == NULL)
              {
                      perror("malloc fail");
                      return;
              }
              // 将旧数据拷贝到新内存中
              for (int i = 0; i < php->size; i++)
              {
                      tmp[i] = php->a[i];
              }
              free(php->a);
              php->a = tmp;
              php->capacity *= 2;
       }
       php->a[php->size] = x;
       php->size++;
       AdjustUp(php->a, php->size - 1);//刚才size++了,所以向上调整的孩子的位置是size-1
}

4.堆的删除

删除堆顶,用处:可用来排序选出前几名

删除堆是删除堆顶的数据,将堆顶的数据根最后一个数据一换,然后删除数组最后一个数据,再进行向下调整算法。

难点:

  • 向下比怎么比?下面哪个儿子大就和谁比

  • 怎么判断已经到叶子节点了?计算该节点的孩子节点有没有超出范围

    //向下调整
    void AdjustDown(HPDataType* a, int n,int parent)
    {
    int child = parent * 2 + 1;//先默认左孩子大
    while (child < n)
    {
    //选出左右孩子中大的那一个 右孩子和左孩子的关系:大一
    if (child+1<n && a[child + 1] > a[child])
    {
    ++child;
    }
    if (a[child] > a[parent])
    {
    Swap(&a[child], &a[parent]);
    parent = child;
    child = parent * 2 + 1;
    }
    else
    {
    break;
    }
    }
    }
    //堆的删除
    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);
    }

如果想要取前k个,那么修改如下:

5堆的其他操作

//显示堆顶元素
HPDataType HeapTop(HP* php)
{
       assert(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 HeapDestroy(HP* php)
{
       assert(php);
       free(php->a);
       php->a = NULL;
       php->size = php->capacity = 0;
}

6代码合集

Heap.h

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
//大堆
typedef int HPDataType;
typedef struct Heap
{
       HPDataType* a;
       int size;
       int capacity;
}HP;
//堆的初始化
void HeapInit(HP* php);
//堆的插入
void HeapPush(HP* php, HPDataType x);
//堆的删除
void HeapPop(HP* php);
//显示堆顶元素
HPDataType HeapTop(HP* php);
//判断堆是否为空
bool HeapEmpty(HP* php);
//显示堆的大小
int HeapSize(HP* php);
//销毁
void HeapDestroy(HP* php);
//从孩子的位置向上调整函数
void AdjustUp(HPDataType* a, int child);
//向下调整
void AdjustDown(HPDataType* a, int n, int parent);

Heap.c

#include"Heap.h"
//堆的初始化
void HeapInit(HP* php)
{
       assert(php);
       php->a = (HPDataType*)malloc(sizeof(HPDataType)*4);
       if (php->a == NULL)
       {
              perror("malloc fail");
              return;
       }
       php->size = 0;
       php->capacity = 4;
}
void Swap(HPDataType* p1, HPDataType* p2)
{
       HPDataType temp = *p1;
       *p1 =*p2;
       *p2 = temp;
}
//从孩子的位置向上调整函数
void AdjustUp(HPDataType* a, int child)//child是向上调整数的下标
{
       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;
              }
       }
}
//堆的插入
void HeapPush(HP* php, HPDataType x)
{
       assert(php);
       if (php->size == php->capacity)
       {
              HPDataType* tmp = (HPDataType*)malloc(sizeof(HPDataType) *  php->capacity*2);
              if (php->a == NULL)
              {
                      perror("malloc fail");
                      return;
              }
              // 将旧数据拷贝到新内存中
              for (int i = 0; i < php->size; i++)
              {
                      tmp[i] = php->a[i];
              }
              free(php->a);
              php->a = tmp;
              php->capacity *= 2;
       }
       php->a[php->size] = x;
       php->size++;
       AdjustUp(php->a, php->size - 1);//刚才size++了,所以向上调整的孩子的位置是size-1
}
//向下调整
void AdjustDown(HPDataType* a, int n,int parent)
{
       int child = parent * 2 + 1;//先默认左孩子大
       while (child < n)
       {
              //选出左右孩子中大的那一个  右孩子和左孩子的关系:大一
              if (child+1<n && a[child + 1] > a[child])
              {
                      ++child;
              }
              if (a[child] > a[parent])
              {
                      Swap(&a[child], &a[parent]);
                      parent = child;
                      child = parent * 2 + 1;
              }
              else
              {
                      break;
              }
       }
}
//堆的删除
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);
       return php->a[0];
}
//判断堆是否为空
bool HeapEmpty(HP* php)
{
       assert(php);
       return php->size==0;
}
//显示堆的大小
int HeapSize(HP* php)
{
       assert(php);
       return php->size;
}
//销毁
void HeapDestroy(HP* php)
{
       assert(php);
       free(php->a);
       php->a = NULL;
       php->size = php->capacity = 0;
}

test.c

#include"Heap.h"
int main()
{
       HP hp;
       HeapInit(&hp);
       HeapPush(&hp, 4);
       HeapPush(&hp, 18);
       HeapPush(&hp, 42);
       HeapPush(&hp, 12);
       HeapPush(&hp, 2);
       HeapPush(&hp, 3);
       int k = 0;
       scanf_s("%d", &k);
       while (!HeapEmpty(&hp)&&k--)
       {
              printf("%d ", HeapTop(&hp));
              HeapPop(&hp);//和栈非常相似,想把老二取出来就得把老大干掉
       }
       printf("\n");
       return 0;
}

三、堆的应用

(一)堆排序(重点)

①. 建堆

  • 升序:建大堆
  • 降序:建小堆

建堆方式:
向上调整建堆:模拟的是插入数据的过程

//排升序建大堆
void HeapSort(int* a, int n)
{
       //建大堆
       for (int i = 1; i < n; i++)
       {
              AdjustUp(a, i);
       }
}

向下调整建堆(左右子树必须是大堆或小堆(插入之前得是堆)):

void HeapSort(int* a, int n)
{
       //向下调整建堆
       for (int i = (n - 1 - 1) / 2; i >= 0;--i)//先找到最后一个非叶子结点即上图的6 n-1是最后一个数据的下标,再-1除以2就是父节点
       {
              AdjustDown(a, n, i);
       }
}

注:向下建堆的效率O(N)比向上建堆的效率O(N*logN)高

数学证明如下:

②. 利用堆删除思想来进行排序

建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

代码实现:

#include<stdlib.h>

void Swap(HPDataType* p1, HPDataType* p2)
{
       HPDataType temp = *p1;
       *p1 =*p2;
       *p2 = temp;
}
//从孩子的位置向上调整函数
void AdjustUp(HPDataType* a, int child)//child是向上调整数的下标
{
       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;
              }
       }
}

//向下调整
void AdjustDown(HPDataType* a, int n,int parent)
{
       int child = parent * 2 + 1;//先默认左孩子大
       while (child < n)
       {
              //选出左右孩子中大的那一个  右孩子和左孩子的关系:大一
              if (child+1<n && a[child + 1] > a[child])
              {
                      ++child;
              }
              if (a[child] > a[parent])
              {
                      Swap(&a[child], &a[parent]);
                      parent = child;
                      child = parent * 2 + 1;
              }
              else
              {
                      break;
              }
       }
}

//O(n*logn)
//排升序建大堆
void HeapSort(int* a, int n)
{
       //向下调整建堆
for (int i = (n - 1 - 1) / 2; i >= 0;--i)//n-1是最后一个数据的下标,再-1除以2就是父节点
{
       AdjustDown(a, n, i);
}
       int end = n - 1;
       while (end>0)
       {
              Swap(&a[end], &a[0]);
        AdjustDown(a, end, 0);
              --end;
       }
}
int main()
{
       int a[10] = { 2,1,5,7,6,8,0,9,3 };
       HeapSort(a, 9);
       return 0;
}

③堆排序的时间复杂度

所以如果用来排序的话,无论是向上调整还是向下调整建堆,总的时间复杂度都是O(N*logN)

(二)TOP-K问题

TOP-K 问题:即求数据结合中前 K 个最大的元素或者最小的元素,一般情况下数据量都比较大
比如:专业前 10 名、世界 500 强、富豪榜、游戏中前 100 的活跃玩家等。
对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了 ( 可能
数据都不能一下子全部加载到内存中 ) 。最佳的方式就是用堆来解决,基本思路如下:

  1. 用数据集合中前 K 个元素来建堆
    前 k 个最大的元素,则建小堆
    前 k 个最小的元素,则建大堆(和堆排序有点反过来的意思)

  2. 用剩余的 N-K 个元素依次与堆顶元素来比较,不满足则替换堆顶元素
    将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素

    #define _CRT_SECURE_NO_WARNINGS 1
    #include"Heap.h"
    void PrintTopK(const char* file, int k)
    {
    // 1. 建堆--用a中前k个元素建小堆
    int* topk = (int*)malloc(sizeof(int) * k);
    assert(topk);
    //读文件
    FILE* fout = fopen(file, "r");
    if (fout == NULL)
    {
    perror("fopen error");
    return;
    }
    //读出前K个数建堆
    for (int i = 0; i < k; ++i)
    {
    fscanf(fout, "%d", &topk[i]);
    }
    //向下调整建堆
    for (int i = (k - 1 - 1) / 2; i >= 0; --i)
    {
    AdjustDown(topk, k, i);
    }
    // 2. 将剩余n-k个元素依次与堆顶元素交换,不满则则替换
    int val = 0;
    int ret= fscanf(fout, "%d", &val);
    while (ret != EOF)
    {
    if (val > topk[0])//如果新元素大于堆顶元素,那么替换堆顶元素
    {
    topk[0] = val;
    AdjustDown(topk, k, 0);
    }
    ret = fscanf(fout, "%d", &val);
    }
    //打印这个数组
    for (int i = 0; i < k; i++)
    {
    printf("%d ", topk[i]);
    }
    printf("\n");
    free(topk);
    fclose(fout);
    }
    void TestTopk()
    {
    //为了测试而造数据
    int n = 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)
    {
    int x = rand() % 10000;
    fprintf(fin, "%d\n", x);
    }
    fclose(fin);
    PrintTopK(file,10);
    }
    int main()
    {
    TestTopk();
    return 0;
    }

注意:这里是建小堆,AdjustDown里面需要调一下,之前是建大堆用的AdjustDown

相关推荐
不爱学习的小枫8 分钟前
scala的集合
开发语言·scala
梦醒沉醉8 分钟前
Scala的初步使用
开发语言·后端·scala
小白学大数据14 分钟前
Fuel 爬虫:Scala 中的图片数据采集与分析
开发语言·爬虫·scala
贩卖纯净水.28 分钟前
《React 属性与状态江湖:从验证到表单受控的实战探险》
开发语言·前端·javascript·react.js
JouJz33 分钟前
Java基础系列:深入解析反射机制与代理模式及避坑指南
java·开发语言·代理模式
Dante79837 分钟前
【数据结构】二叉搜索树、平衡搜索树、红黑树
数据结构·c++·算法
白羊不吃白菜1 小时前
PAT乙级(1101 B是A的多少倍)C语言解析
c语言·开发语言
一号言安1 小时前
牛客python蓝桥杯11-32(自用)
开发语言·python
shinelord明1 小时前
【软件设计】23 种设计模式解析与实践指南
数据结构·设计模式·软件工程
鸽鸽程序猿1 小时前
【JavaEE】SpringIoC与SpringDI
java·开发语言·java-ee