【数据结构】用堆解决TOPK问题
设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。
示例:
输入: arr = [1,3,5,7,2,4,6,8], k = 4 输出: [1,2,3,4]
比较替换堆顶的数时,不需要让堆顶与数组的每一个数再进行比较,比较数组减去k个数之后剩下的数就行。
1.堆排序方法
// 选出最小的K个数,要建一个大堆// 大堆的向下调整算法
void HeapDown(int* heap, int k, int parent) {// 1.根据父亲结点得到左孩子节点下标childint child = parent * 2 + 1;// 2.进入while循环进行向下调整(while的循环条件是child<k)while (child < k) {// 2.1比较左孩子和右孩子的值,如果右孩子更大,child++,注意要避免越界child+1<kif (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跳出循环elsebreak;}
}int* smallestK(int* arr, int arrSize, int k, int* returnSize) {// 0.首先处理两种特殊情况K<=0,K>=arrSzieif (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;
}
边界条件的严谨性
代码开头对k
的特殊情况(k<=0
和k>=数组长度
)做了单独处理:- 当
k<=0
时,返回空数组并设置returnSize=0
; - 当
k
大于等于数组长度时,直接返回原数组。
这种处理避免了后续无效的堆操作,也防止了数组访问越界,体现了对 “异常输入” 的考虑,是健壮性代码的常见做法。
- 当
堆操作的标准化实现
向下调整算法(HeapDown):这是堆操作的核心,代码正确实现了大堆的向下调整逻辑:
① 先比较左右孩子,选择更大的子节点(保证大堆特性);
② 若父节点小于子节点,则交换两者,并继续向下调整;
③ 若父节点已大于子节点,说明堆已平衡,直接退出。
其中对child+1 < k
的越界检查(避免右孩子不存在时的访问错误),体现了细节处理的严谨性。堆的构建方式:从最后一个非叶子节点(
(arrSize-1-1)/2
)开始,向上依次调用HeapDown
,这是构建堆的标准高效方法(时间复杂度O(n)
),比从根节点逐个插入(O(n log n)
)更优。
接口设计的规范性
函数通过returnSize
参数返回结果数组的长度,符合 C 语言中 “动态数组需明确长度” 的设计习惯(调用者可通过returnSize
正确遍历结果),避免了因长度未知导致的越界访问。
最优解:
// 选出最小的K个数,要建一个大堆// 大堆的向下调整算法
void HeapDown(int* heap, int k, int parent) {// 1.根据父亲结点得到左孩子节点下标childint child = parent * 2 + 1;// 2.进入while循环进行向下调整(while的循环条件是child<k)while (child < k) {// 2.1比较左孩子和右孩子的值,如果右孩子更大,child++,注意要避免越界child+1<kif (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跳出循环elsebreak;}
}int* smallestK(int* arr, int arrSize, int k, int* returnSize) {// 0.首先处理两种特殊情况K<=0,K>=arrSzieif (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;
}
大堆的精准应用
选出 “最小的 K 个数” 时,使用 “大堆” 是非常巧妙的选择:- 大堆的堆顶始终是当前 K 个元素中的最大值,便于快速判断新元素是否有资格进入 “最小 K 个” 的集合(只需比较新元素与堆顶)。
- 若新元素更小,则替换堆顶并调整,保证堆中始终是当前最小的 K 个元素。
这种思路体现了 “用合适的数据结构解决特定问题” 的设计思想。
堆构建的高效实现
- 构建堆时,从最后一个非叶子节点(
(k-1-1)/2
)开始向上调用HeapDown
,这是堆初始化的标准高效方法(时间复杂度O(k)
),而非从根节点逐个插入(O(k log k)
)。 HeapDown
函数的实现严谨:先比较左右孩子取较大值,再与父节点比较交换,确保大堆特性,且对右孩子的越界检查(child+1 < k
)避免了数组访问错误。
- 构建堆时,从最后一个非叶子节点(
内存使用的合理性
- 堆空间直接分配为
k
大小(malloc(sizeof(int)*k)
),精准匹配需求,不浪费内存。 - 最终直接返回堆空间作为结果,避免了额外的内存拷贝(上一版中
arrtemp
的拷贝操作)。
- 堆空间直接分配为