设计一个算法,找出数组中最小的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;
}
-
边界条件的严谨性
代码开头对
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
正确遍历结果),避免了因长度未知导致的越界访问。
最优解:
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;
}
-
大堆的精准应用
选出 "最小的 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
的拷贝操作)。
- 堆空间直接分配为