【数据结构】用堆解决TOPK问题

设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。

示例:

输入: arr = [1,3,5,7,2,4,6,8], k = 4 输出: [1,2,3,4]

比较替换堆顶的数时,不需要让堆顶与数组的每一个数再进行比较,比较数组减去k个数之后剩下的数就行。

1.堆排序方法

cpp 复制代码
// 选出最小的K个数,要建一个大堆

// 大堆的向下调整算法
void HeapDown(int* heap, int k, int parent) {
    // 1.根据父亲结点得到左孩子节点下标child
    int child = parent * 2 + 1;
    // 2.进入while循环进行向下调整(while的循环条件是child<k)
    while (child < k) {
        // 2.1比较左孩子和右孩子的值,如果右孩子更大,child++,注意要避免越界child+1<k
        if (child + 1 < k && heap[child] < heap[child + 1]) {
            child++;
        }
        // 2.2如果父亲结点的值小于孩子结点,进行交换,交换后将child结点的下标赋值给父亲结点,
        //    根据此时的父亲结点计算下一个child的下标
        if (heap[parent] < heap[child]) {
            int temp = heap[parent];
            heap[parent] = heap[child];
            heap[child] = temp;

            parent = child;
            child = parent * 2 + 1;
        }
        // 2.3如果父亲结点的值已经大于孩子结点,说明不需要再调整,break跳出循环
        else
            break;
    }
}

int* smallestK(int* arr, int arrSize, int k, int* returnSize) {
    // 0.首先处理两种特殊情况K<=0,K>=arrSzie
    if (k <= 0) {
        *returnSize = 0;
        return NULL;
    }
    if (k >= arrSize) {
        int* result = (int*)malloc(sizeof(int) * arrSize);
        memcpy(result, arr, sizeof(int) * arrSize);
        *returnSize = arrSize;
        return result;
    }
    // 1.首先开辟一个空间存我们新建的大堆数据
    int* heap = (int*)malloc(sizeof(int) * arrSize);
    // 2.将传入的数组的数据memcpy到我们新建的空间
    memcpy(heap, arr, sizeof(int) * arrSize);
    // 3.从下往上进行向下调整构建大堆
    for (int i = (arrSize - 1 - 1) / 2; i >= 0; i--) {
        HeapDown(heap, arrSize, i);
    }
    int end = arrSize - 1;
    while (end > 0) {
        int tem = heap[0];
        heap[0] = heap[end];
        heap[end] = tem;
        end--;
        HeapDown(heap, end, 0);
    }
    int* arrtemp = (int*)malloc(sizeof(int) * k);
    memcpy(arrtemp, heap, sizeof(int) * k);
    *returnSize = k;
    return arrtemp;
}
  1. 边界条件的严谨性

    代码开头对k的特殊情况(k<=0k>=数组长度)做了单独处理:

    • k<=0时,返回空数组并设置returnSize=0
    • k大于等于数组长度时,直接返回原数组。
      这种处理避免了后续无效的堆操作,也防止了数组访问越界,体现了对 "异常输入" 的考虑,是健壮性代码的常见做法。
  2. 堆操作的标准化实现

    • 向下调整算法(HeapDown) :这是堆操作的核心,代码正确实现了大堆的向下调整逻辑:

      ① 先比较左右孩子,选择更大的子节点(保证大堆特性);

      ② 若父节点小于子节点,则交换两者,并继续向下调整;

      ③ 若父节点已大于子节点,说明堆已平衡,直接退出。

      其中对child+1 < k的越界检查(避免右孩子不存在时的访问错误),体现了细节处理的严谨性。

    • 堆的构建方式 :从最后一个非叶子节点((arrSize-1-1)/2)开始,向上依次调用HeapDown,这是构建堆的标准高效方法(时间复杂度O(n)),比从根节点逐个插入(O(n log n))更优。

  3. 接口设计的规范性

    函数通过returnSize参数返回结果数组的长度,符合 C 语言中 "动态数组需明确长度" 的设计习惯(调用者可通过returnSize正确遍历结果),避免了因长度未知导致的越界访问。

最优解:

cpp 复制代码
// 选出最小的K个数,要建一个大堆

