数据结构:堆的实现和堆排序及TopK问题

news2025/1/18 8:51:10

文章目录

  • 1. 堆的概念和性质
    • 1.1 堆的概念
    • 1.2 堆的性质
    • 1.3 堆的作用
  • 2. 堆的声明
  • 3. 堆的实现
    • 3.1 堆的插入
    • 3.2 删除堆顶元素
    • 3.3 利用数组建堆
    • 3.4 完整代码
  • 4. 堆的应用
    • 4.1 堆排序
    • 4.2 TopK问题
      • 代码实现

物理结构有顺序结构存储和链式结构存储两种,二叉树理所应当也是可以顺序结构存储和链式结构存储的.

但是普通的二叉树显然不适合使用数组来存储,因为可能会存在大量的空间浪费,而完全二叉树更加适合用顺序结构存储,因为它中间不会有空的元素,从头到尾一直连续.

在这里插入图片描述

有一种数据结构就是将完全二叉树以数组存放的,这就是下面介绍的堆.

1. 堆的概念和性质

1.1 堆的概念

堆(Heap)是计算机科学中一类特殊的数据结构的统称.堆通常是一个可以被看做一棵完全二叉树的数组对象.

如果有一个关键码的集合 K = { k 0 , k 1 , k 2 , . . . , k n − 1 k_0, k_1, k_2, ..., k_{n-1} k0,k1,k2,...,kn1}, 把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中, 并满足: K i < = K 2 ∗ i + 1 且 K i < = K 2 ∗ i + 2 ( i = 0 , 1 , 2... ) , 则称为小堆 K_i<=K_{2*i+1}且K_i<=K_{2*i+2}(i=0,1,2...),则称为小堆 Ki<=K2i+1Ki<=K2i+2(i=0,1,2...),则称为小堆

或 K i > = K 2 ∗ i + 1 且 K i > = K 2 ∗ i + 2 ( i = 0 , 1 , 2... ) , 则称为大堆 或K_i>=K_{2*i+1}且K_i>=K_{2*i+2}(i=0,1,2...),则称为大堆 Ki>=K2i+1Ki>=K2i+2(i=0,1,2...),则称为大堆将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆.


总结为
小堆:任意一个父亲都 <= 孩子
大堆:任意一个父亲都 >= 孩子


1.2 堆的性质

  • 堆中某个结点的值总是不大于或不小于其父结点的值;
  • 堆总是一棵完全二叉树

在这里插入图片描述


1.3 堆的作用

  • 堆排序:时间复杂度为 O ( N ∗ l o g N ) O(N*logN) O(NlogN)
  • 解决 T o p K TopK TopK问题:在 N N N个数中间找出最大的前 k k k个或者最小的前 k k k
  • 在操作系统中:根据优先级决定若干进程中使用哪个进程

2. 堆的声明

所有的数组都可以被当作完全二叉树,但不是所有的数组都能被称为堆.

本文实现大堆,若想灵活实现大小堆转换,可以使用函数指针

// 堆的结构
typedef struct Heap
{
  HPDatatype* a;    //堆底层用数组存储
  int size;         //堆的元素个数
  int capacity;     //堆的容量
}Heap;

//向上调整
void AdjustUp(HPDatatype* a, int child);
//向下调整
void AdjustDown(HPDatatype* a, int n , int parent);

//交换值
void Swap(HPDatatype* p1, HPDatatype* p2);
//堆初始化
void HeapInit(Heap* hp);
//堆打印
void HeapPrint(Heap* hp);
//堆使用数组初始化
void HeapArrayInit(Heap* hp, HPDatatype* a, int n); 
//堆销毁
void HeapDestroy(Heap* hp);
//堆插入
void HeapPush(Heap* hp, HPDatatype x);
//删除堆顶元素
void HeapPop(Heap* hp);
//返回堆顶数据
HPDatatype HeapTop(Heap* hp);
//判断堆是否为空,为空返回非0,非空返回0
int HeapEmpty(Heap* hp);

可以发现,虽然堆的结构和顺序表是一样的,但是它们的逻辑结构是不同的.堆是特殊的完全二叉树,是树形层次结构;顺序表是顺序存储的线性表,是线性存储结构.

3. 堆的实现

