【树与二叉树】堆的时间复杂度详解以及堆的应用—堆排序、TOP - K问题

简介: 【树与二叉树】堆的时间复杂度详解以及堆的应用—堆排序、TOP - K问题

1. 堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明

(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)d51d0acafb894b69ab2f0b7a38f64a3a.png

1. 堆的时间复杂度

因为堆是完全二叉树,而满二叉树也是完全二叉树,此处为了简化使用满二叉树来证明

(时间复杂度本来看的就是近似值,多几个节点不影响最终结果)image.png

1.1 向下调整建堆

每层节点个数 × 最坏情况向下调整次数:

T(N) = 2^(h-2) × 1 + 2^(h-3) × 2 + … … + 2^1 × (h-2)+2^0*(h-1)

错位相减法

等号左右两边乘个 2 得到一个新公式,再用新公式减去旧的公式,具体见下图image.png严格来说,向下调整的时间复杂度:N-log(N)–>O(N) [log(N)可以忽略不计]

c8e8af1e66384a70a4cfe05cf6caaaa7.png

1.2 向上调整建堆

T(N) = 2^1× 1 + 2^2× 2 + 2^3 ×3+ … … + 2^(h-3)× (h-3) + 2^(h-2) × (h-2)+ 2^(h-1) × (h-1)5bab0c0a280245b19525fae2b73a22a0.png综上:向下调整建堆要更优秀,效率更高ca7afebf23584d3cbf45a64abe3ab6e0.png

但总的来说堆排序的时间复杂度是O(N*logN)

2. 堆的应用

2.1 堆排序

堆创建后,如何进行排序 (升序、降序)

升序建大堆;降序建小堆。不是说升序建小堆;降序建大堆不行,而是因为不好:

如果排升序建小堆:

 1、选出最小的数,最小的数就放在第一个位置

 2、接着就要选次小的数,再选次小的数 … … 不断的选下去,如何选 ?

只能对剩下的 sz-1、sz-2、sz-3 … 个数继续建堆。可想这样的代价是很高的 —— 建堆的时间复杂度是 O(N),整个时间复杂度就是 O(N^2),堆的价值没有体现,不如直接循环遍历

1682f209f0c9459fb51d2dfe956cec42.png

如果排升序建大堆:

  1、选出最大的数,与最后一个数交换位置

  2、怎么选出次大、次次大 ?

堆的结构没有被破坏,且最后一个数不看做堆,左右子树依旧是大堆,向下调整即可

最多调整 log2N 次,整体的时间复杂度是 O(N*log2N)

2e9353ed20dc4a2a86769de0b70de861.png

对比效率:

冒泡排序和堆排序

1000      1000000
O(N2)   1000000(1百万)  1000000000000(1万亿)
O(N*log2N)  10000=10*1000 20000000(2千万)=20*1000000

升序建大堆、降序建小堆

image.png向上调整建堆:模拟插入的过程

向下调整建堆:模拟删除的过程,这里从倒数的第一个非叶子节点开始调整

734bdfa7c6834647a7748b8fbc67d1a0.png

