【数据结构】二叉树顺序结构及实现(二)

简介: 【数据结构】二叉树顺序结构及实现(二)

三.堆的应用:


1.堆排序:


 通过上面的学习我们发现,堆有排序的功能。但是如果我们要通过每次建堆的方式来排序,那还是挺浪费空间的。为什么不在原有的数组里面进行堆排序呢?

堆排序即利用堆的思想来进行排序,总共分为两个步骤:


建堆

升序:建大堆

降序:建小堆

利用堆删除思想来进行排序

 在这里会有同学问道,根据堆的性质,大堆不就是降序,小堆不就是升序吗?为什么升序要建大堆,降序要建小堆呢?


 其实啊,我们的堆在数组中的排序并不是完全的。对于升序,如果我们非要通过建小堆来排序,只能通过拿走堆顶,然后将剩下的元素进行重新建立大堆的方式来寻找第二小的元素,这样的时间效率会非常低,而且过程非常的麻烦。


a9decbab313eb96ae70fdb18818c0bad_1f3af13a506043a186a329fc4563d4fa.png


那么我们应该怎么做呢?


步骤1:建堆:


向上调整建堆:

 向上调整建堆就相当于堆的插入,在原数组的空间里,每插入一个数,就向上调整一遍,代码如下:


void Heapsort(HPDataType* a, int n)
{
  for (int i = 0; i < n; i++)
  {
  AdjustUp(a, i);
  }
}


当然我们可以来看看这种方法的时间复杂度:


0b3c60b0572649f4ebce07ffecfc3d5a_f8c639b0c4594855845a6ef553d69057.png


通过求和:


81acf454eb3f2bb6e66d99bb079918f8_c8fd4c37c0da41eda10af7c0743fce6e.png


通过错位相减法可以得到:


e61852a74e893b72b9291744d52834a9_2ec4ac8cff93441cb555d5e7b941dd7f.png


由此可见,向上调整建堆的时间复杂度为O(N*logN)


向下调整建堆:

 既然我们可以向上调整建堆,能不能向下调整建堆呢?答案是肯定的,只要我们找到最后一个叶子结点的父结点,把该结点及该结点以前的结点向下调整,这样就可以很好的建堆了。代码如下:


//向下调整建堆
  for (int i = (n - 2) / 2; i > 0; i--)
  {
  AdjustDown(a, n,i);
  }


时间复杂度:


8e507dc721b2da88541a8fa575a7d0b1_5c727befd8aa4e3bb059f07ce843699d.png


同样的我们通过求和可以得到以下结果:


a0f713e07b026bc5119c24a22b6dc5e3_10bd1e280f12426bb6d57c11c24f0035.png


通过错位相减得到:


61c1f1fc3f146b4221ef29de33a4f352_a52f9d91a9e34632b88bb7c99fb689e1.png


所以向下调整算法的时间复杂度为O(N)。


步骤2:堆删除思想:


 以升序为例,在我们建完大堆之后,将大堆的第一个元素和最后一个元素交换位置,让size- -,随后将堆进行向下调整(这里就是堆删除的思想)即可。在向下调整的过程中就会将第二大的元素调整到第一个元素,随后在将第一个元素和倒数第二个元素交换……以此循环即可实现排序:


void Heapsort(HPDataType* a, int n)
{
  //向上调整建堆
  for (int i = 0; i < n; i++)
  {
  AdjustUp(a, i);
  }
  //向下调整建堆
  for (int i = (n - 2) / 2; i > 0; i--)
  {
  AdjustDown(a, n,i);
  }
  //堆删除
  int end = n - 1;
  for (int i = end; i > 0; i--)
  {
  swap(&a[i], &a[0]);
  end--;
  AdjustDown(a, end, i);
  }
}




建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。


 其实上面的时间复杂度可以很明显的看出向下调整算法的O(N)更小,因为向上调整算法是更多的结点调整更多次,向下调整算法是更少的结点调整更多次,所以向下调整算法的时间复杂度更小。


2.TOP-K问题


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

比如:专业前10名、世界500强、富豪榜、游戏中前100的活跃玩家等。

对于Top-K问题,能想到的最简单直接的方式就是排序,但是:如果数据量非常大,排序就不太可取了(可能数据都不能全部加载到内存中)。


5fb821d7db06893ed1e2c5573bd941e5_9c3b026e4a86434cad142c80f8c7de3d.png


顺便来复习一下单位的换算:


8c58fad59f9e4e0dad6490005676e4d5_154f43e7752c415789b39694a9fdce64.png


最佳的方式就是用堆来解决,基本思路如下:


假设我们要从N个元素取最大/最小的K个元素


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

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

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

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

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


