【数据结构与算法】第44篇:堆(Heap)的实现

目录

一、堆的基本概念

[1.1 堆的定义](#1.1 堆的定义)

[1.2 堆 vs 堆区](#1.2 堆 vs 堆区)

[1.3 应用场景](#1.3 应用场景)

二、堆的实现(大根堆)

[2.1 结构定义](#2.1 结构定义)

[2.2 初始化与销毁](#2.2 初始化与销毁)

[2.3 辅助函数:交换与扩容](#2.3 辅助函数:交换与扩容)

[2.4 上浮操作(用于插入)](#2.4 上浮操作(用于插入))

[2.5 插入操作](#2.5 插入操作)

[2.6 下沉操作(用于删除最大值)](#2.6 下沉操作(用于删除最大值))

[2.7 删除最大值(弹出堆顶)](#2.7 删除最大值(弹出堆顶))

[2.8 获取堆顶(不删除)](#2.8 获取堆顶(不删除))

[2.9 获取堆大小](#2.9 获取堆大小)

三、完整代码演示

四、堆排序(基于堆实现)

五、小根堆的实现

六、复杂度分析

[七、堆的应用:Top K 问题](#七、堆的应用:Top K 问题)

八、小结

九、思考题


一、堆的基本概念

1.1 堆的定义

堆是一种完全二叉树,分为两种:

  • 大根堆:每个节点的值 ≥ 左右孩子节点的值

  • 小根堆:每个节点的值 ≤ 左右孩子节点的值

用数组存储(下标从0开始):

  • 节点 i 的左孩子:2*i + 1

  • 节点 i 的右孩子:2*i + 2

  • 节点 i 的父节点:(i - 1) / 2

1.2 堆 vs 堆区

概念 含义 说明
堆(数据结构) 一种树形结构,用于优先队列 本章讨论的内容
堆区(内存) 程序运行时动态分配内存的区域 malloc/free 管理的区域

两者是完全不同的概念,只是名字相同。

1.3 应用场景

场景 说明
优先队列 每次取出优先级最高的元素
堆排序 O(n log n) 排序
Top K 问题 找最大/最小的K个元素
合并 K 个有序链表 用堆选择最小值

二、堆的实现(大根堆)

2.1 结构定义

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define INIT_CAPACITY 10

typedef struct {
    int *data;      // 动态数组
    int size;       // 当前元素个数
    int capacity;   // 数组容量
} MaxHeap;

2.2 初始化与销毁

c

复制代码
void initHeap(MaxHeap *heap) {
    heap->data = (int*)malloc(INIT_CAPACITY * sizeof(int));
    heap->size = 0;
    heap->capacity = INIT_CAPACITY;
}

void destroyHeap(MaxHeap *heap) {
    free(heap->data);
    heap->data = NULL;
    heap->size = 0;
    heap->capacity = 0;
}

2.3 辅助函数:交换与扩容

c

复制代码
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void resize(MaxHeap *heap) {
    heap->capacity *= 2;
    heap->data = (int*)realloc(heap->data, heap->capacity * sizeof(int));
}

2.4 上浮操作(用于插入)

新插入的元素放在数组末尾,然后向上调整,直到满足堆性质。

c

复制代码
void shiftUp(MaxHeap *heap, int index) {
    while (index > 0) {
        int parent = (index - 1) / 2;
        if (heap->data[parent] >= heap->data[index]) {
            break;
        }
        swap(&heap->data[parent], &heap->data[index]);
        index = parent;
    }
}

2.5 插入操作

c

复制代码
void push(MaxHeap *heap, int value) {
    if (heap->size >= heap->capacity) {
        resize(heap);
    }
    heap->data[heap->size] = value;
    shiftUp(heap, heap->size);
    heap->size++;
}

2.6 下沉操作(用于删除最大值)

删除堆顶后,将最后一个元素放到堆顶,然后向下调整。

c

复制代码
void shiftDown(MaxHeap *heap, int index) {
    while (index * 2 + 1 < heap->size) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;
        int largest = left;
        
        if (right < heap->size && heap->data[right] > heap->data[left]) {
            largest = right;
        }
        
        if (heap->data[index] >= heap->data[largest]) {
            break;
        }
        
        swap(&heap->data[index], &heap->data[largest]);
        index = largest;
    }
}

2.7 删除最大值(弹出堆顶)

c

复制代码
int pop(MaxHeap *heap) {
    if (heap->size == 0) {
        printf("堆为空\n");
        return -1;
    }
    
    int maxVal = heap->data[0];
    heap->data[0] = heap->data[heap->size - 1];
    heap->size--;
    shiftDown(heap, 0);
    
    return maxVal;
}

2.8 获取堆顶(不删除)

c

复制代码
int top(MaxHeap *heap) {
    if (heap->size == 0) {
        printf("堆为空\n");
        return -1;
    }
    return heap->data[0];
}

2.9 获取堆大小

c

复制代码
int size(MaxHeap *heap) {
    return heap->size;
}

int isEmpty(MaxHeap *heap) {
    return heap->size == 0;
}

三、完整代码演示

c

复制代码
#include <stdio.h>
#include <stdlib.h>

#define INIT_CAPACITY 10

typedef struct {
    int *data;
    int size;
    int capacity;
} MaxHeap;

void initHeap(MaxHeap *heap) {
    heap->data = (int*)malloc(INIT_CAPACITY * sizeof(int));
    heap->size = 0;
    heap->capacity = INIT_CAPACITY;
}

void destroyHeap(MaxHeap *heap) {
    free(heap->data);
    heap->data = NULL;
    heap->size = 0;
    heap->capacity = 0;
}
复制代码
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

void resize(MaxHeap *heap) {
    heap->capacity *= 2;
    heap->data = (int*)realloc(heap->data, heap->capacity * sizeof(int));
}

void shiftUp(MaxHeap *heap, int index) {
    while (index > 0) {
        int parent = (index - 1) / 2;
        if (heap->data[parent] >= heap->data[index]) {
            break;
        }
        swap(&heap->data[parent], &heap->data[index]);
        index = parent;
    }
}

void shiftDown(MaxHeap *heap, int index) {
    while (index * 2 + 1 < heap->size) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;
        int largest = left;
        
        if (right < heap->size && heap->data[right] > heap->data[left]) {
            largest = right;
        }
        
        if (heap->data[index] >= heap->data[largest]) {
            break;
        }
        
        swap(&heap->data[index], &heap->data[largest]);
        index = largest;
    }
}

void push(MaxHeap *heap, int value) {
    if (heap->size >= heap->capacity) {
        resize(heap);
    }
    heap->data[heap->size] = value;
    shiftUp(heap, heap->size);
    heap->size++;
}

int pop(MaxHeap *heap) {
    if (heap->size == 0) {
        printf("堆为空\n");
        return -1;
    }
    
    int maxVal = heap->data[0];
    heap->data[0] = heap->data[heap->size - 1];
    heap->size--;
    shiftDown(heap, 0);
    
    return maxVal;
}

int top(MaxHeap *heap) {
    if (heap->size == 0) {
        printf("堆为空\n");
        return -1;
    }
    return heap->data[0];
}

int size(MaxHeap *heap) {
    return heap->size;
}

int isEmpty(MaxHeap *heap) {
    return heap->size == 0;
}

void printHeap(MaxHeap *heap) {
    printf("堆内容(数组形式): ");
    for (int i = 0; i < heap->size; i++) {
        printf("%d ", heap->data[i]);
    }
    printf("\n");
}

int main() {
    MaxHeap heap;
    initHeap(&heap);
    
    printf("=== 插入元素 ===\n");
    int values[] = {10, 20, 15, 30, 40, 25, 5};
    for (int i = 0; i < 7; i++) {
        push(&heap, values[i]);
        printf("插入 %d 后,堆顶: %d\n", values[i], top(&heap));
    }
    
    printHeap(&heap);
    printf("堆大小: %d\n", size(&heap));
    
    printf("\n=== 弹出元素 ===\n");
    while (!isEmpty(&heap)) {
        int val = pop(&heap);
        printf("弹出: %d, 剩余堆大小: %d\n", val, size(&heap));
    }
    
    destroyHeap(&heap);
    return 0;
}

运行结果:

text

复制代码
=== 插入元素 ===
插入 10 后,堆顶: 10
插入 20 后,堆顶: 20
插入 15 后,堆顶: 20
插入 30 后,堆顶: 30
插入 40 后,堆顶: 40
插入 25 后,堆顶: 40
插入 5 后,堆顶: 40
堆内容(数组形式): 40 30 25 10 20 15 5 
堆大小: 7

=== 弹出元素 ===
弹出: 40, 剩余堆大小: 6
弹出: 30, 剩余堆大小: 5
弹出: 25, 剩余堆大小: 4
弹出: 20, 剩余堆大小: 3
弹出: 15, 剩余堆大小: 2
弹出: 10, 剩余堆大小: 1
弹出: 5, 剩余堆大小: 0

四、堆排序(基于堆实现)

c

复制代码
void heapSort(int arr[], int n) {
    MaxHeap heap;
    initHeap(&heap);
    
    // 建堆
    for (int i = 0; i < n; i++) {
        push(&heap, arr[i]);
    }
    
    // 从大到小输出(大根堆弹出最大值)
    for (int i = n - 1; i >= 0; i--) {
        arr[i] = pop(&heap);
    }
    
    destroyHeap(&heap);
}

int main() {
    int arr[] = {3, 5, 1, 8, 2, 9, 4, 7, 6};
    int n = sizeof(arr) / sizeof(arr[0]);
    
    printf("原数组: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
    
    heapSort(arr, n);
    
    printf("排序后: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
    
    return 0;
}

运行结果:

text

复制代码
原数组: 3 5 1 8 2 9 4 7 6 
排序后: 1 2 3 4 5 6 7 8 9 

五、小根堆的实现

将大根堆的 shiftUpshiftDown 中的比较符号反转即可。

c

复制代码
// 小根堆的上浮
void shiftUpMin(MaxHeap *heap, int index) {
    while (index > 0) {
        int parent = (index - 1) / 2;
        if (heap->data[parent] <= heap->data[index]) {  // 父 ≤ 子则停止
            break;
        }
        swap(&heap->data[parent], &heap->data[index]);
        index = parent;
    }
}

// 小根堆的下沉
void shiftDownMin(MaxHeap *heap, int index) {
    while (index * 2 + 1 < heap->size) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;
        int smallest = left;
        
        if (right < heap->size && heap->data[right] < heap->data[left]) {
            smallest = right;
        }
        
        if (heap->data[index] <= heap->data[smallest]) {
            break;
        }
        
        swap(&heap->data[index], &heap->data[smallest]);
        index = smallest;
    }
}

六、复杂度分析

操作 时间复杂度 说明
插入(push) O(log n) 上浮,最多树高次
删除最大值(pop) O(log n) 下沉,最多树高次
取堆顶(top) O(1) 直接返回数组0
建堆(heapify) O(n) 从最后一个非叶子节点下沉

七、堆的应用:Top K 问题

找数组中最大的 K 个元素,用小根堆实现 O(n log K)。

c

复制代码
int* findTopK(int arr[], int n, int k) {
    if (k <= 0 || k > n) return NULL;
    
    MaxHeap minHeap;  // 实际用小根堆
    // 注意:这里用大根堆取负值模拟小根堆,或单独实现小根堆
    
    // 简化版:先插入k个元素
    // 然后遍历剩余元素,比堆顶大则替换
    
    // 完整实现略,原理相同
}

八、小结

这一篇我们实现了基于动态数组的大根堆:

操作 函数 核心算法
插入 push 上浮(shiftUp)
删除最大值 pop 下沉(shiftDown)
取堆顶 top O(1)

关键点

  • 堆是完全二叉树,用数组存储

  • 大根堆:父 ≥ 子,堆顶最大

  • 上浮:新元素在末尾,向上比较交换

  • 下沉:堆顶元素被删除,末尾元素补上,向下比较交换

注意区分

  • 数据结构堆(本文):树形结构,优先队列

  • 内存堆区:malloc 管理的内存区域

下一篇我们讲跳跃表(Skip List)。


九、思考题

  1. 为什么堆用数组存储而不需要显式的左右指针?

  2. 建堆时,为什么从最后一个非叶子节点开始向下调整,而不是从根开始?

  3. 如何用堆实现一个优先队列,支持按优先级取出元素?

  4. 堆排序的空间复杂度是 O(1),本文的堆排序用了 O(n) 额外空间,如何优化?

欢迎在评论区讨论你的答案。

相关推荐
复杂网络1 小时前
Stable Diffusion 视觉大模型微调技术深度调研
算法
复杂网络1 小时前
基于 Stable Diffusion 架构的视觉大模型代表性工作与原理深度解析
算法
MrZhao4001 小时前
Agent Loop 如何用 Hook 扩展:权限、日志与工具拦截
算法
MrZhao4001 小时前
Agent 为什么需要 Skills:别把所有知识都塞进 system prompt
算法
JieE2122 天前
LeetCode 101. 对称二叉树|JS 递归 + 迭代双解法,彻底搞懂镜像判断
javascript·算法
JieE2122 天前
LeetCode 56. 合并区间|超清晰 JS 图解思路,面试高频区间题
javascript·算法·面试
Jack203 天前
HarmonyOS开发中错误处理策略:网络异常统一处理
算法
小小杨树3 天前
读懂色彩:拍照调色不再难
算法·计算机视觉·配色
JieE2123 天前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2124 天前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法