c++算法之数据结构篇 - 堆

一、堆的基本概念

1.1 什么是堆?生活中的比喻

想象一下你有一堆大小不一的苹果,现在要把它们整理成一种特殊的结构:

  • 最大堆:最大的苹果永远在最上面,每个苹果都比它下面的所有苹果大
  • 最小堆:最小的苹果永远在最上面,每个苹果都比它下面的所有苹果小

这就是堆的基本思想!堆是一种特殊的完全二叉树,它满足以下两个条件:

  1. 结构性质:它是一棵完全二叉树(除了最后一层,其他层都是满的,最后一层从左到右填充)
  2. 堆性质:每个节点的值都大于等于(或小于等于)其子节点的值

1.2 堆的类型

堆主要分为两种类型:

  1. 最大堆(Max Heap)

    • 父节点的值 ≥ 子节点的值
    • 根节点是堆中的最大值
    • 像一个金字塔,塔尖是最大的
  2. 最小堆(Min Heap)

    • 父节点的值 ≤ 子节点的值
    • 根节点是堆中的最小值
    • 像一个倒金字塔,塔尖是最小的

1.3 为什么需要堆?

堆解决了两个重要问题:

  1. 快速找到最大/最小值:O(1)时间复杂度
  2. 高效插入和删除:O(log n)时间复杂度

这就像一个智能的排队系统:

  • 医院急诊室:最紧急的病人(最大值)优先处理
  • 操作系统任务调度:优先级最高的任务先执行

二、堆的存储方式

2.1 数组表示法

虽然堆在概念上是树形结构,但在计算机中我们通常用数组来存储它!为什么?因为完全二叉树有很好的数学性质:

对于数组中索引为 i 的元素:

  • 它的父节点索引 = (i - 1) / 2
  • 它的左子节点索引 = 2 * i + 1
  • 它的右子节点索引 = 2 * i + 2

示例:最大堆 [100, 80, 90, 50, 60, 70, 40]

数组表示:[100, 80, 90, 50, 60, 70, 40]

树形结构:
       100
      /   \
     80    90
    / \   / \
   50 60 70 40

2.2 为什么用数组存储?

  1. 空间效率:不需要存储指针,节省空间
  2. 缓存友好:连续内存访问,CPU缓存命中率高
  3. 计算简单:通过简单计算就能找到父子节点关系

三、堆的基本操作

3.1 堆化(Heapify)

堆化是堆的核心操作,它确保一个节点满足堆的性质。有两种堆化方式:

3.1.1 自顶向下堆化(向下调整)

当某个节点的值变小(最大堆)或变大(最小堆)时,需要向下调整:

void maxHeapify(vector<int>& heap, int size, int index) {
    int largest = index;        // 初始化最大值为当前节点
    int left = 2 * index + 1;   // 左子节点
    int right = 2 * index + 2;  // 右子节点

    // 如果左子节点存在且大于当前最大值
    if (left < size && heap[left] > heap[largest])
        largest = left;

    // 如果右子节点存在且大于当前最大值
    if (right < size && heap[right] > heap[largest])
        largest = right;

    // 如果最大值不是当前节点,交换并继续堆化
    if (largest != index) {
        swap(heap[index], heap[largest]);
        maxHeapify(heap, size, largest);  // 递归堆化
    }
}

过程比喻:就像一个家庭会议,如果孩子比家长更"重要"(值更大),就让"孩子"上位,然后继续检查这个"孩子"是否需要继续调整位置。

3.1.2 自底向上堆化(向上调整)

当在堆末尾插入新元素时,需要向上调整:

void heapifyUp(vector<int>& heap, int index) {
    while (index > 0) {
        int parent = (index - 1) / 2;  // 父节点索引
        
        // 如果当前节点大于父节点(最大堆),交换
        if (heap[index] > heap[parent]) {
            swap(heap[index], heap[parent]);
            index = parent;  // 继续向上比较
        } else {
            break;  // 满足堆性质,停止
        }
    }
}

