[数据结构]堆详解

目录

一、堆的概念及结构

二、堆的实现

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

相关推荐
catchadmin41 分钟前
PHP 8.7 协程新提案 让跨协程代码共享请求身份
开发语言·php
念何架构之路1 小时前
go-grpc设计思想
开发语言·后端·golang
酷炫码神2 小时前
Day 10|Pinia 状态管理:把散落在各组件的数据,收进一个全局仓库
android·开发语言·javascript
careathers2 小时前
【数据结构】从堆到 PriorityQueue:手写一个优先级队列
数据结构
Elastic 中国社区官方博客3 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1883 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn3 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su4 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
传奇开心果编程4 小时前
【Rust入门练中学】第5课:引用与借用
开发语言·rust·引用·编程语言理论·通用编程语言·借用
qetfw4 小时前
Bash 变量:export、位置参数与默认值展开验证
linux·开发语言·chrome·bash