算法---二叉堆

二叉堆

二叉堆基础知识

二叉堆分为两种类型:

最大堆 (Max Heap):任何一个父节点的值都大于或等于其子节点的值。根节点是最大值。

最小堆 (Min Heap):任何一个父节点的值都小于或等于其子节点的值。根节点是最小值。

存储方式虽然逻辑上是树形结构,但二叉堆通常使用数组来存储,因为完全二叉树的特性使得索引计算非常简单

若父节点索引为 i i i(从 0 开始):

左子节点索引: 2 i + 1 2i + 1 2i+1

右子节点索引: 2 i + 2 2i + 2 2i+2父节点索引: ( i − 1 ) / 2 (i - 1) / 2 (i−1)/2

核心操作

  1. 插入 (Push):将新元素放在数组末尾,然后向上调整(Sift Up)以维护堆序。
  2. 弹出 (Pop):移除根节点,将数组最后一个元素放到根部,然后向下调整(Sift Down)

和二叉搜索树的区别

特性,二叉堆 (Binary Heap),二叉搜索树 (BST)

主要用途,快速查找最大/最小值(优先队列),快速查找、插入、删除任意元素

顺序性,纵向有序(父比子大),横向无序,全局有序(左 < 根 < 右)

形状限制,必须是完全二叉树,结构紧凑,形状取决于插入顺序(可能退化为链表)

查找效率,查找最大/小值为 O(1),其他为 O(n),查找平均为 O(logn)

特性 二叉堆 二叉搜索树
主要用途 快速查找最大/最小值(优先队列) 快速查找、插入、删除任意元素
顺序性 ,纵向有序(父比子大) 横向无序,全局有序(左 < 根 < 右)
形状限制 必须是完全二叉树 结构紧凑,形状取决于插入顺序(可能退化为链表)
查找效率 查找效率,查找最大/小值为 O(1)其他为 O(n) 查找平均为 O(logn)

代码

cpp 复制代码
class MinHeap {
private:
    std::vector<int> heap;

    // 辅助函数:获取父节点和子节点的索引
    int getParent(int i) { return (i - 1) / 2; }
    int getLeft(int i)   { return 2 * i + 1; }
    int getRight(int i)  { return 2 * i + 2; }
};

向上调整

当你向堆末尾添加一个新元素时,它可能会破坏"父节点比子节点小"的规则。这时需要让它不断与父节点交换,直到找到合适的位置。

cpp 复制代码
void siftUp(int index) {
        // 如果当前节点不是根节点,且值小于父节点,则交换
        while (index > 0 && heap[index] < heap[getParent(index)]) {
            std::swap(heap[index], heap[getParent(index)]);
            index = getParent(index); // 继续向上检查
        }
    }

public:
    void push(int val) {
        heap.push_back(val);      // 1. 先插到最后
        siftUp(heap.size() - 1);  // 2. 向上调整
    }

删除堆顶:向下调整

删除堆顶(最小值)时,我们不能直接删掉 heap[0]。 标准做法: 把数组最后一个元素覆盖到堆顶,然后让它不断与较小的子节点交换,直到重新平衡。

cpp 复制代码
void siftDown(int index) {
        int smallest = index;
        int left = getLeft(index);
        int right = getRight(index);
        int size = heap.size();

        // 找出当前节点、左孩子、右孩子中最小的那个
        if (left < size && heap[left] < heap[smallest]) smallest = left;
        if (right < size && heap[right] < heap[smallest]) smallest = right;

        // 如果最小的不是当前节点,说明需要下沉
        if (smallest != index) {
            std::swap(heap[index], heap[smallest]);
            siftDown(smallest); // 递归或循环继续向下
        }
    }

public:
    void pop() {
        if (heap.empty()) return;
        heap[0] = heap.back();    // 1. 末尾元素覆盖堆顶
        heap.pop_back();          // 2. 删掉末尾
        if (!heap.empty()) {
            siftDown(0);          // 3. 向下调整
        }
    }

其他的操作

cpp 复制代码
int top() {
        return heap.empty() ? -1 : heap[0]; // 简单处理空堆情况
    }

    bool empty() { return heap.empty(); }
};