// 大堆的向下调整算法
void HeapDown(int* heap, int k, int parent) {
    // 1.根据父亲结点得到左孩子节点下标child
    int child = parent * 2 + 1;
    // 2.进入while循环进行向下调整(while的循环条件是child<k)
    while (child < k) {
        // 2.1比较左孩子和右孩子的值,如果右孩子更大,child++,注意要避免越界child+1<k
        if (child + 1 < k && heap[child] < heap[child + 1]) {
            child++;
        }
        // 2.2如果父亲结点的值小于孩子结点,进行交换,交换后将child结点的下标赋值给父亲结点,
        //    根据此时的父亲结点计算下一个child的下标
        if (heap[parent] < heap[child]) {
            int temp = heap[parent];
            heap[parent] = heap[child];
            heap[child] = temp;

            parent = child;
            child = parent * 2 + 1;
        }
        // 2.3如果父亲结点的值已经大于孩子结点,说明不需要再调整,break跳出循环
        else
            break;
    }
}

int* smallestK(int* arr, int arrSize, int k, int* returnSize) {
    // 0.首先处理两种特殊情况K<=0,K>=arrSzie
    if (k <= 0) {
        *returnSize = 0;
        return NULL;
    }
    if (k >= arrSize) {
        int* result = (int*)malloc(sizeof(int) * arrSize);
        memcpy(result, arr, sizeof(int) * arrSize);
        *returnSize = arrSize;
        return result;
    }
    // 1.首先开辟一个空间存我们新建的大堆数据
    int* heap = (int*)malloc(sizeof(int) * k);
    // 2.将传入的数组的数据memcpy到我们新建的空间
    memcpy(heap, arr, sizeof(int) * k);
    // 3.从下往上进行向下调整构建大堆
    for (int i = (k - 1 - 1) / 2; i >= 0; i--) {
        HeapDown(heap, k, i);
    }

    // 4.将新建大堆的堆顶值与传入数组剩下的arrSize-k个数据进行比较,如果堆顶的数更大,替换堆顶的数并进行向下调整
    //j从k开始即可
    for (int j = k; j < arrSize; j++) {
        if (heap[0] > arr[j]) {
            //直接替换即可
            heap[0]=arr[j];

            HeapDown(heap, k, 0);
        }
    }
    *returnSize = k;
    return heap;
}
  1. 大堆的精准应用

    选出 "最小的 K 个数" 时,使用 "大堆" 是非常巧妙的选择:

    • 大堆的堆顶始终是当前 K 个元素中的最大值,便于快速判断新元素是否有资格进入 "最小 K 个" 的集合(只需比较新元素与堆顶)。
    • 若新元素更小,则替换堆顶并调整,保证堆中始终是当前最小的 K 个元素。
      这种思路体现了 "用合适的数据结构解决特定问题" 的设计思想。
  2. 堆构建的高效实现

    • 构建堆时,从最后一个非叶子节点((k-1-1)/2)开始向上调用HeapDown,这是堆初始化的标准高效方法(时间复杂度O(k)),而非从根节点逐个插入(O(k log k))。
    • HeapDown函数的实现严谨:先比较左右孩子取较大值,再与父节点比较交换,确保大堆特性,且对右孩子的越界检查(child+1 < k)避免了数组访问错误。
  3. 内存使用的合理性

    • 堆空间直接分配为k大小(malloc(sizeof(int)*k)),精准匹配需求,不浪费内存。
    • 最终直接返回堆空间作为结果,避免了额外的内存拷贝(上一版中arrtemp的拷贝操作)。
相关推荐
Tisfy几秒前
LeetCode 837.新 21 点:动态规划+滑动窗口
数学·算法·leetcode·动态规划·dp·滑动窗口·概率
CoovallyAIHub32 分钟前
为高空安全上双保险!无人机AI护航,YOLOv5秒判安全带,守护施工生命线
深度学习·算法·计算机视觉
huangzixuan100739 分钟前
08.18总结
算法·深度优先·图论
liang_jy1 小时前
数组(Array)
数据结构·面试·trae
逆向菜鸟1 小时前
【摧毁比特币】椭圆曲线象限细分求k-陈墨仙
python·算法
DolphinDB1 小时前
DolphinDB 回测插件快速上手
算法
利刃大大1 小时前
【动态规划:路径问题】最小路径和 && 地下城游戏
算法·动态规划·cpp·路径问题
武大打工仔2 小时前
用 Java 复现哲学家就餐问题
算法
秋难降3 小时前
LRU缓存算法(最近最少使用算法)——工业界缓存淘汰策略的 “默认选择”
数据结构·python·算法