过程比喻:就像新员工入职,如果比上级能力强,就向上级挑战,赢了就交换位置,直到遇到更强的上级或成为CEO。

3.2 建堆(Build Heap)

将一个无序数组构建成堆,有两种方法:

3.2.1 逐个插入法(O(n log n))
void buildHeapSlow(vector<int>& heap) {
    for (int i = 0; i < heap.size(); i++) {
        heapifyUp(heap, i);  // 对每个元素进行向上调整
    }
}
3.2.2 Floyd建堆法(O(n))

更高效的方法是从最后一个非叶子节点开始,向前进行向下调整:

void buildHeap(vector<int>& heap) {
    // 从最后一个非叶子节点开始(最后一个节点的父节点)
    for (int i = (heap.size() / 2) - 1; i >= 0; i--) {
        maxHeapify(heap, heap.size(), i);
    }
}

为什么Floyd方法更快?

  • 大部分节点都在堆的底部,向下调整的路径很短
  • 时间复杂度从O(n log n)降到O(n),这是堆的"魔法"之一

3.3 插入操作(Insert)

  1. 将新元素添加到数组末尾
  2. 进行向上调整(heapifyUp)
void insert(vector<int>& heap, int value) {
    heap.push_back(value);      // 添加到末尾
    heapifyUp(heap, heap.size() - 1);  // 向上调整
}

过程比喻:新成员加入团队,从最底层开始,如果能力比上级强,就不断向上晋升。

3.4 删除堆顶元素(Extract Max/Min)

  1. 保存堆顶元素(要返回的值)
  2. 将最后一个元素移到堆顶
  3. 进行向下调整(heapifyDown)
int extractMax(vector<int>& heap) {
    if (heap.empty()) {
        throw runtime_error("Heap is empty!");
    }
    
    int max = heap[0];          // 保存最大值
    heap[0] = heap.back();      // 将最后一个元素移到堆顶
    heap.pop_back();            // 删除最后一个元素
    maxHeapify(heap, heap.size(), 0);  // 向下调整
    
    return max;
}

过程比喻:CEO离职,让最底层的员工临时接替,然后这个临时CEO需要和下属比较,如果下属更强,就让位,直到找到合适的位置。

四、堆的应用

4.1 优先队列(Priority Queue)

堆最经典的应用就是实现优先队列:

  • 医院急诊系统:根据病情严重程度(优先级)安排治疗顺序
  • 操作系统任务调度:高优先级任务先执行
  • 事件驱动模拟:最早发生的事件先处理
// 使用C++标准库的优先队列(默认最大堆)
#include <queue>
priority_queue<int> maxHeap;  // 最大堆
priority_queue<int, vector<int>, greater<int>> minHeap;  // 最小堆

// 操作示例
maxHeap.push(30);
maxHeap.push(10);
maxHeap.push(50);
cout << maxHeap.top();  // 输出50
maxHeap.pop();          // 删除50

4.2 堆排序(Heap Sort)

堆排序是一种高效的排序算法,时间复杂度O(n log n):

void heapSort(vector<int>& arr) {
    // 1. 构建最大堆
    buildHeap(arr);
    
    // 2. 逐个提取最大元素
    for (int i = arr.size() - 1; i > 0; i--) {
        swap(arr[0], arr[i]);      // 将当前最大值移到末尾
        maxHeapify(arr, i, 0);     // 调整剩余元素为堆
    }
}

过程比喻

  1. 先把所有人按能力排成金字塔(建堆)
  2. 让塔尖的人(最强)站到队伍最后
  3. 重新组织剩下的人,让新的最强者到塔尖
  4. 重复直到所有人都排好序

4.3 其他应用

  1. Top K问题:快速找出数组中最大的K个元素
  2. 中位数查找:使用一个最大堆和一个最小堆
  3. Dijkstra算法:使用最小堆优化最短路径查找
  4. 哈夫曼编码:构建最优二叉树

五、手动实现一个堆类

下面是一个完整的最大堆实现:

#include <vector>
#include <algorithm>
#include <stdexcept>
using namespace std;

class MaxHeap {
private:
    vector<int> heap;

    // 向上调整
    void heapifyUp(int index) {
        while (index > 0) {
            int parent = (index - 1) / 2;
            if (heap[index] > heap[parent]) {
                swap(heap[index], heap[parent]);
                index = parent;
            } else {
                break;
            }
        }
    }

    // 向下调整
    void heapifyDown(int index) {
        int size = heap.size();
        while (true) {
            int left = 2 * index + 1;
            int right = 2 * index + 2;
            int largest = index;

            if (left < size && heap[left] > heap[largest])
                largest = left;
            if (right < size && heap[right] > heap[largest])
                largest = right;

            if (largest != index) {
                swap(heap[index], heap[largest]);
                index = largest;
            } else {
                break;
            }
        }
    }

public:
    // 插入元素
    void push(int value) {
        heap.push_back(value);
        heapifyUp(heap.size() - 1);
    }

    // 删除堆顶元素
    void pop() {
        if (heap.empty()) {
            throw runtime_error("Heap is empty!");
        }
        heap[0] = heap.back();
        heap.pop_back();
        if (!heap.empty()) {
            heapifyDown(0);
        }
    }

    // 获取堆顶元素
    int top() const {
        if (heap.empty()) {
            throw runtime_error("Heap is empty!");
        }
        return heap[0];
    }

    // 检查堆是否为空
    bool empty() const {
        return heap.empty();
    }

    // 获取堆的大小
    size_t size() const {
        return heap.size();
    }

    // 构建堆
    void buildHeap(const vector<int>& arr) {
        heap = arr;
        for (int i = (heap.size() / 2) - 1; i >= 0; i--) {
            heapifyDown(i);
        }
    }
};

六、堆的时间复杂度分析

操作时间复杂度说明
建堆O(n)Floyd算法
插入O(log n)只需要向上调整
删除堆顶O(log n)只需要向下调整
获取堆顶元素O(1)直接访问数组第一个元素
堆排序O(n log n)建堆O(n) + n次删除O(log n)

为什么建堆是O(n)而不是O(n log n)?

  • 大部分节点都在堆的底部,向下调整的路径很短
  • 数学证明:总操作次数 ≈ n - log₂(n+1) ≈ O(n)

七、堆的常见问题和优化

7.1 常见问题

  1. 堆溢出:当堆的大小超过系统限制

    • 解决方案:使用动态数组(如vector)
  2. 重复元素:堆可以处理重复元素,但需要明确定义比较规则

  3. 稳定性:堆排序不是稳定排序(相同值的元素相对顺序可能改变)

7.2 优化技巧

  1. 使用迭代代替递归:避免递归深度过大
   // 迭代版本的heapifyDown
   void heapifyDown(int index) {
       int size = heap.size();
       while (true) {
           int left = 2 * index + 1;
           int right = 2 * index + 2;
           int largest = index;

           if (left < size && heap[left] > heap[largest])
               largest = left;
           if (right < size && heap[right] > heap[largest])
               largest = right;

           if (largest != index) {
               swap(heap[index], heap[largest]);
               index = largest;
           } else {
               break;
           }
       }
   }
  1. 批量建堆:直接使用Floyd算法,比逐个插入更高效

  2. 使用更高效的数据结构:对于特定场景,可以考虑斐波那契堆等

7.3 堆的变种

  1. 二项堆(Binomial Heap):支持更高效的合并操作
  2. 斐波那契堆(Fibonacci Heap):理论上有更好的平摊时间复杂度
  3. d-ary堆:每个节点有d个子节点,减少堆的高度

八、C++标准库中的堆

8.1 std::priority_queue

C++标准库提供了优先队列容器适配器:

#include <queue>

// 默认最大堆
priority_queue<int> maxHeap;

// 最小堆
priority_queue<int, vector<int>, greater<int>> minHeap;