Top K 问题

数组中的第K个最大元素

思路就是维护一个小堆,堆顶最小,然后往里面加数字,加的数字要是大,就把原来的小堆顶pop出来

把新加入的这个大的加到头部,重新维护小堆,加到最后就会剩下来k个大的数字

堆顶就是第k个最大的元素(因为是小堆)

cpp 复制代码
class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
       priority_queue<int,vector<int>,greater<int>> minHeap;

       for(int num:nums){
        if(minHeap.size()<k){
            minHeap.push(num);
        }else if(minHeap.top()<num){
            minHeap.pop();
            minHeap.push(num);
        }
       } 
       return minHeap.top();
    }
};

前 K 个高频元素

新知识点pair

std容器,pair

因为对只能存储一个数字,而现在需要用哈希表将数字和频率绑定,另外,绑定之后的存入到小堆中,小堆不知道应该如何比较,先比较第一个数字还是先比较第二个数组??

所以这时候用pair函数,他规定了先比较第一个,如果第一个元素一样,在比较第二个

最后在push进入堆中,先push频率在push元素,就可以按频率高低排列。

最后可以成功得到结果

cpp 复制代码
class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        unordered_map<int, int> counts;
        for(int num:nums){
            counts[num]++;
        }

        using PI = pair<int,int>;
        priority_queue<PI,vector<PI>,greater<PI>> minHeap;

        for(auto& it: counts){
            minHeap.push({it.second,it.first});
             if(minHeap.size() > k){
                     minHeap.pop();
            }
        }

        vector<int> result;
        while(!minHeap.empty()){
            result.push_back(minHeap.top().second);
            minHeap.pop();
        }
        return result;
    }
};

多路归并(合并有序序列)

这里自己实现了一下比较规则(a>b)a的优先级更低所以a下沉实现最小堆

不用greater是因为,传入的是指针地址,他回去默认按照指针地址的大小实现最小堆,不会去访问val。

cpp 复制代码
class Solution {
public:
	struct cmp
	{
		bool operator()(ListNode* a, ListNode* b){
			return a->val > b->val;
		}
	};
    ListNode* mergeKLists(vector<ListNode*>& lists) 
    {
		priority_queue<ListNode*,vector<ListNode*>,cmp> pq;
		for(auto node:lists){
		if(node != nullptr){
				pq.push(node);
			}
		}
		ListNode dummy(-1);
		ListNode* p = &dummy;
		while(!pq.empty())
		{
			ListNode* minNode = pq.top();
			pq.pop();
			p->next = minNode;
			p = p->next;
			if(minNode->next != nullptr)
			{
				pq.push(minNode->next);
			}
		}
		return dummy.next;
	}
};

数据流的中位数

利用堆顶堆,左边的数字用大顶堆,右边的数字用小顶堆

cpp 复制代码
class MedianFinder {

    priority_queue<int,vector<int>,less<int>> queMax;
    priority_queue<int,vector<int>,greater<int>> queMin;
public:
    MedianFinder() {
        
    }
    
    void addNum(int num) {
        if(queMax.empty() || num<=queMax.top()){
            queMax.push(num);
            if(queMax.size()>queMin.size()+1){
                queMin.push(queMax.top());
                queMax.pop();
            }
        }else{
            queMin.push(num);
            if(queMin.size()>queMax.size()){
                queMax.push(queMin.top());
                queMin.pop();
            }
        }
    }
    
    double findMedian() {
        if(queMax.size()>queMin.size()){
            return queMax.top();
        }
        return (queMax.top()+queMin.top()) / 2.0;
    }
};

滑动窗口最大值

滑动窗口最大值

思路1:利用二叉堆的top最大最小的性质

利用pair存储索引和数值

然后依次push进二叉堆中

滑动一次之后,先不急于去pop()

等到堆顶元素的索引小于窗口最左边时候(窗口划过达最大值)在把堆顶元素pop()出即可

cpp 复制代码
class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
    priority_queue<pair<int,int>> pq;
    vector<int> res;
	for(int i = 0;i<nums.size();i++){
		pq.push({nums[i],i});
		if(i+1>=k){
			while(pq.top().second > i+1-k){
				pq.pop();
			}
			res.push_back(pq.top().first);
		}
	}
	return res;
}

};