关于堆的初始化,打印,销毁等操作,与顺序表基本一致,这里不过多赘述.

最重要的是堆的插入,删除堆顶元素和堆使用数组初始化,需要保持堆序性质,

3.1 堆的插入

先将元素 X X X 插入到数组最后一个位置,随后将它与它的父亲相比较,如果不满足堆序,则交换两值, 直至 X X X 到堆顶或者满足堆序结束判断.
在这里插入图片描述

上面的行为可以称之为向上调整(AdjustUp), 具体实现如下

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* hp, HPDatatype x)
{
  assert(hp);

  //扩容
  if (hp->size == hp->capacity)
  {
    int newCapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
    HPDatatype* tmp = (HPDatatype*)realloc(hp->a, sizeof(HPDatatype) * newCapacity);
    if (tmp != NULL)
    {
      hp->a = tmp;
      hp->capacity = newCapacity;
    }
    else 
    {
      perror("realloc");
    }
  }

  //先将元素插到数组末尾
  hp->a[hp->size] = x;
  
  //向上调整
  AdjustUp(hp->a, hp->size);
  
  //修改大小
  hp->size++;
}

向上调整逻辑上控制树,物理上控制数组,向上调整只会影响该元素的祖先,不会影响根结点的另外一棵子树.

时间复杂度为 O ( l o g N ) O(logN) O(logN)


3.2 删除堆顶元素

首先交换堆顶和堆底的元素,删除此时位于堆底的元素,随后将此时堆顶的元素和其孩子中的较大(或较小)值比较,如果不满足堆序进行交换,直至满足堆序或者该元素已经没有孩子.

在这里插入图片描述

需要注意的是:并不是所有的结点都有两个孩子,所以在寻找结点孩子中的较大(较小)值,需要提前判断是否有右孩子

void AdjustDown(HPDatatype* a, int n, int parent)
{
  int child = parent * 2 + 1;

  while (child < n)
  {
    //找到更大的孩子
    if (child + 1 < n && a[child + 1] > a[child])
    {
      child++;
    }

    //如果孩子比父亲大,交换元素
    if (a[child] > a[parent])
    {
      Swap(&a[child], &a[parent]);
      parent = child;
      child = parent * 2 + 1;
    }
    else 
    {
      break;
    }
  }
}

向下调整有个前提,就是该结点的左右子树都是堆.
时间复杂度也是 O ( l o g N ) O(logN) O(logN),最坏情况是从根节点移动到叶子节点.

void HeapPop(Heap* hp)
{
  assert(hp); //确保hp合法
  assert(!HeapEmpty(hp)); //确保堆不为空
 
  //交换最后一个元素和首元素
  Swap(&hp->a[0], &hp->a[hp->size - 1]);
  hp->size--;

  //向下调整
  AdjustDown(hp->a, hp->size, 0);
}

3.3 利用数组建堆

有两种方法:
一. 从头遍历数组依次将元素 push 进堆, 即每 push 一个元素调用一次向上调整算法
二. 从最后一个不为叶子结点的结点开始向下调整,向前直到根节点, 即使用向下调整算法

  • 使用插入建堆
    在这里插入图片描述
void HeapArrayInit(Heap* hp, HPDatatype* a, int n)
{
  assert(hp); //确保hp合法

  hp->size = hp->capacity = n;
  HPDatatype* tmp = (HPDatatype*)malloc(sizeof(HPDatatype) * n);
  if (tmp == NULL)
  {
     perror("malloc");
  }

  hp->a = tmp;
  memcpy(hp->a, a, sizeof(HPDatatype) * n);
  
  //建堆
  int i = 0;
  for (i = 0; i < n; i++)
  {
    AdjustUp(hp->a,i);
  }
}

由于每个向上调整算法是 O ( l o g N ) O(logN) O(logN)的时间复杂度, 一共有 N 个结点, 需要调用 N 次向上调整算法, 所以插入建堆的时间复杂度为 O ( N ∗ l o g N ) O(N*logN) O(NlogN)

假设堆是满二叉树,方便运算.
在这里插入图片描述