// 自定义比较函数(最大堆)
auto cmp = [](int a, int b) { return a < b; };
priority_queue<int, vector<int>, decltype(cmp)> customHeap(cmp);

8.2 堆算法

C++算法库提供了堆操作函数:

#include <algorithm>
vector<int> vec = {3, 1, 4, 1, 5, 9};

// 建堆(默认最大堆)
make_heap(vec.begin(), vec.end());

// 插入元素
vec.push_back(6);
push_heap(vec.begin(), vec.end());

// 删除堆顶
pop_heap(vec.begin(), vec.end());
vec.pop_back();

// 检查是否是堆
bool isHeap = is_heap(vec.begin(), vec.end());

// 排序堆(变成有序序列)
sort_heap(vec.begin(), vec.end());

九、堆的实际应用案例

9.1 合并K个有序数组

vector<int> mergeKSortedArrays(vector<vector<int>>& arrays) {
    // 最小堆,存储元素值和来自哪个数组
    priority_queue<pair<int, pair<int, int>>, 
                  vector<pair<int, pair<int, int>>>, 
                  greater<pair<int, pair<int, int>>>> minHeap;
    
    // 初始化堆,放入每个数组的第一个元素
    for (int i = 0; i < arrays.size(); i++) {
        if (!arrays[i].empty()) {
            minHeap.push({arrays[i][0], {i, 0}});
        }
    }
    
    vector<int> result;
    while (!minHeap.empty()) {
        auto current = minHeap.top();
        minHeap.pop();
        result.push_back(current.first);
        
        int arrayIndex = current.second.first;
        int elementIndex = current.second.second;
        
        // 如果当前数组还有下一个元素,放入堆中
        if (elementIndex + 1 < arrays[arrayIndex].size()) {
            minHeap.push({arrays[arrayIndex][elementIndex + 1], 
                         {arrayIndex, elementIndex + 1}});
        }
    }
    
    return result;
}

9.2 查找数据流中的中位数

class MedianFinder {
private:
    priority_queue<int> maxHeap;   // 存储较小的一半
    priority_queue<int, vector<int>, greater<int>> minHeap;  // 存储较大的一半

public:
    void addNum(int num) {
        if (maxHeap.empty() || num <= maxHeap.top()) {
            maxHeap.push(num);
        } else {
            minHeap.push(num);
        }
        
        // 平衡两个堆的大小
        if (maxHeap.size() > minHeap.size() + 1) {
            minHeap.push(maxHeap.top());
            maxHeap.pop();
        } else if (minHeap.size() > maxHeap.size()) {
            maxHeap.push(minHeap.top());
            minHeap.pop();
        }
    }
    
    double findMedian() {
        if (maxHeap.size() == minHeap.size()) {
            return (maxHeap.top() + minHeap.top()) / 2.0;
        } else {
            return maxHeap.top();
        }
    }
};

十、总结

10.1 关键要点回顾

  1. 堆的本质:一种特殊的完全二叉树,满足堆性质
  2. 核心操作
    • 堆化(向上调整和向下调整)
    • 建堆(Floyd算法O(n))
    • 插入和删除(O(log n))
  3. 存储方式:用数组存储,通过索引计算父子关系
  4. 主要应用
    • 优先队列
    • 堆排序
    • Top K问题
    • 中位数查找

10.2 学习建议

  1. 动手实现:亲自编写堆的各个操作,加深理解
  2. 可视化工具:使用可视化工具观察堆的调整过程
  3. 实际应用:尝试用堆解决实际问题,如合并有序数组
  4. 性能测试:比较不同建堆方法的性能差异

10.3 进阶方向

  1. 高级堆结构:学习二项堆、斐波那契堆等
  2. 并行堆:研究多线程环境下的堆操作
  3. 外部堆:处理无法全部装入内存的大数据
  4. 持久化堆:支持历史版本查询的堆结构

堆是计算机科学中最重要的数据结构之一,它不仅理论优美,而且应用广泛。掌握了堆,你就掌握了解决许多实际问题的强大工具!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值