运用堆结构来解决Top-K问题

简介: 运用堆结构来解决Top-K问题

前言

  Top-K问题也算是面试中或者题目中常见的一类问题,本篇文章将会使用堆结构的方法来解决Top-K问题。


一、什么是Top-K问题

  所谓Top-K问题就是从众多数据中找到前K个最大值或者最小值。这个问题不是简单的排个序就能完成的,因为Top-K问题往往设计到的数据量都非常非常多,具体有多多我也不敢定论,反正就是在非常非常多的数据中找出前K个最值。

二、回顾堆结构

1.堆的概念和性质

  所谓堆,就是一种特殊的完全二叉树。在堆当中,任何一个结点的值都要比其孩子要大或者小,任意结点都比其孩子大的叫做大堆(或大根堆),任意结点都比其孩子小的叫做小堆(或小根堆)。

2.堆在数组中的存储

  由于堆是一个完全二叉树,而完全二叉树从根结点到最后一个结点之间是没有空值的,是一种非常连续的结构。所以我们大多数情况下都是使用顺序存储在数组当中。

  堆,或者说二叉树在数组中都是一层一层、从左往右存储的。根结点的下标为 0 。

  需要注意的是,根据上图,我们可以找到一个规律,这个规律在堆当中是蛮重要的。假设某一个结点的下标是 i ,那么它的父结点(假如存在)的下标就是 (i - 1) / 2 ,它的左孩子(假如存在)的下标就是 i * 2 + 1 ,它的右孩子(假如存在)的下标就是 i * 2 + 2。所以在数组当中,我们可以根据以上规律很快的找到任意结点的父结点或者子结点。

三、算法思想

  因为堆的根结点是堆的最值,比较特殊,所以在涉及到堆的问题中往往要对根结点进行分析。使用堆解决Top-K问题其实只需要知道两个步骤,1.建什么堆2.调整堆结构

1.建什么堆

  我们既然要找到前k个最值,那么我们就需要建立一个大小为k的堆来存放这几个最值。那么我们要建立大堆还是小堆呢?假设我们要找前k个最大值,我们需要建立一个小堆,因为堆顶存放的是k个最大值中的最小值,那个最不配为最大值的数,我们在遍历数据的时候要不断与堆顶元素进行比较,遇到比堆顶元素更符合要求的数就要替换掉堆顶元素,然后再将其调整到它应该在的位置。

  建堆就是建使堆顶元素尽量不符合要求的堆

2.调整堆结构

  在让新数据覆盖到根结点时,此时的堆不一定满足堆结构,需要进行调整。让父结点不断与其孩子结点比较,小堆让最小值到父结点,大堆让最大值到父结点,没有发生交换说明已经符合堆的结构,不需要再调整了。

  首先将前k个数据存放到堆中并使其成堆结构,然后遍历数据,依次与堆顶元素比较,比堆顶元素更符合要求的则覆盖堆顶元素,再使其调整到正确位置上,重复此操作,将所有数据遍历完了以后,堆中存放的就是前k个最值,而堆顶元素就是第k个最值。

四、代码实现

  代码中有部分属于堆实现的函数,在此不过多展开。感兴趣的可以看看堆的实现。

1.库函数头文件及声明

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>
// 宏定义数据类型
typedef int HPDataType;
// 堆结构体
typedef struct Heap
{
    // 指向数组的指针
  HPDataType* a;
  // 堆的有效数据个数
  int size;
  // 堆的容量
  int capacity;
}HP;
// 初始化
void HeapInit(HP* php);
// 销毁
void HeapDestroy(HP* php);
// 向上调整
void AdjustUp(HPDataType* a, int child);
// 向下调整
void AdjustDown(HPDataType* a, int size, int parent);
// 交换
void Swap(HPDataType* a, HPDataType* b);

2.其他函数实现

// 堆的初始化
void HeapInit(HP* php)
{
    // php为指向堆结构体指针,为空说明传参错误
  assert(php);
    // 指向数组的指针置空
  php->a = NULL;
  // 堆的有效数据个数初始为0
  php->size = 0;
  // 堆的容量初始为0
  php->capacity = 0;
}
// 堆的销毁
void HeapDestroy(HP* php)
{
  assert(php);
    // 释放数组空间
  free(php->a);
  // 指针置空
  php->a = NULL;
  // 堆的大小回归默认
  php->size = 0;
  // 堆的容量回归默认
  php->capacity = 0;
}
// 交换函数
void Swap(HPDataType* a, HPDataType* b)
{
  HPDataType tmp = *a;
  *a = *b;
  *b = tmp;
}
// 向上调整
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 = (parent - 1) / 2;
    }
    // 不需要调整说明已经形成堆结构,直接退出
    else
      break;
  }
}
// 向下调整
void AdjustDown(HPDataType* a, int size, int parent)
{
  // 父结点的左孩子
  int child = parent * 2 + 1;
  // 如果左孩子结点在有效数据内
  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;
  }
}