我们以寻找最大的前k个数为例:


我们先通过文件操作进行造数据:

忘记文件操作的朋友们可以看看这篇博客 文件操作(上)


void CreateNdata()
{
  const char* file = "data.txt";
  FILE* fq = fopen(file, "w");
  if (fq == NULL)
  {
  perror("malloc::fail");
  return;
  }
  srand(time(NULL));
  int n = 10000000;
  for (int i = 0; i < n; i++)
  {
  int ret = rand() % 10000;
  fprintf(fq, "%d\n", ret);
  }
  fclose(fq);
  free(fq);
}


大家会发现我们造的数据非常多,没错,就是要这种效果,现实生活中数据过多我们就不能用内存来存储,用文件来储存。


接下来我们将这些数据的前k个进行建堆,并将剩余的N-K个元素依次与堆顶元素来比较,不满足则替换堆顶元素。一定要记得建的是小堆!


void PrintTopK(const char* file, int k)
{
  // 1. 建堆--用a中前k个元素建小堆
  int* topk = (int*)malloc(sizeof(int) * k);
  assert(topk);
  FILE* fq = fopen(file, "r");
  if (fq == NULL)
  {
  perror("malloc:fail");
  return;
  }
  for (int i = 0; i < k; i++)
  {
  fscanf(fq, "%d", &topk[i]);
  }
  //建小堆
  for (int i = (k - 2) / 2; i >= 0; i--)
  {
  AdjustDown(topk, k, i);
  }
  int val = 0;
  //ret是fscanf的返回值,fscanf返回eof,则结束
  int ret = fscanf(fq, "%d", &val);
  while (ret != EOF)
  {
  if (val > topk[0])
  {
    swap(&val ,&topk[0]);
    AdjustDown(topk, k, 0);
  }
  ret = fscanf(fq, "%d", &val);
  }
  for (int i = 0; i < k; ++i)
  {
  printf("%d ", topk[i]);
  }
  printf("\n");
  fclose(fq);
  free(fq);
}


随后我们看看结果:

先创建数据:


int main()
{
  CreateNdata();
  //PrintTopK("data.txt", 10);
}


然后在取前k个数据:


4bc880f7270f6a84767f267a306b3e2d_5b5b25feff064f56a23a7f1fe2a3e992.png


int main()
{
  //CreateNdata();
  PrintTopK("data.txt", 10);
}



产生这个结果的原因是数据太多了,导致产生随机数9999的数据太多了。现在我们直接改改文件里的数据:


d8e83871e3b40e83367fc12f938f8921_490e007119184ab38e81ddc8f631f396.png


在改了以后最大的几个数一定会有最后几个,现在我们来看看结果:


e408b38e3e273255954ba6bdf0509ec9_b57e55a06c784ea58daa1b93397afba2.png


这样最大的前k个数就以小堆的形式打印出来了。

目录
相关文章
|
26天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
63 16
|
26天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
87 8
|
2月前
|
存储 算法 关系型数据库
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
这篇文章主要介绍了多路查找树的基本概念,包括二叉树的局限性、多叉树的优化、B树及其变体(如2-3树、B+树、B*树)的特点和应用,旨在帮助读者理解这些数据结构在文件系统和数据库系统中的重要性和效率。
24 0
数据结构与算法学习二一:多路查找树、二叉树与B树、2-3树、B+树、B*树。(本章为了解基本知识即可,不做代码学习)
|
2月前
|
存储 算法 搜索推荐
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
这篇文章主要介绍了顺序存储二叉树和线索化二叉树的概念、特点、实现方式以及应用场景。
27 0
数据结构与算法学习十七:顺序储存二叉树、线索化二叉树
|
2月前
|
Java
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(二)
28 1
|
2月前
|
算法 Java C语言
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
【用Java学习数据结构系列】震惊,二叉树原来是要这么学习的(一)
25 1
|
2月前
|
存储
【数据结构】二叉树链式结构——感受递归的暴力美学
【数据结构】二叉树链式结构——感受递归的暴力美学
|
2月前
|
存储 算法
探索数据结构:分支的世界之二叉树与堆
探索数据结构:分支的世界之二叉树与堆
|
2月前
探索顺序结构:栈的实现方式
探索顺序结构:栈的实现方式
|
2月前
|
存储 算法
数据结构与算法学习十六:树的知识、二叉树、二叉树的遍历(前序、中序、后序、层次)、二叉树的查找(前序、中序、后序、层次)、二叉树的删除
这篇文章主要介绍了树和二叉树的基础知识,包括树的存储方式、二叉树的定义、遍历方法(前序、中序、后序、层次遍历),以及二叉树的查找和删除操作。
28 0