【树与二叉树】堆的时间复杂度详解以及堆的应用—堆排序、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

相关文章
|
前端开发
开发过程中遇到过的docx、pptx、xlsx、pdf文件预览多种方式
开发过程中遇到过的docx、pptx、xlsx、pdf文件预览多种方式
273 0
|
存储 DataWorks Unix
Dataworks数据集成之“文本数据”
Dataworks不是支持文本数据导入么?为什么Excel数据不能导入?CSV文件不就是Excel文件么?关于这些问题,我整理了一篇文章进行解释。
1264 2
|
存储 Linux 开发工具
掌握 Git 命令:每个开发者的必备技能
无论团队项目还是个人开发,掌握 Git 命令都是必备技能。本文介绍 Git 的基本概念与命令,如初始化仓库 (`git init`)、添加文件 (`git add`)、提交更改 (`git commit`)、检出分支 (`git checkout`)、合并分支 (`git merge`) 等,还分享了高级技巧如查看差异 (`git diff`)、撤销提交 (`git revert`)、修复合并冲突 (`git mergetool`) 和使用别名简化命令 (`git config --global alias.ci commit`)。
|
11月前
|
安全 Java 应用服务中间件
SpringBoot:CORS是什么?SpringBoot如何解决跨域问题?
CORS是Web开发中常见且重要的机制,SpringBoot通过提供注解、全局配置和过滤器等多种方式来解决跨域问题。选择适合的方式可以帮助开发者轻松处理跨域请求,提高应用的灵活性和安全性。
637 2
|
NoSQL Java Redis
Spring Boot 监听 Redis Key 失效事件实现定时任务
Spring Boot 监听 Redis Key 失效事件实现定时任务
292 0
|
网络协议 定位技术 Windows
Windows Server 2019 DNS服务器搭建
Windows Server 2019 DNS服务器搭建
470 1
|
开发者 UED
Axure“Web高端交互元件库”:产品与设计的得力助手
这套“Web高端交互元件库”精心构建了四大板块内容,分别是登陆首页集合、Web框架集合、表单元件集合以及主流后台组件。每一板块都包含了大量实用且美观的交互元件,设计师与开发者可以根据具体项目需求,快速找到并应用这些元件,从而大大提升工作效率。
253 1
|
存储 关系型数据库 MySQL
MySQL 索引优化:深入探索自适应哈希索引的奥秘
MySQL 索引优化:深入探索自适应哈希索引的奥秘
|
数据采集 供应链 安全
利用大数据优化业务流程:策略与实践
【5月更文挑战第11天】本文探讨了利用大数据优化业务流程的策略与实践,包括明确业务目标、构建大数据平台、数据采集整合、分析挖掘及流程优化。通过实例展示了电商和制造企业如何利用大数据改进库存管理和生产流程,提高效率与客户满意度。随着大数据技术进步,其在业务流程优化中的应用将更加广泛和深入,企业需积极采纳以适应市场和客户需求。
|
中间件 开发者
史上最详细的Gin中间件使用教程
1.概述 Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
1480 90
史上最详细的Gin中间件使用教程