C++ 优先级队列 大小堆 模拟 力扣 1046. 最后一块石头的重量 每日一题

文章目录


一、题目描述

题目链接:力扣 1046. 最后一块石头的重量

题目描述:

示例 1:

输入:[2,7,4,1,8,1]

输出:1

解释:

先选出 7 和 8,得到 1,所以数组转换为 [2,4,1,1,1];

再选出 2 和 4,得到 2,所以数组转换为 [2,1,1,1];

再选出 2 和 1,得到 1,所以数组转换为 [1,1,1];

再选出 1 和 1,得到 0,所以数组转换为 [1];

最后剩下 1,返回 1。
提示:

1 <= stones.length <= 30

1 <= stones[i] <= 1000

二、为什么这道题值得我们花几分钟弄懂?

这道题是优先级队列(堆)的经典入门应用,是大厂面试中考察 堆的基本使用 的高频基础题。它把"每次选最大值"的贪心逻辑和堆的核心特性完美结合,既能检验我们对堆的理解程度,又能训练我们将实际问题转化为数据结构应用的能力,是夯实堆结构基础的必做题。

题目核心价值:

  • 基础能力的落地:堆的核心作用是"快速获取最值",本题把这一核心特性直接应用到"每次选最重石头"的场景中,让我们直观理解堆的实际价值。
  • 贪心思维的训练:本题的解题逻辑本质是贪心------每一步都选当前最优(最重)的两个石头,堆则是实现这一贪心策略的最优数据结构。
  • 面试的"基础筛选题":优先级队列是面试高频考点,本题作为堆的入门应用,能区分"只会背堆的概念"和"能实际应用堆"的候选人。
  • 代码简洁性的体现:用堆实现的代码仅需十几行,逻辑清晰且高效,契合面试中"简洁且易读"的代码评分标准。

面试考察的核心方向:

  1. 堆的核心特性理解:能否意识到"每次取最大值"的场景最适合用大顶堆实现。
  2. 优先级队列的使用能力:能否熟练使用C++标准库的priority_queue完成入队、出队、取顶操作。
  3. 手写堆的能力:这是面试重点------能否脱离标准库,自己实现大顶堆的核心逻辑(上浮、下沉、插入、删除堆顶)。
  4. 复杂度分析:能否准确分析用堆实现的时间复杂度,理解堆相比"每次排序找最大值"的效率优势。

三、算法原理

什么是优先级队列(堆)

优先级队列(Priority Queue)是一种特殊的队列,队列中的元素不再遵循"先进先出"的规则,而是按照元素的优先级决定出队顺序(优先级高的先出队)。

在C++中,标准库的priority_queue默认是大顶堆(最大值优先级最高,堆顶是队列中的最大值),底层通常用完全二叉树实现,核心操作的时间复杂度为O(logn):

  • 插入元素:O(logn)
  • 删除堆顶元素:O(logn)
  • 获取堆顶元素:O(1)

C++ priority_queue 常用接口

接口 功能
priority_queue<int> pq; 定义一个存储int类型的大顶堆(默认)
pq.push(x); 将元素x插入堆中,自动调整堆结构
pq.top(); 获取堆顶元素(最大值),不删除
pq.pop(); 删除堆顶元素,自动调整堆结构
pq.size(); 获取堆中元素的个数
pq.empty(); 判断堆是否为空

本题解题思路

本题的核心算法是 "大顶堆 + 模拟":用大顶堆存储所有石头重量,每次取出堆顶的两个最大值(最重的两块石头),按规则处理后将剩余重量重新入堆,直到堆中只剩0或1个元素。

  1. 初始化一个大顶堆,将所有石头的重量入堆。
  2. 当堆中元素数量大于1时,执行以下操作:
    • 取出堆顶元素a(当前最重的石头),并删除堆顶。
    • 取出新的堆顶元素b(当前第二重的石头),并删除堆顶。
    • 计算两块石头的重量差diff = a - b
      • diff > 0,将diff入堆(剩余的石头重量);
      • diff = 0,两块石头完全粉碎,无需入堆。
  3. 遍历结束后:
    • 若堆为空,返回0;
    • 若堆中有一个元素,返回该元素的值。

