数据结构之堆的应用

简介: 数据结构之堆的应用

在我们学习了堆之后我们知道,无论是大堆还是小堆,堆顶的元素要么最大,要么最小。

对于堆插入的时间复杂度为O(logn)也就是向上调整算法的复杂度,删除一个堆中的元素为O(logn),堆在我们日常生活中还是常用到的。

1.top k问题(优质筛选问题)

什么是top k问题?即求数据结合中前k个最大元素或最小元素,一般情况下数据量较大。

假设N是100万,k是100,求100 万中的前10个最大数,当数足够大,排序已经无法实现了,空间太大,无法内存中开辟,在磁盘上开辟(但磁盘中无法建堆),而且磁盘运行速度很慢。

如:求得大量数字中的前k个大或前k个小的数,普通的算法时间复杂度较高,效率较慢,数量太大,无法寻找。利用堆的特性,算法效率高,面对数量大的数据也适用。

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

前k个最大元素  建大堆

前k个最小元素  建小堆


c327611d9259434cad2b8de0b64e51a1.png 我们这里设置随机数10个,这里你可以改变,越大越能体现堆的功能,找出其中的最大五个,

思路就是先读取五个数进入数组里,之后再从第k+1个数开始与堆顶比较,比堆顶大就替换,每一次都要向下调整,确保是一个小堆,到最后最大的五个数就是小堆的五个数,第一个是五个里最小的。