3.在文件中造数据函数

void CreateNDate(const char* filename)
{
  // 造的数据个数
  int n = 10000;
  // 随机时间种子
  srand(time(0));
  // 文件名
  const char* file = filename;
  // 以写方式打开文件,没有则创建
  FILE* fin = fopen(file, "w");
  // 没打开
  if (!fin)
  {
      // 弹出反馈
    perror("fopen error");
    exit(-1);
  }
    
    // 造数据
  for (size_t i = 0; i < n; ++i)
  {
      // 随机值
    int x = (rand() + i) % 1000000;
    // 打印到文件当中
    fprintf(fin, "%d\n", x);
  }
    // 关闭文件
  fclose(fin);
}

4.Top-K函数

void TopK(const char* filename, int k)
{
  // 以读的方式打开文件
  FILE* file = fopen(filename, "r");
  // 没打开
  if (!file)
  {
      // 弹出反馈
    perror("fopen fail");
    exit(-1);
  }
  // 创建堆
  HP hp;
  // 初始化堆
  HeapInit(&hp);
  // 建立k个大小为存储的数据类型的空间
  HPDataType* minheap = (HPDataType*)malloc(sizeof(HPDataType) * k);
  // 将文件前k个元素存入到堆当中
  for (int i = 0; i < k; ++i)
  {
    // 从文件中读取1个数据存放到小堆当中
    fscanf(file, "%d", &minheap[i]);
    // 每读取一个数据向上调整
    AdjustUp(minheap, i);
  }
  // 存储文件数据
  HPDataType x = 0;
  // 文件没读取到末尾
  while (fscanf(file, "%d", &x) != EOF)
  {
    // 如果读取到的数据比小堆中的最小数据要大(更符合要求)
    if (x > minheap[0])
    {
        // 将数据存入堆中
      minheap[0] = x;
      // 新的堆顶元素向下调整
      AdjustDown(minheap, k, 0);
    }
  }
  // 打印堆中元素
  for (int i = 0; i < k; ++i)
    printf("%d ", minheap[i]);
  printf("\n");
}

5.主函数

int main()
{
    // 这个函数在执行过一次后需要注释掉,避免不断更改文件数据
  CreateNDate("data.txt");
  // 前10大的数据
  TopK("data.txt", 10);
  return 0;
}

  CreateNData函数在执行过一次后需要注释掉!!

6.结果演示

  无法验证这是否是真正正确的答案,所以我们往文件里面手动改几个数字,这个数字是刚刚随机数所不能超过的数字。我们再来演示一下。

  嗯,完美。

提一嘴

  代码是实现小堆的,所以找出的是前k个最大值。如果想要最小值,可以在调整函数里面将部分 < 改成 > 即可,至于是哪部分就不需要我多说了。

  我们在向堆中插入k个数据的时候,我们使用的是向上调整函数建立的堆,当然,建立堆也可以使用向下调整函数,至于怎么更改,我也不多说,理解了就非常简单。


总结

  本篇文章到这里就结束了,我们使用了堆的结构来解决Top-K问题。希望本篇文章对你有所帮助,如果发现错误,请读者不吝赐教。谢谢。

目录
相关文章
|
8月前
堆的经典TOP K问题
堆的经典TOP K问题
|
8月前
|
存储 算法 C语言
【数据结构】TOP-K问题/使用堆解决
文章目录 TOP-K问题 一、题目描述 二、 思路: 三、代码实现 1.随机产生一万个数据,存入文件中。 2.找前K个最大值 3.测试类: 四、时间复杂度和空间复杂度分析 TOP-K问题
|
1天前
|
存储 人工智能 程序员
技术心得记录:堆(heap)与栈(stack)的区别
技术心得记录:堆(heap)与栈(stack)的区别
|
1月前
|
存储 Java 程序员
堆栈与堆(Stack vs Heap)有什么区别?
堆栈与堆(Stack vs Heap)有什么区别?
33 0
|
1月前
|
算法
数据结构 - 堆:TOP-K问题
数据结构 - 堆:TOP-K问题
|
1月前
|
存储
数据结构之优先级队列(堆)及top-k问题讲解(二)
数据结构之优先级队列(堆)及top-k问题讲解(二)
16 0
|
1月前
|
存储 安全 Java
数据结构之优先级队列(堆)及top-k问题讲解(一)
数据结构之优先级队列(堆)及top-k问题讲解(一)
32 0
|
6月前
|
算法 C语言
数据结构-堆的实现及应用(堆排序和TOP-K问题)(上)
数据结构-堆的实现及应用(堆排序和TOP-K问题)(上)
|
6月前
|
算法 大数据 C语言
数据结构-堆的实现及应用(堆排序和TOP-K问题)(下)
数据结构-堆的实现及应用(堆排序和TOP-K问题)(下)