思路2:

利用单调队列,通过维护一个单调队列让队列的头始终是最大值

当push进一个数值,和队列的尾部相比,如果比队列的尾部大,则证明原来的队列的尾部永远不可能是最大值,将未来的尾部pop掉,再重新比较新的尾部和新来的数值,直到尾部比新进来的数值大,此时将新进来的数值push进队列。

当移动窗口的时候,判断左边出队列这个是不是当前的最大值(q.front)如果是则pop出去,如果不是就证明这个数之前已经被淘汰掉了,因为举个例子,342,在4进入的时候,3就已经被剔除掉了。

cpp 复制代码
// 单调队列类封装
class MonotonicQueue {
private:
    // 使用双端队列 deque,因为它支持头部和尾部的高效插入删除
    deque<int> q; 

public:
    // 入队操作:维护队列的单调递减性质
    void push(int n) {
        // 如果队列尾部的元素比新来的 n 小,说明尾部元素"废了"
        // 只要 n 在,尾部那个小的永远不可能是最大值,直接淘汰
        while (!q.empty() && q.back() < n) {
            q.pop_back();
        }
        q.push_back(n);
    }

    // 获取最大值:由于是单调递减,队头永远是最大的
    int max() {
        return q.front();
    }

    // 出队操作:试图移除某个数值 n
    void pop(int n) {
        // 只有当要移除的元素 n 恰好等于队头元素时,才真正弹出
        // 如果 n 不等于队头,说明 n 早就在 push 阶段被淘汰了,不需要操作
        if (!q.empty() && q.front() == n) {
            q.pop_front();
        }
    }
};

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        MonotonicQueue window;
        vector<int> res;
        int left = 0, right = 0;

        while (right < nums.size()) {
            // c 是即将移入窗口的数字
            int c = nums[right];
            right++;
            // 1. 新元素入队(同时淘汰弱者)
            window.push(c);

            // 窗口还没填满 k 个时,不记录结果
            // 当 right - left == k 时,说明窗口大小刚好为 k
            if (right - left == k) {
                // 2. 记录当前窗口的最大值
                res.push_back(window.max());

                // d 是即将移出窗口的数字
                int d = nums[left];
                left++;
                // 3. 旧元素出队(检查是否需要移除队头)
                window.pop(d);
            }
        }
        return res;
    }
};

贪心+堆

贪心策略:我们在每一步都想选择当前"最优"的选项(比如利润最高、结束最早、成本最小)。

堆的作用:由于数据的状态在动态变化(比如做完一个任务,解锁了新任务;或者时间推进,有了新会议),我们需要一个数据结构能 O ( 1 ) O(1) O(1) 拿到当前的最优解,并 O ( log ⁡ N ) O(\log N) O(logN) 维护剩余数据的有序性。

  1. 连接棒材的最低费用

  2. 会议室 II

  3. 最多可以参加的会议数目

  4. IPO (Hard)

  5. 任务调度器

  6. 重构字符串

相关推荐
zhuqiyua6 小时前
第一次课程家庭作业
c++
只是懒得想了6 小时前
C++实现密码破解工具:从MD5暴力破解到现代哈希安全实践
c++·算法·安全·哈希算法
m0_736919107 小时前
模板编译期图算法
开发语言·c++·算法
玖釉-7 小时前
深入浅出:渲染管线中的抗锯齿技术全景解析
c++·windows·图形渲染
【心态好不摆烂】7 小时前
C++入门基础:从 “这是啥?” 到 “好像有点懂了”
开发语言·c++
dyyx1117 小时前
基于C++的操作系统开发
开发语言·c++·算法
AutumnorLiuu7 小时前
C++并发编程学习(一)——线程基础
开发语言·c++·学习
m0_736919107 小时前
C++安全编程指南
开发语言·c++·算法
阿猿收手吧!7 小时前
C++ std::lock与std::scoped_lock深度解析:从死锁解决到安全实践
开发语言·c++
蜡笔小马7 小时前
11.空间索引的艺术:Boost.Geometry R树实战解析
算法·r-tree