#include<stdlib.h> 
#include<stdio.h>
#include<time.h>
void Adjustdown1(int* a, int size, int parent)
{
  int child = parent * 2 + 1;
  while (child < size)
  {
    if ((child + 1 < size) && a[child + 1] < a[child])//若右孩子存在且小于左孩子
    {
      ++child;
    }
    if (a[child] < a[parent])
    {
      //交换
      int tmp = a[child];
      a[child] = a[parent];
      a[parent] = tmp;
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
void creatnDATA()
{
  int n = 10;
  srand(time(NULL));
  const char* file = "data.txt";
  FILE* fp = fopen(file, "w");
  if (fp == NULL)
  {
    perror("fopen fail");
    return;
  }
  for (size_t i = 0; i < n; i++)
  {
    int x = rand() % 100000;
    fprintf(fp, "%d\n", x);
  }
  fclose(fp);
}
void printtopk(int k)
{
  const char* file = "data.txt";
  FILE* fp = fopen(file, "r");
  if (fp == NULL)
  {
    perror("fopen fail");
    return;
  }
  int* kminheap = (int*)malloc(sizeof(int) * k);
  //从文件中读k个数
  for (size_t i = 0; i < k; i++)
  {
    fscanf(fp, "%d", &kminheap[i]);
  }
  //建小堆
  for (int i = (k - 1 - 1) / 2; i >= 0; i--)
  {
    Adjustdown1(kminheap, k, i);
  }
  int val = 0;
  //比较 ,如果比堆顶大,则需要交换,之后再向下调整。
  while (!feof(fp))
  {
    //从第k+1个数处再开始读
    fscanf(fp, "%d", &val);
    //val用来存放读的数 
    if (val > kminheap[0])
    {
       kminheap[0]=val;
      Adjustdown1(kminheap, k, 0);
    }
  }
  for (int i = 0; i < k; i++)
  {
    printf("%d ", kminheap[i]);
  }
  printf("\n");
}
int main()
{
  creatnDATA();
  printtopk(5);
  return 0;
}

2.堆排序

正常的思维,所谓对堆排序就是用堆这个数据结构来实现排序,思路也很简单,取堆头存入数组,向下调整,在出堆,循环往复。

如下,在有堆的前提,我们可以实现堆排序:

//前提有堆的实现
void heapsort(int *a,int n)
{
  HP f;
  Heapinit(&f);
  for (int i = 0; i < n; i++)
  {
    Heappush(&f, a[i]);
  }
      int i = 0;
  while (!HeapEmpty(&f))
  {
    //进行排序
    int top = HeapTop(&f);
    a[i++] = top;
    Heappop(&f);
  }
}
int main()
{
  int a[] = {10,6,9,3,5,7,65};
  heapsort(a,sizeof(a)/sizeof(int));
  for (int i=0; i < sizeof(a) / sizeof(int); i++)
  {
    printf("%d ", a[i]);
  }
}

但我们也会发现其空间复杂度较高,需要拷贝数据,虽然这个堆排序效率还行,但是我们又看到想要以这种方法实现我们还的需要实现堆的所有操作!!!!这太麻烦了,那这种排序又有谁会用呢?

实则不然,真正的堆排序是利用堆的思路,我们是把传进来这个数组直接建堆,省去一部分操作,提高效率。因此建堆又有两种方式:

1.向上调整建堆

给出排序的数组,以此来建堆,根据堆的结构  

建小堆-----最后排序为降序


c4af32149fc1459db7faef6142eb4e6e.png

void Adjustup1(HPDATAtype* a, int child)
{
  //根据孩子找父亲
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (a[child] < a[parent])
    {
      HPDATAtype p = a[child];
      a[child] = a[parent];
      a[parent] = p;
      child = parent;
      parent = (child - 1) / 2;
    }
    else
    {
      break;
    }
  }
}
void Adjustdown1(HPDATAtype* a, int size,int parent)
{
  int child =parent * 2 +1;
  while (child<size)
  {
    if ((child+1<size)&&a[child + 1] <a[child])//若右孩子存在且小于左孩子
    {
      ++child;
    }
    if (a[child] < a[parent])
    {
      //交换
      int tmp = a[child];
      a[child] = a[parent];
      a[parent] = tmp;
      parent = child;
      child = parent * 2 + 1;
    }else
    {
      break;
    }
  }
}
void heapsort1(int* a, int n)
{
  //向上调整建堆
  //从第一个元素开始就向上调整,保证前面都是堆结构,建小堆
  for (int i = 0; i < n; i++)
  {
    Adjustup1(a, i);
  }
  //在调整  ,选出一个个次小的数
  int end = n - 1;
  while (end > 0)
  {
    //把最小的数据交换到最后一个位置
    int tmp = a[0];
    a[0] = a[end];
    a[end] = tmp;
  Adjustdown1(a, end, 0);
  --end;
  }
}

建大堆------最后排序为升序


7fedda6da49a4ea0a0079600d2772937.png

void Adjustup2(HPDATAtype*a, int child)
{
  //根据孩子找父亲
  int parent = (child - 1) / 2;
  while (child>0)
  {
    if ( a[child]>a[parent])
    {
      HPDATAtype p = a[child];
      a [child] = a[parent];
      a[parent]=p;
      child = parent;
      parent = (child - 1) / 2;
    }
    else 
    {
      break;
    }
  }
}
void Adjustdown2(HPDATAtype* a, int size, int parent)
{
  int child = parent * 2 + 1;
  while (child < size)
  {
    if ((child + 1 < size) && a[child + 1] > a[child])//若右孩子存在且小于左孩子
    {
      ++child;
    }
    if (a[child] > a[parent])
    {
      //交换
      int tmp = a[child];
      a[child] = a[parent];
      a[parent] = tmp;
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
void heapsort2(int* a, int n)
{
  //向上调整建堆
  //从第一个元素开始就向上调整,保证前面都是堆结构,建大堆
  for (int i = 0; i < n; i++)
  {
    Adjustup2(a, i);
  }
  //在调整  ,选出一个个次小的数
  int end = n - 1;
  while (end > 0)
  {
    //把最小的数据交换到最后一个位置
    int tmp = a[0];
    a[0] = a[end];
    a[end] = tmp;
    Adjustdown2(a, end, 0);
    --end;
  }
}

利用向上调整算法建堆,时间复杂度O(N*logN),这里是以2为底N的对数。

2.向下调整建堆

建堆与向上调整建堆是相反的,向下调整建堆,从数组后面开始调整,也就是从二叉树后面开始从前调整,知道符合堆的特性。

向下调整建堆也分建大堆还是小堆,根据你设计向下调整算法时,取决于条件-孩子大与父亲还是父亲大于孩子。

这里建小堆,调整数组是下标从中间开始的。

建小堆--排序结果为降序

void Adjustdown1(HPDATAtype* a, int size,int parent)
{
  int child =parent * 2 +1;
  while (child<size)
  {
    if ((child+1<size)&&a[child + 1] <a[child])//若右孩子存在且小于左孩子
    {
      ++child;
    }
    if (a[child] < a[parent])
    {
      //交换
      int tmp = a[child];
      a[child] = a[parent];
      a[parent] = tmp;
      parent = child;
      child = parent * 2 + 1;
    }else
    {
      break;
    }
  }
}
//向下调整建堆
void heapsort4(int* a, int n)
{
  HP f;
  Heapinit(&f);
  //向下调整建小堆
  //与向生调整建堆相反,这里是从数组后面开始调整堆,也就是从二叉树的下面开始
  for (int i =(n-1-1)/2 ; i >=0; i--)
  {
    Adjustdown1(a, n, i);
  }
  int end = n-1;
  while (end>0)
  {
    //进行排序
    int tmp = a[0];
    a[0] = a[end];
    a[end] = tmp;
    Adjustdown1(a, end, 0);
    --end;
  }
}

建大堆---排序结果为升序

void Adjustdown2(HPDATAtype* a, int size, int parent)
{
  int child = parent * 2 + 1;
  while (child < size)
  {
    if ((child + 1 < size) && a[child + 1] > a[child])//若右孩子存在且小于左孩子
    {
      ++child;
    }
    if (a[child] > a[parent])
    {
      //交换
      int tmp = a[child];
      a[child] = a[parent];
      a[parent] = tmp;
      parent = child;
      child = parent * 2 + 1;
    }
    else
    {
      break;
    }
  }
}
void heapsort5(int* a, int n)
{
  HP f;
  Heapinit(&f);
  //向下调整建大堆
  //与向生调整建堆相反,这里是从数组后面开始调整堆,也就是从二叉树的下面开始
  for (int i = (n - 1 - 1) / 2; i >= 0; i--)
  {
    Adjustdown2(a, n, i);
  }
  int end = n - 1;
  while (end > 0)
  {
    //进行排序
    int tmp = a[0];
    a[0] = a[end];
    a[end] = tmp;
    Adjustdown2(a, end, 0);
    --end;
  }
}

向上与向下比较

根据计算我们发现向下调整建堆是优于向上调整建堆的,这里的对于向下调整算法实现复杂度是O(N),向上调整算法的时间复杂度为(N*logN),应为向下调整算法,就如同在数组中插入,而向上调整算法经过错位相减或等比数列法计算大概为N*logN)。

相关文章
|
1月前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
37 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
2月前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
47 1
|
2月前
|
存储 算法 C语言
通义灵码在考研C语言和数据结构中的应用实践 1-5
通义灵码在考研C语言和数据结构中的应用实践,体验通义灵码的强大思路。《趣学C语言和数据结构100例》精选了五个经典问题及其解决方案,包括求最大公约数和最小公倍数、统计字符类型、求特殊数列和、计算阶乘和双阶乘、以及求斐波那契数列的前20项和。通过这些实例,帮助读者掌握C语言的基本语法和常用算法,提升编程能力。
73 4
|
1月前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
67 16
|
2月前
|
机器学习/深度学习 存储 人工智能
数据结构在实际开发中的广泛应用
【10月更文挑战第20天】数据结构是软件开发的基础,它们贯穿于各种应用场景中,为解决实际问题提供了有力的支持。不同的数据结构具有不同的特点和优势,开发者需要根据具体需求选择合适的数据结构,以实现高效、可靠的程序设计。
81 7
|
2月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
83 1
|
2月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
2月前
探索数据结构:队列的的实现与应用
探索数据结构:队列的的实现与应用
|
2月前
|
存储
探索数据结构:单链表的实践和应用
探索数据结构:单链表的实践和应用
|
2月前
|
存储 测试技术
探索数据结构:顺序表的实现与应用
探索数据结构:顺序表的实现与应用