#include<stdio.h>
//实现父子交换的函数
void Swap(int* px, int* py)
{
  int temp = *px;
  *px = *py;
  *py = temp;
}
//实现调整
void AdjustDown(int* arr, int sz, int parent)
{
  //确定左孩子的下标
  int child = parent * 2 + 1;
  //孩子的下标超出数组的范围就停止
  while (child < sz)
  {
    //确定左右孩子中较小/大的那个
      //左孩子大于右孩子,所以让child记录较小孩子的下标 || (arr[child]<arr[child+1]记录较大孩子的下标)
    if (arr[child] > arr[child + 1] && child + 1 < sz)
    {
      child++; //(当只有一个左孩子时,会越界,且后面使用时会发生非法访问)
    }
    //判断父亲和小孩子
      //小孩子小于父亲,则交换,且继续调整 || (arr[child]>arr[parent]大孩子大于父亲,则交换,且继续调整)
    if (arr[child] < arr[parent])
    {
      Swap(&arr[child], &arr[parent]);
      //迭代
      parent = child;
      //重新确定左孩子的下标(当最后的叶子节点是parent时,这时去确定child会以读的方式越界,但可以不关心)
      child = parent * 2 + 1;
    }
    //小孩子大于父亲,则停止调整
    else
    {
      break;
    }
  }
}
//堆排序 -> 效率更高
void HeapSort(int* arr, int sz)
{
  //建堆
  int i = 0;
  //从最后一棵树开始调整,也就是最后一个节点的父亲
  for (i = (sz - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(arr, sz, i);
  }
}
int main()
{
  //左右子树都为堆
  int arr1[] = { 27, 15, 19, 18, 28, 34, 65, 49, 25, 37 };
  //左右子树都为非堆
  int arr2[] = { 27, 37, 28, 18, 19, 34, 65, 25, 49, 15 };
  HeapSort(arr1, sizeof(arr1) / sizeof(arr1[0]));
  int i = 0;
  for (i = 0; i < sizeof(arr1) / sizeof(arr1[0]); i++)
  {
    printf("%d ", arr1[i]);
  }
  printf("\n");
  HeapSort(arr2, sizeof(arr2) / sizeof(arr2[0]));
  for (i = 0; i < sizeof(arr2) / sizeof(arr2[0]); i++)
  {
    printf("%d ", arr2[i]);
  }
  return 0;
}

2.2 TOP - K问题

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

比如:CSDN总榜前10、世界500强、未央区排名前10的泡馍等。

2.2.1 方法 1:

排序:时间复杂度 O(N * logN)

2.2.2 方法 2:

建一个 N 个数的堆(优先级队列),不断选数,选出前 K 个,时间复杂度 O(N+K * log(N))

假设 N 是十亿,显然前两个方法都不适用

ee1669b136d44594af280e2cc1d29b71.png

2.2.3 方法 3:

对于 Top-K 问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能一下子全部加载到内存中)。最佳的方式就是用堆 (优化方法 ) 来解决,基本思路如下:

1 . 用数据集合中前 K 个元素来建堆

 ▶ 前k个最大的元素,则建小堆

 ▶ 前k个最小的元素,则建大堆

2.用剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素

 ▶ 将剩余 N-K 个元素依次与堆顶元素比完之后,堆中剩余的 K 个元素就是所求的前 K 个最小或者最大的元素

 

模拟-创建随机数种子,生成随机数

怎么知道前十个数就是 TOP - 10 ?

 默认随机生成的数都是小于 1000000 的,然后给随机位置的 10 个数都是比 1000000 要大的,把这 10 个数选出来就说明算法是对的

I. TOP-K.h 用于函数的声明

#pragma once
//头
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<stdbool.h>
#include<time.h>
typedef int HPDataType;
//C++ -> priority_queue 在C++里用的是优先级队列,其底层就是一个堆
//大堆
typedef struct Heap
{
  HPDataType* a;
  int size;
  int capacity;
}HP;
//函数的声明
//交换
void Swap(int* px, int* py);
//向下调整
void AdjustDown(int* arr, int n, int parent);
//向上调整
void AdjustUp(int* a, int child);
//使用数组进行初始化
void HeapInit(HP* php, HPDataType* a, int n);
//回收空间
void HeapDestroy(HP* php);
//插入x,保持它继续是堆
void HeapPush(HP* php, HPDataType x);
//删除堆顶的数据,保持它继续是堆
void HeapPop(HP* php);
//获取堆顶的数据,也就是最值
HPDataType HeapTop(HP* php);
//判空
bool HeapEmpty(HP* php);
//堆的数据个数
int HeapSize(HP* php);
//输出
void HeapPrint(HP* php);

II. TOP-K.c 用于函数的定义