等式 1 : T ( N ) = 2 0 ∗ 0 + 2 1 ∗ 1 + . . . + 2 h − 1 ∗ ( h − 1 ) 等式1:T(N) = 2^0*0 + 2^1*1 +...+2^{h-1}*(h-1) 等式1:T(N)=200+211+...+2h1(h1)
等式 2 : 2 T ( N ) = 2 1 ∗ 0 + 2 2 ∗ 1 + . . . + 2 h ∗ ( h − 1 ) 等式2:2T(N) = 2^1*0 + 2^2*1 +...+2^h*(h-1) 等式2:2T(N)=210+221+...+2h(h1)

等式2 - 等式1推出:
T ( N ) = − ( 2 1 + 2 2 + . . . + 2 h − 1 ) − 2 h ∗ ( h − 1 ) T(N) = -(2^1 + 2^2 +...+2^{h-1})-2^h*(h-1) T(N)=(21+22+...+2h1)2h(h1)

= 2 ∗ ( 1 − 2 h − 1 ) 1 − 2 − 2 h ∗ ( h − 1 ) = \frac{2*(1-2^{h-1})}{1-2} -2^h*(h-1) =122(12h1)2h(h1)

= 2 h − 1 − 1 − 2 h ∗ ( h − 1 ) = 2^{h-1}-1-2^h*(h-1) =2h112h(h1)

又因为 h = l o g 2 ( N + 1 ) 即 2 h = N + 1 又因为 h = log_2(N+1) 即 2^h = N+1 又因为h=log2(N+1)2h=N+1

最终 T ( N ) = N + 1 2 − 1 − ( N + 1 ) ∗ [ l o g 2 ( N + 1 ) − 1 ] = O ( N ∗ l o g N ) 最终 T(N) = \frac{N+1}{2} - 1 - (N+1)*[log_2(N+1) - 1] = O(N*logN) 最终T(N)=2N+11(N+1)[log2(N+1)1]=O(NlogN)


  • 从最后一个分支节点开始 由下向上 向下调整
    在这里插入图片描述
void HeapArrayInit(Heap* hp, HPDatatype* a, int n)
{
  assert(hp); //确保hp合法

  hp->size = hp->capacity = n;
  HPDatatype* tmp = (HPDatatype*)malloc(sizeof(HPDatatype) * n);
  if (tmp == NULL)
  {
     perror("malloc");
  }

  hp->a = tmp;
  memcpy(hp->a, a, sizeof(HPDatatype) * n);
  
  int i = 0;  
  //从最后一个分支结点开始向下调整建堆
  for (i = (n-2)/2; i >= 0; i--)
  {
    AdjustDown(hp->a, n, i);
  }
}

让我们分析使用向下调整的时间复杂度,假设堆是满二叉树,方便计算.
在这里插入图片描述

等式 1 : T ( N ) = 2 0 ∗ ( h − 1 ) + 2 1 ∗ ( h − 2 ) + . . . + 2 h − 2 ∗ 1 等式1:T(N) = 2^0*(h-1) + 2^1*(h-2) +...+2^{h-2}*1 等式1:T(N)=20(h1)+21(h2)+...+2h21
等式 2 : 2 T ( N ) = 2 1 ∗ ( h − 1 ) + 2 2 ∗ ( h − 2 ) + . . . + 2 h − 1 ∗ 1 等式2:2T(N) = 2^1*(h-1) + 2^2*(h-2) +...+2^{h-1}*1 等式2:2T(N)=21(h1)+22(h2)+...+2h11

等式2 - 等式1推出:
T ( N ) = − ( h − 1 ) + 2 1 + 2 2 + . . . + 2 h − 2 + 2 h − 1 T(N) = -(h-1) + 2^1+2^2+...+2^{h-2}+2^{h-1} T(N)=(h1)+21+22+...+2h2+2h1

= − h + 2 0 + 2 1 + 2 2 + . . . + 2 h − 2 + 2 h − 1 = 1 ∗ ( 1 − 2 h ) 1 − 2 − h = -h + 2^0 +2^1+2^2+...+2^{h-2}+2^{h-1} = \frac{1*(1-2^h)}{1-2} -h =h+20+21+22+...+2h2+2h1=121(12h)h

= 2 h − 1 − h = 2^h-1-h =2h1h

又因为 h = l o g 2 ( N + 1 ) 即 2 h = N + 1 又因为 h = log_2(N+1) 即 2^h = N+1 又因为h=log2(N+1)2h=N+1

