数据结构-堆和堆排序-TopK问题

简介: 数据结构-堆和堆排序-TopK问题

1.堆的定义

堆是以二叉树的结构方式,所存储的一维数组。

逻辑结构:二叉树

物理结构:一维数组

堆的特性:

  • 堆中某个节点的值总是不大于或不小于它的父亲节点的值。根结点值总是大于或等于其左右孩子结点的值,叫大根堆。根节点总是小于或等于其左右孩子结点的值,叫小根堆。
  • 堆总是一棵完全二叉树。

如下图堆的示例:

2.堆的实现接口(大堆)

2.1 堆结构体定义

使用一维数组来存储堆

typedef int HPDataType;
typedef struct Heap
{
  HPDataType* data;
  int size;
  int capacity;
}Heap;

2.2 堆的初始化与销毁

//堆初始化
void HeapInit(Heap* pa)
{
  assert(pa);
  pa->data = NULL;
  pa->capacity = pa->size = 0;
}
//销毁堆
void HeapDestroy(Heap* pa)
{
  assert(pa);
  free(pa->data);
  pa->data = NULL;
  pa->capacity = pa->size = 0;
}

2.3 堆的向上调整算法和插入

堆进行插入的时候,接口是进行尾插的,这样能保证不会破坏之前的堆的结构。插入过程大概如下步骤:假设输入数据为:

算法思想:由需要调整的结点开始,把它当作孩子结点child。计算出父亲节点parent,将孩子和父亲的值作比较,如果父亲小于孩子,择交换两结点的值。并且令child = parent.一直重复,直到调整到根结点为止。

