【数据结构】TopK,堆排序, --堆的初始化与应用

简介: 相信经过上一阶段的学习(二叉树概念)大家都对二叉树有了一个初步的印象,接下里我们将介绍一下二叉树在内存中一般如何存储.

Halo,这里是Ppeua。平时主要更新C语言,C++,数据结构算法......感兴趣就关注我吧!你定不会失望。


🌈个人主页:主页链接


🌈算法专栏:专栏链接


    我会一直往里填充内容哒!


🌈LeetCode专栏:专栏链接


目前在刷初级算法的LeetBook 。若每日一题当中有力所能及的题目,也会当天做完发出


🌈代码仓库:Gitee链接


🌈点击关注=收获更多优质内容🌈


809c89c988374fe381cdefdcb34edbf4.jpg


书接上回,本章节也是关于树的内容:二叉树的存储简介,堆的初始化以及应用


1.二叉树的存储结构


相信经过上一阶段的学习(二叉树概念)大家都对二叉树有了一个初步的印象,接下里我们将介绍一下二叉树在内存中一般如何存储.

1.1链式存储


不同于上章所讲的用一个节点存储子结点,另一个节点用来存储兄弟节点,


因为二叉树中的子节点个数是确定的:一个到两个,最多两个.所以我们可以直接创建两个指针记录下其子结点的信息.


二叉链式存储:


就是用两个指针信息来记录其左右子节点的地址.再用一个变量来存储数据.一般是这样写的


typedef int TDatatype;
typedef struct tree
{
    TDatatype val;
    tree *left;
    tree *right;
}tree;


形象的结构如图所示:


ed97ad0024fa4014b3034d659fb1ccc9.jpg



二叉链式 存储一般是比较常见的用在二叉树上.但还有一种结构,在后期学习到红黑树会用到的结构


三叉链式存储:


与上方不同,其是用两个指针存储左右儿子,再用一个指针来存储父节点,所以叫做三叉链式存储


typedef int TDatatype;
typedef struct tree
{
    TDatatype val;
    tree *left;
    tree *right;
    tree *parent;
}tree;


形象的结构如图所示:


56e741fdc699413da23686abfa756f41.jpg


1.2顺序存储结构


与链表的相同,二叉树也可以通过数组来存储信息,也就是顺序存储.


如果是二叉树可以用此结构存储,因为其他类型的树使用这种结构会造成大量的空间浪费.

就会出现这种情况


c2adb353c157491eae463efe216d44fe.jpg


所以不推荐使用


2.堆


2.1堆的性质


先来了解下堆的性质


其本质就是一颗具有特殊属性的完全二叉树

               若其父节点都大于等于子节点 则称为大顶堆

               若其父节点都小于等于子节点 则称为小顶堆


下面这些就是大/小顶堆:


509665bbe0214c2c9b7170727c2f01ac.jpg


2.2堆的数据结构


刚刚提到堆是一颗完全二叉树,那么其结构具有二叉树的顺序存储结构的特点.


typedef int TDataType;
struct tree{
    int size;
    int capacity;
    TDataType *a;
};


其中 size用来存储当前堆中有多少个元素. capacity来判定当前数组是否需要扩容

       而a自然是用来存储数据的.这与栈的顺序结构十分相似.

2.3堆的初始化


与链表等结构相同,使用堆前也需要对其初始化.这里没什么好说的,与其他数据结构都相同


void HeapInit(Heap* hp)
{
  hp->size = 0;
  hp->a = (HDataType*)malloc(sizeof(HDataType) * 4);
  if (hp->a == NULL)
  {
    perror("malloc failed");
    return;
  }
  hp->capacity = 4;
}


2.4堆的向上调整


刚刚说过,堆有两种(大顶堆与小顶堆)


想要让一个无序的数组变成这样的,肯定需要对每一个插入的数进行调整.


其中就有向上调整向下调整两种方式,这里先来讲讲向上调整


5eb60d9dc80745f4886317fd40c8ac66.jpg