最终 T ( N ) = N − l o g 2 ( N + 1 ) = O ( N ) 最终 T(N) = N - log_2(N+1) = O(N) 最终T(N)=Nlog2(N+1)=O(N)


最终可以发现,两种方式不仅建堆的结果不同,而且由下向上的建堆方式时间复杂度更低.

在这里插入图片描述

平常都是使用第二种方式进行建堆的


3.4 完整代码

  • Heap.h
#pragma once 

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

typedef int HPDatatype;

// 堆的结构
typedef struct Heap
{
  HPDatatype* a;    //堆底层用数组存储
  int size;         //堆的元素个数
  int capacity;     //堆的容量
}Heap;

void AdjustUp(HPDatatype* a, int child);
void AdjustDown(HPDatatype* a, int n , int parent);

void Swap(HPDatatype* p1, HPDatatype* p2);
void HeapInit(Heap* hp);
void HeapPrint(Heap* hp);
void HeapArrayInit(Heap* hp, HPDatatype* a, int n); 
void HeapDestroy(Heap* hp);
void HeapPush(Heap* hp, HPDatatype x);
void HeapPop(Heap* hp);
HPDatatype HeapTop(Heap* hp);
int HeapEmpty(Heap* hp);

  • Heap.c
#include "Heap.h"

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 AdjustDown(HPDatatype* a, int n, int parent)
{
  int child = parent * 2 + 1;

  while (child < n)
  {
    //找到更大的孩子
    if (child + 1 < n && 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 Swap(HPDatatype* p1, HPDatatype* p2)
{
  HPDatatype tmp = *p1;
  *p1 = *p2;
  *p2 = tmp;
}

void HeapInit(Heap* hp)
{
  assert(hp); //确保hp合法

  hp->a = NULL;
  hp->size = hp->capacity = 0;
}

void HeapArrayInit(Heap* hp, HPDatatype* a, int n)
{
  assert(hp); //确保hp合法

  hp->size = hp->capacity = n;
  HPDatatype* tmp = (HPDatatype*)malloc(sizeof(HPDatatype) * n);
  if (tmp == NULL)
  {
     perror("malloc");
  }

  hp->a = tmp;
  memcpy(hp->a, a, sizeof(HPDatatype) * n);
  
  int i = 0;
  //插入建堆
  //for (i = 0; i < n; i++)
  //{
  //  AdjustUp(hp->a,i);
  //}
  
  //从最后一个分支结点开始向下调整建堆
  for (i = (n-2)/2; i >= 0; i--)
  {
    AdjustDown(hp->a, n, i);
  }
}

void HeapPrint(Heap* hp)
{
  assert(hp); //确保hp合法

  int i = 0;
  for (i = 0; i < hp->size; i++)
  {
    printf("%d ", hp->a[i]);
  }
  printf("\n");
}

void HeapDestroy(Heap* hp)
{
  assert(hp); //确保hp合法

  free(hp->a);
  hp->a = NULL;
  hp->size = hp->capacity = 0;
}

void HeapPush(Heap* hp, HPDatatype x)
{
  assert(hp);

  //扩容
  if (hp->size == hp->capacity)
  {
    int newCapacity = hp->capacity == 0 ? 4 : 2 * hp->capacity;
    HPDatatype* tmp = (HPDatatype*)realloc(hp->a, sizeof(HPDatatype) * newCapacity);
    if (tmp != NULL)
    {
      hp->a = tmp;
      hp->capacity = newCapacity;
    }
    else 
    {
      perror("realloc");
    }
  }

  //先将元素未查到数组末尾
  hp->a[hp->size] = x;
  
  //向上调整
  AdjustUp(hp->a, hp->size);
  
  //修改大小
  hp->size++;
}

void HeapPop(Heap* hp)
{
  assert(hp); //确保hp合法
  assert(!HeapEmpty(hp)); //确保堆不为空
 
  //交换最后一个元素和首元素
  Swap(&hp->a[0], &hp->a[hp->size - 1]);
  hp->size--;

  //向下调整
  AdjustDown(hp->a, hp->size, 0);
}

HPDatatype HeapTop(Heap* hp)
{
  assert(hp);
  assert(!HeapEmpty(hp));

  return hp->a[hp->size-1];
}

int HeapEmpty(Heap* hp)
{
  assert(hp); //确保hp合法

  if (hp->size == 0)
  {
    return 1;
  }
  else 
  {
    return 0;
  }
}

  • test.c
#include "Heap.h"

void HeapTest1()
{ 
  int a[] = {60, 70, 80, 50, 40, 30}; 
  AdjustDown(a, 6, 0);

  int i = 0;

  for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
  {
    printf("%d ", a[i]);
  }
  printf("\n");
}

void HeapTest2()
{
  Heap heap;

  HeapInit(&heap);
  HeapPush(&heap, 60);
  HeapPush(&heap, 70);
  HeapPush(&heap, 80);
  HeapPush(&heap, 50);
  HeapPush(&heap, 40);
  HeapPrint(&heap);
  HeapPush(&heap, 30);
  AdjustDown(heap.a, 6, 0);
  HeapPrint(&heap);
}

void HeapTest3()
{
  Heap heap;

  int a[] = {7, 8, 6, 4, 9, 2, 1, 0};
  HeapArrayInit(&heap, a, 8);
  HeapPrint(&heap);

  HeapPop(&heap);
  HeapPrint(&heap);
}

//升序
void HeapSort(int* a, int n)
{
  int i = 0;
  //建大堆
  for (i = 0; i < n; i++)
  {
    AdjustUp(a, i);
  }

  //不断将根元素放置最后一个,让前面的元素向下调整
  for (i = 0; i < n; i++)
  {
    Swap(&a[0], &a[n - 1 - i]);
    AdjustDown(a, n - 1 - i, 0);
  }
}

int main(void)
{
  //HeapTest1();
  //HeapTest2();
  HeapTest3();
  
  int a[] = {3,4,5,6,9,10,2};
  HeapSort(a, sizeof(a) / sizeof(a[0]));

  int i = 0;
  for (i = 0; i < sizeof(a) / sizeof(a[0]); i++)
  {
    printf("%d ", a[i]);
  }
  printf("\n");
  return 0;
}

4. 堆的应用

4.1 堆排序

堆排序即用堆的思想来进行排序.

总共分为两个步骤:

  • 建堆
    • 升序:建大堆
    • 降序:建小堆
  • 利用堆删除思想来进行排序

首先考虑,为什么升序只能建大堆,我建小堆不可以吗?

  • 答案肯定是否定的.如果我建小堆,根据小堆的性质,小堆的根结点是最小数.此时 1 是最小值.
    在这里插入图片描述

  • 随后我忽视 1, 对剩下的元素进行堆排序,但是剩下的元素并不一定是堆.

    忽视 1,2,3 后, 此时只剩下

    在这里插入图片描述

    但是此时剩下的数并不能构成小堆,需要我重新建堆

  • 为了避免剩下的数不能构成小堆的情况,我需要每次都对剩下元素进行重新建堆.每一次将剩余元素前移需要消耗 O ( N ) O(N) O(N)的时间复杂度,同时上面已经讨论过,建堆至少也要消耗 O ( N ) O(N) O(N)的时间复杂度.

  • 也就是说,这样操作的时间复杂度是 O ( N 2 ) O(N^2) O(N2),那我为什么不直接用代码更简便的冒泡排序呢?


堆排序的基本思想:

  • 将待排序的序列构造成一个大顶堆.此时,整个序列的最大值就是堆顶的根结点.
  • 将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值)
  • 然后将剩余的 n-1 个序列重新构造成一个堆, 这样就会得到 n 个元素中的次大值.
  • 如此反复执行,便能得到一个有序序列了.

在这里插入图片描述

//升序
void HeapSort(int* a, int n)
{
  int i = 0;
  //建大堆,从最后一个分支结点开始向下调整
  for (i = (n-1-1)/2; i>=0; i--)
  {
    AdjustDown(a, n, i);
  }

  //将堆顶元素与堆底元素交换后,向下调整
  int end = n - 1;
  while (end > 0)
  {
    Swap(&a[0], &a[end]);
    AdjustDown(a, end, 0);
    --end;
  }

}

堆排序时间复杂度分析:

  • 首先建堆的时间复杂度为 O ( N ) O(N) O(N)
  • 每次向下调整的时间复杂度为 O ( l o g N ) O(logN) O(logN), 一共会有 n-1 次.那么就是 O ( N ∗ l o g N ) O(N*logN) O(NlogN)
  • 所以总体来说L:堆排序的时间复杂度是 O ( N ∗ l o g N ) O(N*logN) O(NlogN), 最好,最坏和平均时间复杂度都是如此.

4.2 TopK问题

在 N 个数中找出最大的前 K 个 (比如在1000个数中找出最大的前10个)

  • 方法一:按降序 堆排序 所有的元素, 前 K 个元素就是最大的前 K 个元素
    时间复杂度: O ( N ∗ l o g N ) O(N*logN) O(NlogN)

有没有更优化的呢?我只需要取前 K 个, 那么我并不需要将所有的元素都排序.


  • 方法二:先建大堆, Pop K 次, 取到最大的 K 个数
    时间复杂度: O ( N + l o g N ∗ K ) O(N + logN*K) O(N+logNK)
    空间复杂度: O ( N ) O(N) O(N)

还是有点复杂,如果N很大的话,空间复杂度也会很大.

假设 N 是10亿,我需要先建一个占据 10亿字节的堆, 10亿字节约为 1G 空间,这所消耗的空间是巨大的.


  • 方法三:
  • 用前 K 个数先建小堆
  • 剩下的 N-K 个数, 依次和堆顶元素进行比较. 如果比堆顶元素大, 替换堆顶元素并且向下调整.
  • 遍历完 N 个数后, 最后堆里的 K 个数就是最大的 K 个数

时间复杂度: O ( K + ( N − K ) ∗ l o g K ) = O ( N ∗ l o g K ) O(K + (N-K)*logK) = O(N*logK) O(K+(NK)logK)=O(NlogK)
空间复杂度: O ( K ) O(K) O(K)

为什么建的是小堆呢?

最大的 K 个数一定比其他的数都要大, 同时小堆大数会沉底. 也就是说, 留在堆顶的只能是第 K 个大的数, 或者是其他比 第 K 个大的数 小的数.这样最大的 K 个数一定会进堆.

代码实现

首先我写了个CreateData()函数用来往文件中存放数据,我存放的数的范围是 [ 0 , 10000000 ) [0,10000000) [0,10000000),为了验证我之后写的代码是否能找到前 K 个数, 我在 data 文件中修改了 K 个数, 都比 10000000 大.

void CreateData()
{
  //打开文件
  FILE* fout = fopen("data", "w+");
  if (fout == NULL)
  {
    perror("fopen");
  }

  //随机放数
  srand(time(0));
  int i = 0;
  int n = 10000000;

  for (i = 0; i < 10000; i++)
  {
    int num = (rand() + i) % n; 
    fprintf(fout, "%d\n", num);
  }

  fclose(fout);
}

接着是解决 TopK 的核心代码

//小堆向下调整
void AdjustDown_small(HPDatatype* a, int n, int parent)
{
  assert(a);
  
  int child = parent * 2 + 1;

  while (child < n)
  {
    if (child + 1 < n && a[child + 1] < a[child])
    {
      child++;
    }

    if (a[parent] > a[child])
    {
      Swap(&a[parent], &a[child]);
      parent = child;
      child = parent * 2 + 1;
    }
    else 
    {
      break;
    }
  }
}

//得到n个数中的最大k个
void PrintTopK(const char* filename, int k)
{
  //打开文件
  FILE* fout = fopen(filename, "r");
  if (fout == NULL)
  {
    perror("fopen");
  }

  //申请一个大小为k的数组空间
  int* a = (int*)malloc(sizeof(int) * k);

  //先将前k个数据建小堆
  int i = 0; 
  for (i = 0; i < k; i++)
  {
    fscanf(fout, "%d", &a[i]);
  }

  for (i = (k-2)/2; i>=0; i--)
  {
    AdjustDown_small(a, k, i);
  }

  //遍历剩余的数,如果有比堆顶大的数,入堆并向下调整
  int num = 0;
  while (fscanf(fout, "%d", &num) != EOF)
  {
    //如果num大于堆顶元素,入堆并向下调整
    if (num > a[0])
    {
      a[0] = num;
      AdjustDown_small(a, k, 0);
    }
  }

  fclose(fout);

  for (i = 0; i < k; i++)
  {
    fprintf(stdout, "%d\n", a[i]);
  }

  free(a);
}

结果如下:
k = 10:
在这里插入图片描述

k = 11:
在这里插入图片描述

本章完.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1052086.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实时通信协议

本文旨在简要解释如何在Web上实现客户端/服务器和客户端/客户端之间的实时通信&#xff0c;以及它们的内部工作原理和最常见的用例。 TCP vs UDP TCP和UDP都位于OSI模型的传输层&#xff0c;负责在网络上传输数据包。它们之间的主要区别在于&#xff0c;TCP在传输数据之前会打开…

26960-2011 半自动捆扎机 学习笔记

声明 本文是学习GB-T 26960-2011 半自动捆扎机. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了半自动捆扎机(以下简称"捆扎机")的术语和定义、型号、型式与基本参数、技术要求、 试验方法、检验规则及标志、包装、运…

Python变量的三个特征

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 我们来看这些代码 x 10 print(x) # 获取变量的变量值 print(id(x)) # 获取变量的id&#xff0c;可以理解成变量在内存中的地址python的内置功能id()&#xff0c;内存地址不一样&#xff0c;则id()后打印的结果不一样&…

【HTML】表格行和列的合并

概述 当我们需要在 HTML 表格中展示复杂的数据时&#xff0c;行和列的合并可以帮助我们实现更灵活的布局和结构。通过合并行和列&#xff0c;我们可以创建具有更多层次和结构的表格&#xff0c;使数据更易于理解和分析。 在 HTML 表格中&#xff0c;我们可以使用 rowspan 和 …

【Spring Cloud】深入探索 Nacos 注册中心的原理,服务的注册与发现,服务分层模型,负载均衡策略,微服务的权重设置,环境隔离

文章目录 前言一、初识 Nacos 注册中心1.1 什么是 Nacos1.2 Nacos 的安装&#xff0c;配置&#xff0c;启动 二、服务的注册与发现三、Nacos 服务分层模型3.1 Nacos 的服务分级存储模型3.2 服务跨集群调用问题3.3 服务集群属性设置3.4 修改负载均衡策略为集群策略 四、根据服务…

【JUC】一文弄懂@Async的使用与原理

文章目录 1. Async异步任务概述2. 深入Async的底层2.1 Async注解2.2 EnableAsync注解2.3 默认线程池 1. Async异步任务概述 在Spring3.X的版本之后&#xff0c;内置了Async解决了多个任务同步进行导致接口响应迟缓的情况。 使用Async注解可以异步执行一个任务&#xff0c;这个…

棱镜七彩受邀参加“数字政府建设暨数字安全技术研讨会”

近日&#xff0c;为深入学习贯彻党的二十大精神&#xff0c;落实《数字中国建设整体布局规划》中关于“发展高效协同的数字政务”的要求&#xff0c;由国家信息中心主办、复旦大学义乌研究院承办、苏州棱镜七彩信息科技有限公司等单位协办的“数字政府建设暨数字安全技术研讨会…

zemax埃尔弗目镜

可以认为是一种对称设计&#xff0c;在两个双胶合透镜之间增加一个双凹单透镜 将半视场增大到30&#xff0c;所有的轴外像差维持在可以接受的水平。 入瞳直径4mm波长0.51、0.56、0.61半视场30焦距27.9mm 镜头参数&#xff1a; 成像效果&#xff1a;

Win11配置多个CUDA环境

概述 由于跑项目发现需要配置不同版本的Pytorch&#xff0c;而不同版本的Pytorch又对应不同版本的CUDA&#xff0c;于是有了在Win上装多个CUDA的打算 默认已经在电脑上装了一个CUDA 现在开始下载第二个CUDA版本&#xff0c;前面下载的操作和普通安装的几乎一样 安装CUDA CU…

CFS内网穿透靶场实战

一、简介 不久前做过的靶场。 通过复现CFS三层穿透靶场&#xff0c;让我对漏洞的利用&#xff0c;各种工具的使用以及横向穿透技术有了更深的理解。 一开始nmap探测ip端口,直接用thinkphpv5版本漏洞工具反弹shell&#xff0c;接着利用蚁剑对服务器直接进行控制&#xff0c;留下…

识别消费陷阱,反消费主义书单推荐

在消费主义无所不在的今天&#xff0c;商家是如何设置消费陷阱的&#xff1f;人们在做出消费决策时又是如何“犯错”的&#xff1f;如何才能做出更加理性的选择&#xff1f; 本书单适合对经济学、市场营销感兴趣的朋友阅读。 《小狗钱钱》 “你的自信程度决定了你是否相信自已…

kaggle_competition1_CIFAR10_Reg

一、查漏补缺、熟能生巧&#xff1a; 1.关于shutil.copy或者这个copyfile的作用和用法&#xff1a; 将对应的文件复制到对应的文件目录下 2.关于python中dict的键值对的获取方式&#xff1a; #终于明白了&#xff0c;原来python中的键_值 对的用法就是通过调用dict.keys()和…

Windows/Linux下进程信息获取

Windows/Linux下进程信息获取 前言一、windows部分二、Linux部分三、完整代码四、结果 前言 Windows/Linux下进程信息获取&#xff0c;目前可获取进程名称、进程ID、进程状态 理论分析&#xff1a; Windows版本获取进程列表的API: CreateToolhelp32Snapshot() 创建进程快照,…

GPIO的输入模式

1. GPIO支持4种输入模式&#xff08;浮空输入、上拉输入、下拉输入、模拟输入&#xff09; 1. 模拟输入 首先GPIO输出部分(N-MOS,P-MOS)是不起作用的。并且TTL施密特触发器也是不工作的。 上下拉电阻的开关都是关闭的。相当于I/o直接接在模拟输入。 模拟输入模式下&#xff…

测试开源下载模块Downloader

微信公众号“DotNet”的文章《.NET 异步、跨平台、支持分段下载的开源项目 》&#xff08;参考文献1&#xff09;介绍了GitHub中的开源下载模块Downloader的基本用法&#xff0c;本文学习Downloader的主要参数设置方式及基本用法&#xff0c;最后编写简单的测试程序进行文件下载…

[尚硅谷React笔记]——第2章 React面向组件编程

目录&#xff1a; 基本理解和使用&#xff1a; 使用React开发者工具调试函数式组件复习类的基本知识类式组件组件三大核心属性1: state 复习类中方法this指向&#xff1a; 复习bind函数&#xff1a;解决changeWeather中this指向问题&#xff1a;一般写法&#xff1a;state.htm…

毛玻璃态计算器

效果展示 页面结构组成 从上述的效果可以看出&#xff0c;计算机的页面比较规整&#xff0c;适合grid布局。 CSS3 知识点 grid 布局 实现计算机布局 <div class"container"><form class"calculator" name"calc"><input type…

【无标题】ICCV 2023 | CAPEAM:基于上下文感知规划和环境感知记忆机制构建具身智能体

文章链接&#xff1a; https://arxiv.org/abs/2308.07241 2023年&#xff0c;大型语言模型&#xff08;LLMs&#xff09;以及AI Agents的蓬勃发展为整个机器智能领域带来了全新的发展机遇。一直以来&#xff0c;研究者们对具身智能&#xff08;Embodied Artificial Intelligenc…

macOS 14 Sonoma 如何删除不需要的 4k 动态壁纸

概览 在升级到 macOS 14&#xff08;Sonoma&#xff09;之后&#xff0c;小伙伴们惊喜发现  提供了诸多高清&#xff08;4k&#xff09;动态壁纸的支持。 现在&#xff0c;从锁屏到解锁进入桌面动态到静态的切换一气呵成、无比丝滑。 壁纸显现可谓是有了“天水相连为一色&…

卷发棒上架亚马逊美国销售需要做什么认证?卷发棒UL859测试报告

卷发棒是一种美发DIY工具&#xff0c;目前美发沙龙和发廊的的美发师都会使用一套卷发棒工具。卷发棒可以造出各种卷发。如&#xff1a;大波浪卷发、下垂自然卷发、垂至肩头卷发、碎卷、麦穗烫、内翻式卷发、外翻式卷发。目前很多家庭会自己备有这样的产品DIY。 什么是UL检测报告…