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

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

三.堆的应用:


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个数就以小堆的形式打印出来了。

目录
相关文章
|
2天前
【数据结构】二叉树(遍历,递归)
【数据结构】二叉树(遍历,递归
16 2
|
2天前
|
存储 监控 NoSQL
Redis处理大量数据主要依赖于其内存存储结构、高效的数据结构和算法,以及一系列的优化策略
【5月更文挑战第15天】Redis处理大量数据依赖内存存储、高效数据结构和优化策略。选择合适的数据结构、利用批量操作减少网络开销、控制批量大小、使用Redis Cluster进行分布式存储、优化内存使用及监控调优是关键。通过这些方法,Redis能有效处理大量数据并保持高性能。
18 0
|
2天前
|
算法 编译器 C语言
数据结构——二叉树四种遍历的实现-3
数据结构——二叉树四种遍历的实现
数据结构——二叉树四种遍历的实现-3
|
2天前
|
存储
数据结构——二叉树四种遍历的实现-2
数据结构——二叉树四种遍历的实现
数据结构——二叉树四种遍历的实现-2
|
2天前
|
机器学习/深度学习
数据结构——二叉树四种遍历的实现-1
数据结构——二叉树四种遍历的实现
数据结构——二叉树四种遍历的实现-1
|
2天前
【数据结构】二叉树的三种遍历(非递归讲解)
【数据结构】二叉树的三种遍历(非递归讲解)
9 1
|
2天前
|
存储
【数据结构】二叉树相关oj题(一)
【数据结构】二叉树相关oj题(一)
10 1
|
2天前
|
存储 分布式数据库
[数据结构]~二叉树
[数据结构]~二叉树
|
2天前
|
C语言
【C语言/数据结构】二叉树(层序遍历|判断完全二叉树|性质)
【C语言/数据结构】二叉树(层序遍历|判断完全二叉树|性质)
280 52
|
2天前
【数据结构】二叉树-堆(top-k问题,堆排序,时间复杂度)
【数据结构】二叉树-堆(top-k问题,堆排序,时间复杂度)
17 4