这个思路的本质是:用大顶堆快速获取每次需要的"最重两块石头",通过贪心策略逐步处理,最终得到剩余石头的重量

模拟过程

我们用示例1完整模拟,一起直观理解每一步的堆状态变化。

场景:示例1 石头重量 [2,7,4,1,8,1]

初始状态:

  • 大顶堆 pq = [8,7,4,1,2,1](堆顶为8)
步骤 堆状态(堆顶在前) 取出的a 取出的b 差值diff 入堆后堆状态
1 [8,7,4,1,2,1] 8 7 1 [4,2,1,1,1]
2 [4,2,1,1,1] 4 2 2 [2,1,1,1]
3 [2,1,1,1] 2 1 1 [1,1,1]
4 [1,1,1] 1 1 0 [1]
5 [1] - - - 结束循环

最终堆中剩余1,返回1。

细节注意

  1. 堆的类型选择:本题需要"每次取最大值",必须用大顶堆 ,C++priority_queue默认就是大顶堆,无需额外设置。
  2. 差值处理:只有当差值大于0时才需要入堆,等于0时两块石头都粉碎,无需处理。
  3. 边界条件:
    • 石头数组长度为1时,直接返回该石头的重量;
    • 所有石头都粉碎(堆为空)时,返回0。
  4. 面试重点:笔试可以直接用标准库的priority_queue,但面试时面试官大概率会要求我们手写大顶堆,而非直接调用容器。

手写大顶堆的核心逻辑(面试必备)

如果面试时要求手写堆,核心需要实现三个函数:

  • up(int idx):元素上浮,维护堆的性质(子节点大于父节点时交换)。
  • down(int idx):元素下沉,维护堆的性质(父节点小于子节点时交换)。
  • push(int x):插入元素(放到数组末尾,然后上浮)。
  • pop():删除堆顶(将最后一个元素放到堆顶,然后下沉)。
  • top():返回堆顶元素(数组第一个元素)。

四、代码实现

方法1:使用C++标准库priority_queue(笔试/日常开发)

cpp 复制代码
#include <vector>
#include <queue>
using namespace std;

class Solution {
public:
    int lastStoneWeight(vector<int>& stones) {
        // 定义大顶堆(默认),存储石头重量
        priority_queue<int> pq;

        // 将所有石头重量入堆
        for(auto s : stones)
            pq.push(s);
        
        // 当堆中元素大于1时,继续处理
        while(pq.size() > 1)
        {
            // 取出最重的石头a
            int a = pq.top();
            pq.pop();
            // 取出第二重的石头b
            int b = pq.top();
            pq.pop();
            
            // 计算差值,只有差值大于0时才入堆
            int diff = a - b;
            if(diff > 0)
                pq.push(diff);
        }

        // 堆为空返回0,否则返回堆顶元素
        return pq.empty() ? 0 : pq.top();
    }
};

方法2:手写大顶堆(面试必备)

cpp 复制代码
#include <vector>
using namespace std;

class MaxHeap {
private:
    vector<int> heap; // 用数组存储堆

    // 元素上浮:维护大顶堆性质
    void up(int idx) {
        while(idx > 0) {
            int parent = (idx - 1) / 2; // 父节点索引
            if(heap[parent] < heap[idx]) {
                swap(heap[parent], heap[idx]);
                idx = parent;
            } else {
                break;
            }
        }
    }

    // 元素下沉:维护大顶堆性质
    void down(int idx) {
        int n = heap.size();
        while(true) {
            int max_pos = idx; // 最大值位置初始化为当前节点
            // 比较左子节点
            if(2*idx + 1 < n && heap[2*idx + 1] > heap[max_pos])
                max_pos = 2*idx + 1;
            // 比较右子节点
            if(2*idx + 2 < n && heap[2*idx + 2] > heap[max_pos])
                max_pos = 2*idx + 2;
            // 最大值就是当前节点,无需下沉
            if(max_pos == idx) break;
            // 交换当前节点和最大值节点
            swap(heap[idx], heap[max_pos]);
            idx = max_pos;
        }
    }

public:
    // 插入元素
    void push(int x) {
        heap.push_back(x);
        up(heap.size() - 1); // 最后一个元素上浮
    }