#include"TOP-K.h"
void Swap(int* px, int* py)
{
  int temp = *px;
  *px = *py;
  *py = temp;
}
void AdjustDown(int* arr, int n, int parent)
{
  int child = parent * 2 + 1;
  while (child < n)
  {
    if (arr[child] > arr[child + 1] && child + 1 < n)
    {
      child++;
    }
    if (arr[child] < arr[parent])
    {
      Swap(&arr[child], &arr[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
void AdjustUp(int* a, int 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 HeapPrint(HP* php)
{
  assert(php);
  int i = 0;
  for (i = 0; i < php->size; i++)
  {
    printf("%d ", php->a[i]);
  }
  printf("\n");
}
//1、对于HeapCreate函数,结构体不是外面传进来的,而是在函数内部自己malloc空间,再创建的
/*
HP* HeapCreate(HPDataType* a, int n)
{}
*/
//2、对于HeapInit函数,在外面定义一个结构体,把结构体的地址传进来
void HeapInit(HP* php, HPDataType* a, int n)
{00j000 
  assert(php);
  //malloc空间(当前数组大小一样的空间)
  php->a = (HPDataType*)malloc(sizeof(HPDataType) * n);
  if (php->a == NULL)
  {
    printf("malloc fail\n");
    exit(-1);
  }
  //使用数组初始化
  memcpy(php->a, a, sizeof(HPDataType) * n);
  php->size = n;
  php->capacity = n;
  //建堆 
  int i = 0;
  for (i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    AdjustDown(php->a, n, i);
  }
}
void HeapDestroy(HP* php)
{
  assert(php);
  free(php->a);
  php->a = NULL;
  php->size = php->capacity = 0;
}
bool HeapEmpty(HP* php)
{
  assert(php);
  return php->size == 0;
}
void HeapPush(HP* php, HPDataType x)
{
  assert(php);
  //空间不够,增容
  if (php->size == php->capacity)
  {
    HPDataType* temp = (HPDataType*)realloc(php->a, php->capacity * 2 * sizeof(HPDataType));
    if (temp == NULL)
    {
      printf("realloc fail\n");
      exit(-1);
    }
    else
    {
      php->a = temp;
    }
    php->capacity *= 2;
  }
  //将x放在最后
  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];
}
int HeapSize(HP* php)
{
  assert(php);
  return php->size;
}

III. Test.c 用于函数的测试

#include"TOP-K.h"
void PrintTopK(int* a, int n, int k)
{
  HP hp;
  HeapInit(&hp, a, k);
  int i = 0;
  for (i = k; i < n; i++)
  {
    //比较堆顶的数据
    if (a[i] > HeapTop(&hp))//如果大于堆顶元素,替代进堆
    {
      HeapPop(&hp);
      HeapPush(&hp, a[i]);
    }
  }
  HeapPrint(&hp);
  HeapDestroy(&hp);
}
void TestTopk()
{
  int n = 100000;
  int* a = (int*)malloc(sizeof(int)*n);
  //创建随机数种子
  srand((unsigned int)time(0));
  //生成随机数
  for (size_t i = 0; i < n; ++i)
  {
    a[i] = rand() % 1000000;
  }
  //如果找出这10个数,说明TOP-K算法是正确的
  a[5] = 1000000 + 1;
  a[1231] = 1000000 + 2;
  a[531] = 1000000 + 3;
  a[5121] = 1000000 + 4;
  a[115] = 1000000 + 5;
  a[2335] = 1000000 + 6;
  a[9999] = 1000000 + 7;
  a[76] = 1000000 + 8;
  a[423] = 1000000 + 9;
  a[3144] = 1000000 + 10;
  PrintTopK(a, n, 10);
}
int main()
{
  TestTopk();
  return 0;
}

3.总结:

今天我们认识并学习了堆的时间复杂度,并且通过分析对向下调整算法和向上调整算法也有了更深入的了解。还对堆的应用——堆排序、TOP - K问题进行了分析。下一篇博客我们将对二叉树链式结构进行分析及实现。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

c3ad96b16d2e46119dd2b9357f295e3f.jpg

相关文章
|
算法 分布式数据库 C语言
堆的相关时间复杂度计算(C语言)
堆的相关时间复杂度计算(C语言)
|
机器学习/深度学习 自然语言处理 算法
机器学习-特征选择:如何用信息增益提升模型性能?
机器学习-特征选择:如何用信息增益提升模型性能?
1029 1
|
机器学习/深度学习 算法 数据可视化
浅析特征数据离散化的几种方法(上)
什么是离散化? 离散化就是把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。例如:
|
11月前
|
供应链 监控 前端开发
如何开发供应商管理系统中的供应商管理板块(附架构图+流程图+代码参考)
在现代企业中,供应商管理至关重要。高效的供应商管理系统(VMS)可优化供应链、提升采购效率、降低成本并增强合作。本文介绍VMS的核心模块——供应商管理板块,涵盖供应商注册、准入、资质管理、产品与样品管理等功能。通过系统化管理,企业可实现信息透明、操作规范、风险可控,从而提升整体运营效率。内容还包含业务流程设计、开发技巧、实现效果及优化建议,助力企业构建完善的供应商管理体系。
|
自然语言处理 数据可视化 前端开发
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
合合信息的智能文档处理“百宝箱”涵盖文档解析、向量化模型、测评工具等,解决了复杂文档解析、大模型问答幻觉、文档解析效果评估、知识库搭建、多语言文档翻译等问题。通过可视化解析工具 TextIn ParseX、向量化模型 acge-embedding 和文档解析测评工具 markdown_tester,百宝箱提升了文档处理的效率和精确度,适用于多种文档格式和语言环境,助力企业实现高效的信息管理和业务支持。
从数据提取到管理:合合信息的智能文档处理全方位解析【合合信息智能文档处理百宝箱】
|
存储 人工智能 分布式计算
Parquet 文件格式详解与实战 | AI应用开发
Parquet 是一种列式存储文件格式,专为大规模数据处理设计,广泛应用于 Hadoop 生态系统及其他大数据平台。本文介绍 Parquet 的特点和作用,并演示如何在 Python 中使用 Pandas 库生成和读取 Parquet 文件,包括环境准备、生成和读取文件的具体步骤。【10月更文挑战第13天】
3612 60
|
数据挖掘 BI
数字化产科管理平台解决方案
数字化产科管理平台旨在解决产科应用痛点,通过建设孕产保健信息系统,实现保健、临床及上级管理系统间的互联互通,打造一体化服务模式。平台涵盖门诊、住院、统计三大模块,实现从建档到产后管理的全生命周期管理,提升高危管理效率,减少人工文书工作,优化资源配置,并提供精准的统计分析功能,助力科研与业务发展。
469 0
数字化产科管理平台解决方案
|
缓存 安全 小程序
从基础到进阶:掌握Java中的Servlet和JSP开发
【6月更文挑战第23天】Java Web开发中的Servlet和JSP是关键技术,用于构建动态网站。Servlet是服务器端小程序,处理HTTP请求,生命周期包括初始化、服务和销毁。基础Servlet示例展示了如何响应GET请求并返回HTML。随着复杂性增加,JSP以嵌入式Java代码简化页面创建,最佳实践提倡将业务逻辑(Servlet)与视图(JSP)分离,遵循MVC模式。安全性和性能优化,如输入验证、HTTPS、会话管理和缓存,是成功应用的关键。本文提供了一个全面的学习指南,适合各级开发者提升技能。
354 7
|
安全 API 数据安全/隐私保护
​验证码邮件API有哪些?分析最好的3个接口平台
验证码邮件API如AOKSend、SendGrid和Mailgun是用户身份验证的关键工具。这些API提供高效、可靠的邮件发送服务,确保验证码的安全传输。AOKSend以其快速发送和易用性著称,SendGrid则以全面功能和扩展性见长,而Mailgun则以灵活性和高送达率闻名。开发者可以根据需求选择合适的API,通过示例代码轻松集成到应用中,增强安全性和用户体验。