//向上调整
void AdjustUp(HPDataType* 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 HeapPush(Heap* pa, HPDataType x)
{
  assert(pa);
  //检查容量
  if (pa->size == pa->capacity)
  {
    int newcapacity = (pa->capacity == 0 ? 4 : pa->capacity * 2);
    HPDataType* tem = (HPDataType*)realloc(pa->data, sizeof(HPDataType) * newcapacity);
    //申请失败,终止程序
    if (tem == NULL)
    {
      perror("realloc failed");
      exit(-1);
    }
    pa->data = tem;
    pa->capacity = newcapacity;
  }
  //插入数据
  pa->data[pa->size] = x;
  pa->size++;
  //向上调整
  AdjustUp(pa->data,pa->size-1);
}

2.4 堆的向下调整算法和删除堆顶元素

堆进行删除时候,不能直接删除堆顶元素,这样会把数据整体前移,破坏了堆的结构。规定:把堆顶元素和最后一个元素交换。然后将size-1,就可完成删除任务。只需利用向下调整算法,就能在O(logN)的时间内完成恢复堆的任务。

算法思想:从堆顶元素开始向下调整,首先比较两个孩子结点,找出较大的结点。与父亲节点比较,若孩子结点的值小于父亲结点的值,则说明符合堆的结构,调整完成。否则将父亲结点和孩子结点的值交换,并且令parent = child(继续向下调整),直到符合堆结构或者没有叶子结点,调整完成。

//向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{
  int child = 2 * parent + 1; //左孩子下标
  //如果有孩子,必须小于size
  while (child < size)
  {
    //选出较大的孩子(右孩子不能超出范围)
    if (child + 1 < size && 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(Heap* pa)
{
  assert(pa);
  assert(pa->size > 0);
  //换到最低层
  Swap(&(pa->data[0]), &(pa->data[pa->size-1]));
  pa->size--;
  //向下调整
  AdjustDown(pa->data,pa->size,0);
}

2.5 堆的其他接口(调整堆递归版本)

//获取堆顶元素
HPDataType HeapTop(Heap* pa)
{
  assert(pa);
  assert(pa->size > 0);
  return pa->data[0];
}
//堆的数据个数
int HeapSize(Heap* pa)
{
  assert(pa);
  return pa->size;
}
//堆的判空
bool HeapEmpty(Heap* pa)
{
  assert(pa);
  if (pa->size > 0)
  {
    return false;
  }
  return true;
}
//向上调整(递归版本)
void AdjustUp(HPDataType* a, int child)
{
  int parent = (child - 1) / 2;
  if(child > 0)
  {
    //不符合堆定义
    if (a[child] > a[parent])
    {
      Swap(&a[child], &a[parent]);
      AdjustUp(a, parent); //把父亲当作儿子传进去
    }
  }
}
//向下调整(递归版本)
void AdjustDown(HPDataType* a, int size, int parent)
{
  int child = 2 * parent + 1; //左孩子下标
  //如果有孩子,必须小于size
  if (child < size)
  {
    //选出较大的孩子(右孩子不能超出范围)
    if (child + 1 < size && a[child + 1] > a[child])
    {
      child++;
    }
    //孩子大于父亲就交换,不大于就退出
    if (a[child] > a[parent])
    {
      Swap(&a[child], &a[parent]);
      AdjustDown(a, size, child); //把孩子当成父亲传进去
    }
  }
}

3.建堆效率问题分析

3.1 向上建堆

因此向上建堆的时间复杂度为O(NlogN)。

3.2 向下建堆

因此向下建堆的时间复杂度为O(N)。

4. 堆排序(升序)

  1. 利用堆的Top和Pop接口来实现排序,但是需要申请空间来保存Pop之后的数据。并且每次都需要实现堆的接口比较麻烦。
  2. 利用Pop的思想,将最大的元素和最后一个元素互换。每次交换size–,不断循环这个动作,直到size大小为0。

本文采用第二种方法:

//升序
void HeapSort(HPDataType* a, int size)
{
  //先建堆
  for (int i = (size - 1 - 1) / 2; i >= 0; i--)
  {
    //向下调整(从后往前)
    AdjustDown(a, size, i);
  }
  //再一个一个调整
  int end = size - 1; //记录堆的最后一个位置
  while (end > 0)
  {
    //第一个和最后一个位置交换
    Swap(&a[0], &a[end]);
    //向下调整
    AdjustDown(a, end, 0);
    end--;
  }
}

分析:第一部分向下建堆时间复杂度为0(N),堆排序的时间复杂度为O(NlogN)。

综上:堆排序的时间复杂度为O(NlogN)。

5.TopK问题分析

题目背景:要求找出100万个数字里最大的前10个数。

方法1:

建立N个数的大根堆,每次找出最大的数字,重复10次即可。但是空间效率极高。

时间复杂度O(N+KlogN)。

方法2:

建立K个数字的小根堆,依次遍历剩下数据,如果数值大于堆顶数据,则进堆,如果数值小于堆顶数据,则跳过。遍历完,堆中的数据就是最大的前10个。

时间复杂度O(K+(N-K)logK)。

方法2的效率更高一点,且占用空间小:

//找前K个最大的数字
void PrintTopK(int* a, int size, int K)
{
  int* tem = (int*)malloc(sizeof(int) * K);
  assert(tem);
  //建立前K个数字的小堆
  for (int i = 0; i < K; i++)
  {
    tem[i] = a[i];
    if (tem == NULL)
    {
      exit(-1);
    }
    AdjustUp(tem, i);
  }
  //剩下的数字依次和堆顶元素比较
  int j = K;
  while (j < size)
  {
    if (a[j] >= tem[0])
    {
      tem[0] = a[j];
      AdjustDown(tem, K, 0);
    }
    j++;
  }
  for (int i = 0; i < K; i++)
  {
    printf("%d ", tem[i]);
  }
}

总结:以上就是堆相关的问题介绍,后续还会继续更新数据结构相关的知识。敬请期待。💞

目录
相关文章
|
19天前
|
存储 算法 Java
散列表的数据结构以及对象在JVM堆中的存储过程
本文介绍了散列表的基本概念及其在JVM中的应用,详细讲解了散列表的结构、对象存储过程、Hashtable的扩容机制及与HashMap的区别。通过实例和图解,帮助读者理解散列表的工作原理和优化策略。
29 1
散列表的数据结构以及对象在JVM堆中的存储过程
|
21天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
60 16
|
1月前
|
存储 JavaScript 前端开发
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
为什么基础数据类型存放在栈中,而引用数据类型存放在堆中?
71 1
|
1月前
|
算法 搜索推荐
数据结构与算法学习十八:堆排序
这篇文章介绍了堆排序是一种通过构建堆数据结构来实现的高效排序算法,具有平均和最坏时间复杂度为O(nlogn)的特点。
73 0
数据结构与算法学习十八:堆排序
|
1月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
1月前
|
存储 算法 Java
【用Java学习数据结构系列】用堆实现优先级队列
【用Java学习数据结构系列】用堆实现优先级队列
31 0
|
1月前
|
存储 算法
【数据结构】二叉树——顺序结构——堆及其实现
【数据结构】二叉树——顺序结构——堆及其实现
|
22天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
109 9
|
13天前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
22 1
|
16天前
|
存储 算法 Java
数据结构的栈
栈作为一种简单而高效的数据结构,在计算机科学和软件开发中有着广泛的应用。通过合理地使用栈,可以有效地解决许多与数据存储和操作相关的问题。