    // 删除堆顶元素
    void pop() {
        swap(heap[0], heap.back()); // 堆顶和最后一个元素交换
        heap.pop_back(); // 删除最后一个元素
        down(0); // 堆顶元素下沉
    }

    // 获取堆顶元素
    int top() {
        return heap[0];
    }

    // 获取堆大小
    int size() {
        return heap.size();
    }

    // 判断堆是否为空
    bool empty() {
        return heap.empty();
    }
};

class Solution {
public:
    int lastStoneWeight(vector<int>& stones) {
        MaxHeap pq;
        for(auto s : stones)
            pq.push(s);
        
        while(pq.size() > 1)
        {
            int a = pq.top();
            pq.pop();
            int b = pq.top();
            pq.pop();
            
            int diff = a - b;
            if(diff > 0)
                pq.push(diff);
        }

        return pq.empty() ? 0 : pq.top();
    }
};

代码细节说明

  1. 标准库实现
    • 核心逻辑:通过top()取最大值、pop()删最大值、push()插入差值,三步完成一次石头粉碎操作。
    • 边界处理:最后通过empty()判断堆是否为空,避免访问空堆的top()
  2. 手写堆实现
    • up函数:新插入的元素从末尾向上比较,若大于父节点则交换,直到满足大顶堆性质。
    • down函数:堆顶元素向下比较,与左右子节点中的最大值交换,直到满足大顶堆性质。
    • 与标准库接口保持一致,替换后业务逻辑完全不变,符合"开闭原则"。

复杂度分析

  • 时间复杂度:O(nlogn)。n是石头的数量,每个石头入堆、出堆各一次,每次堆操作的时间复杂度为O(logn),因此整体为O(nlogn)。
  • 空间复杂度:O(n)。堆需要存储所有石头的重量,空间复杂度为O(n)。

五、总结

关键点回顾

  1. 核心逻辑:大顶堆 + 模拟是解决本题的最优方案,堆的"快速获取最大值"特性完美适配"每次选最重石头"的需求。
  2. 工具使用:笔试可直接用C++ priority_queue(默认大顶堆),但面试一定必须掌握手写大顶堆的核心逻辑(上浮、下沉)
  3. 细节处理:差值为0时无需入堆,堆为空时返回0,避免空指针/越界问题。

六、下题预告

下一篇我们一起学习堆的进阶应用,攻克 力扣 703. 数据流中的第K大元素。这道题会用到小顶堆的核心特性,是面试中考察堆的"进阶高频题",带你从"用大顶堆"过渡到"灵活选择堆类型解决问题"~

😜 能搞定这道堆的入门题,你已经迈出了掌握优先级队列的关键一步!如果对手写堆的上浮/下沉逻辑还有疑问,或者有更简洁的实现思路,都可以在评论区交流~

别忘了点个赞、关个注~(๑˃̵ᴗ˂̵)و 你的支持就是继续肝优质算法内容的最大动力~我们下道题,不见不散~

相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
Next.js 与 React 深度解析:为什么选择 Next.js?
开发语言·javascript·react.js
KiefaC2 小时前
【C++】特殊类设计
开发语言·c++
blazeDP2 小时前
洛谷P7224 [RC-04] 子集积解析
算法·深度优先·图论
坐怀不乱杯魂2 小时前
Linux - 进程信号
linux·c++
June bug2 小时前
【python基础】常见的数据结构的遍历
开发语言·数据结构·python
冬奇Lab2 小时前
【Kotlin系列14】编译器插件与注解处理器开发:在编译期操控Kotlin
android·开发语言·kotlin·状态模式
程序员小白条2 小时前
面试 Java 基础八股文十问十答第二十一期
java·开发语言·数据库·面试·职场和发展
GGGLF2 小时前
Qt网络/串口通信开发:QByteArray 数据类型转换方法解析
开发语言·qt