【数据结构与算法】第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) 额外空间,如何优化?

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

相关推荐
kaikaile19953 小时前
能量算子的MATLAB实现与详细算法
人工智能·算法·matlab
tankeven3 小时前
HJ175 小红的整数配对
c++·算法
chushiyunen3 小时前
python fastapi使用、uvicorn
开发语言·python·fastapi
Aaron15883 小时前
数字波束合成DBF与模拟波束合成ABF对比浅析
大数据·人工智能·算法·硬件架构·硬件工程·信息与通信·信号处理
成都易yisdong3 小时前
实现三北方向转换计算器(集成 WMM2025 地磁模型)
开发语言·windows·算法·c#·visual studio
汀、人工智能3 小时前
[特殊字符] 第91课:课程表
数据结构·算法·数据库架构·图论·bfs·课程表
ShineWinsu3 小时前
对于Linux:“一切皆文件“以及缓冲区的解析
linux·运维·c++·面试·笔试·缓冲区·一切皆文件
白露与泡影3 小时前
2026 全新 Java 面试题汇总(含答案)
java·开发语言
jinanwuhuaguo3 小时前
OpenClaw 2026年4月升级大系深度解读剖析:从“架构重塑”到“信任内建”的范式跃迁
android·开发语言·人工智能·架构·kotlin·openclaw