将1插入到一个小顶堆,其做的变化如图所示。


先是与父节点比较,父节点(i-1/2)比其大(大顶堆则相反)则交换(不满足小顶堆的规则).

直到child=0时停下

void UpAdjust(HDataType* hp, int child)
{
  int parent = (child - 1) / 2;
  while (child > 0)
  {
    if (hp[parent] > hp[child])
    {
      Swap(&hp[parent], &hp[child]);
      child = parent;
      parent = (parent - 1) / 2;
    }
    else
      break;
  }
}


2.4堆的向下调整


这里介绍下向下调整,向下调整是建一个堆的必要步骤


cba26cf42754449dbb00b72b6c50e4a1.jpg


例如将9插入到一个小顶堆中,需要经历的是这几步


     将9的两个子节点进行比较,因为是小顶堆,所以需要挑出其最小的部分,与9进行交换

       之后一步步向下调整即可.当child>=n时停止

void DownAdjust(HDataType* hp, int n,int parent)
{
  int child = parent * 2 + 1;
  while (child < n)
  {
    if (child+1<n&&hp[child] > hp[child + 1])
    {
      child++;
    }
    if (hp[child] < hp[parent])
    {
      Swap(&hp[child], &hp[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else break;
  }
}


向下调整的效率比向上调整的效率高.这个道理非常的简单,倒数第二层是除叶子节点外最多节点的一层,其向下调整最多只需要调整一层.往上节点越少,调整越多.


而向上调整则刚好相反,第一层节点最少,最多需要调整一次,但最后一层节点最多,需要调整的次数也是最多的.所以一般用向下调整来建堆


2.5堆的插入


有了之前的两个利器,接下来就十分的简单了.


void HeapPush(Heap* hp, HDataType x)
{
  if (hp->size == hp->capacity)
  {
    HDataType* tmp = (HDataType*)realloc(hp->a,sizeof(HDataType) * hp->capacity * 2);
    if (tmp == NULL)
    {
      perror("malloc failed");
      return;
    }
    hp->a = tmp;
    hp->capacity *= 2;
  }
  hp->a[hp->size++] = x;
  UpAdjust(hp->a,hp->size-1);
}


这与栈的插入十分相同,这里就不过多赘述了.值得注意的是最后一步,


UpAdjust的参数为:堆的数组与需要调整的子节点


2.6堆的删除


void HeapDelete(Heap* hp)
{
  if (hp->size <= 0)return;
  Swap(&hp->a[hp->size - 1], &hp->a[0]);
  hp->size--;
  DownAdjust(hp->a, hp->size - 1, 0);
}


首先检查是否为空.

之后交换最后一个数据与第一个数据.将size--,释放最后一段空间,再将刚刚交换的数字进行向下调整


9a1ade98144047a9b9854f74b62288b4.jpg


2.7堆的销毁,大小,Empty


这与之前所学相同 不过多赘述


需要注意Malloc了多少空间.就要释放多少空间


void HeapDestory(Heap* hp)
{
  free(hp->a);
  hp->capacity = hp->size = 0;
  free(hp);
}
HDataType HeapTop(Heap* hp)
{
  return hp->a[0];
}
int HeapSize(Heap* hp)
{
  return hp->size;
}
int HeapEmpty(Heap* hp)
{
  return hp->size == 0;
}


3.堆排序


其核心仍然是DownAdjust,先通过DownAdjust建一个堆,之后再删除堆顶元素,向下调整,直到size为0


虽然size为0,但是刚刚的数据依照大/小放到最后一位(大小堆决定),所以此时读取数组就变成了有序的.


注意:要排升序需要建大堆(因为大的被放到了最后一个),排降序建小堆


void heapSort(HDataType *a,int n)
{
  /*for (int i = 1; i < n; i++)
  {
    UpAdjust(a, i);
  }*/
  for (int i = (n - 1 - 1 / 2); i >= 0; i--)
  {
    DownAdjust(a, n,i);
  }
  int end = n - 1;
  while (end > 0)
  {
    Swap(&a[end], &a[0]);
    end--;
    DownAdjust(a, end,0);
  }
} 


因为向下建堆效率更高,所以采用这种方法.


4.TopK问题


给你一个无穷大的数组,想要让你找到前k个最大(最小的数组)如何去找?

十分的简单,先去将这个数组的前十个数据建堆.若找最大的,则建小堆,

将之后的每一个元素都与堆顶元素进行比较(因为堆顶是这十个数中最小的) ,若比他大,则说明堆中至少有一个数(堆顶)需要被换掉,将堆顶等于刚刚这个数,之后执行向下调整即可.


void TopK(const char* f, int top)
{
  int* topk = (int*)malloc(sizeof(int) * top);
  FILE* fl = fopen(f, "r");
  for(int i=0;i<top;++i)
  {
    fscanf(fl, "%d", &topk[i]);
  }
  for (int i = (top-2)/2; i>=0; --i)
  {
    DownAdjust(topk, top, i);
  }
  int val = 0;
  int ret = fscanf(fl, "%d", &val);
  while (ret != EOF)
  {
    if (val > topk[0])
    {
      topk[0] = val;
      DownAdjust(topk, top, 0);
    }
    ret = fscanf(fl, "%d", &val);
  }
  for (int i = 0; i < top; i++)
    printf("%d \n", topk[i]);
  printf("\n");
  free(topk);
  fclose(fl);
}
int main()
{
  const char* f = "data.txt";
  srand(time(NULL));
  FILE* fl = fopen(f, "w");
  for (int i = 0; i < N; i++)
  {
    fprintf(fl, "%d\n", rand() % 10000);
  }
  TopK(f, 10);
  return 0;
}


这里采用了文件的方式生成一万个随机数


fprintf(文件指针,数据格式,数据) 可以这样理解 printf将内容打印到了屏幕上,而fprintf打印到了文件里

fscanf(文件指针,数据格式,存入地址) 可以这样理解 scanf将键盘键入的内容存到了变量,而fscanf将文件中的内容存入到了变量,若读到的为空则返回EOF


🌈本篇博客的内容【TopK,堆排序, --堆的初始化与应用】已经结束。


🌈若对你有些许帮助,可以点赞、关注、评论支持下博主,你的支持将是我前进路上最大的动力。


🌈若以上内容有任何问题,欢迎在评论区指出。若对以上内容有任何不解,都可私信评论询问。


🌈诸君,山顶见!

目录
相关文章
|
5天前
|
算法 安全 测试技术
golang 栈数据结构的实现和应用
本文详细介绍了“栈”这一数据结构的特点,并用Golang实现栈。栈是一种FILO(First In Last Out,即先进后出或后进先出)的数据结构。文章展示了如何用slice和链表来实现栈,并通过golang benchmark测试了二者的性能差异。此外,还提供了几个使用栈结构解决的实际算法问题示例,如有效的括号匹配等。
golang 栈数据结构的实现和应用
|
17天前
|
存储 Java
【数据结构】优先级队列(堆)从实现到应用详解
本文介绍了优先级队列的概念及其底层数据结构——堆。优先级队列根据元素的优先级而非插入顺序进行出队操作。JDK1.8中的`PriorityQueue`使用堆实现,堆分为大根堆和小根堆。大根堆中每个节点的值都不小于其子节点的值,小根堆则相反。文章详细讲解了如何通过数组模拟实现堆,并提供了创建、插入、删除以及获取堆顶元素的具体步骤。此外,还介绍了堆排序及解决Top K问题的应用,并展示了Java中`PriorityQueue`的基本用法和注意事项。
23 5
【数据结构】优先级队列(堆)从实现到应用详解
|
28天前
|
存储 机器学习/深度学习
【数据结构】二叉树全攻略,从实现到应用详解
本文介绍了树形结构及其重要类型——二叉树。树由若干节点组成,具有层次关系。二叉树每个节点最多有两个子树,分为左子树和右子树。文中详细描述了二叉树的不同类型,如完全二叉树、满二叉树、平衡二叉树及搜索二叉树,并阐述了二叉树的基本性质与存储方式。此外,还介绍了二叉树的实现方法,包括节点定义、遍历方式(前序、中序、后序、层序遍历),并提供了多个示例代码,帮助理解二叉树的基本操作。
43 13
【数据结构】二叉树全攻略,从实现到应用详解
|
29天前
|
存储 Java 索引
【数据结构】链表从实现到应用,保姆级攻略
本文详细介绍了链表这一重要数据结构。链表与数组不同,其元素在内存中非连续分布,通过指针连接。Java中链表常用于需动态添加或删除元素的场景。文章首先解释了单向链表的基本概念,包括节点定义及各种操作如插入、删除等的实现方法。随后介绍了双向链表,说明了其拥有前后两个指针的特点,并展示了相关操作的代码实现。最后,对比了ArrayList与LinkedList的不同之处,包括它们底层实现、时间复杂度以及适用场景等方面。
43 10
【数据结构】链表从实现到应用,保姆级攻略
|
6天前
|
存储 数据安全/隐私保护 Python
Python常用数据结构——字典的应用
Python常用数据结构——字典的应用
10 2
|
7天前
|
JSON 前端开发 JavaScript
一文了解树在前端中的应用,掌握数据结构中树的生命线
该文章详细介绍了树这一数据结构在前端开发中的应用,包括树的基本概念、遍历方法(如深度优先遍历、广度优先遍历)以及二叉树的先序、中序、后序遍历,并通过实例代码展示了如何在JavaScript中实现这些遍历算法。此外,文章还探讨了树结构在处理JSON数据时的应用场景。
一文了解树在前端中的应用,掌握数据结构中树的生命线
|
25天前
|
存储 算法 C语言
数据结构基础详解(C语言):单链表_定义_初始化_插入_删除_查找_建立操作_纯c语言代码注释讲解
本文详细介绍了单链表的理论知识,涵盖单链表的定义、优点与缺点,并通过示例代码讲解了单链表的初始化、插入、删除、查找等核心操作。文中还具体分析了按位序插入、指定节点前后插入、按位序删除及按值查找等算法实现,并提供了尾插法和头插法建立单链表的方法,帮助读者深入理解单链表的基本原理与应用技巧。
|
25天前
|
存储 C语言
数据结构基础详解(C语言): 树与二叉树的应用_哈夫曼树与哈夫曼曼编码_并查集_二叉排序树_平衡二叉树
本文详细介绍了树与二叉树的应用,涵盖哈夫曼树与哈夫曼编码、并查集以及二叉排序树等内容。首先讲解了哈夫曼树的构造方法及其在数据压缩中的应用;接着介绍了并查集的基本概念、存储结构及优化方法;随后探讨了二叉排序树的定义、查找、插入和删除操作;最后阐述了平衡二叉树的概念及其在保证树平衡状态下的插入和删除操作。通过本文,读者可以全面了解树与二叉树在实际问题中的应用技巧和优化策略。
|
26天前
|
Java
【数据结构】栈和队列的深度探索,从实现到应用详解
本文介绍了栈和队列这两种数据结构。栈是一种后进先出(LIFO)的数据结构,元素只能从栈顶进行插入和删除。栈的基本操作包括压栈、出栈、获取栈顶元素、判断是否为空及获取栈的大小。栈可以通过数组或链表实现,并可用于将递归转化为循环。队列则是一种先进先出(FIFO)的数据结构,元素只能从队尾插入,从队首移除。队列的基本操作包括入队、出队、获取队首元素、判断是否为空及获取队列大小。队列可通过双向链表或数组实现。此外,双端队列(Deque)支持两端插入和删除元素,提供了更丰富的操作。
29 0
【数据结构】栈和队列的深度探索,从实现到应用详解
|
1月前
|
存储 程序员 C语言
堆和栈之间有什么区别
【9月更文挑战第1天】堆和栈之间有什么区别
116 0