文章目录
- 一、题目描述
- 二、为什么这道题值得我们花几分钟弄懂?
- 三、算法原理
-
- 什么是优先级队列(堆)
-
- [C++ priority_queue 常用接口](#C++ priority_queue 常用接口)
- 本题解题思路
- 模拟过程
- 细节注意
- 手写大顶堆的核心逻辑(面试必备)
- 四、代码实现
- 五、总结
- 六、下题预告


一、题目描述
题目链接:力扣 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
二、为什么这道题值得我们花几分钟弄懂?
这道题是优先级队列(堆)的经典入门应用,是大厂面试中考察 堆的基本使用 的高频基础题。它把"每次选最大值"的贪心逻辑和堆的核心特性完美结合,既能检验我们对堆的理解程度,又能训练我们将实际问题转化为数据结构应用的能力,是夯实堆结构基础的必做题。
题目核心价值:
- 基础能力的落地:堆的核心作用是"快速获取最值",本题把这一核心特性直接应用到"每次选最重石头"的场景中,让我们直观理解堆的实际价值。
- 贪心思维的训练:本题的解题逻辑本质是贪心------每一步都选当前最优(最重)的两个石头,堆则是实现这一贪心策略的最优数据结构。
- 面试的"基础筛选题":优先级队列是面试高频考点,本题作为堆的入门应用,能区分"只会背堆的概念"和"能实际应用堆"的候选人。
- 代码简洁性的体现:用堆实现的代码仅需十几行,逻辑清晰且高效,契合面试中"简洁且易读"的代码评分标准。
面试考察的核心方向:
- 堆的核心特性理解:能否意识到"每次取最大值"的场景最适合用大顶堆实现。
- 优先级队列的使用能力:能否熟练使用C++标准库的
priority_queue完成入队、出队、取顶操作。 - 手写堆的能力:这是面试重点------能否脱离标准库,自己实现大顶堆的核心逻辑(上浮、下沉、插入、删除堆顶)。
- 复杂度分析:能否准确分析用堆实现的时间复杂度,理解堆相比"每次排序找最大值"的效率优势。
三、算法原理
什么是优先级队列(堆)
优先级队列(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时,执行以下操作:
- 取出堆顶元素
a(当前最重的石头),并删除堆顶。 - 取出新的堆顶元素
b(当前第二重的石头),并删除堆顶。 - 计算两块石头的重量差
diff = a - b:- 若
diff > 0,将diff入堆(剩余的石头重量); - 若
diff = 0,两块石头完全粉碎,无需入堆。
- 若
- 取出堆顶元素
- 遍历结束后:
- 若堆为空,返回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。
细节注意
- 堆的类型选择:本题需要"每次取最大值",必须用大顶堆 ,C++
priority_queue默认就是大顶堆,无需额外设置。 - 差值处理:只有当差值大于0时才需要入堆,等于0时两块石头都粉碎,无需处理。
- 边界条件:
- 石头数组长度为1时,直接返回该石头的重量;
- 所有石头都粉碎(堆为空)时,返回0。
- 面试重点:笔试可以直接用标准库的
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();
}
};
代码细节说明
- 标准库实现 :
- 核心逻辑:通过
top()取最大值、pop()删最大值、push()插入差值,三步完成一次石头粉碎操作。 - 边界处理:最后通过
empty()判断堆是否为空,避免访问空堆的top()。
- 核心逻辑:通过
- 手写堆实现 :
up函数:新插入的元素从末尾向上比较,若大于父节点则交换,直到满足大顶堆性质。down函数:堆顶元素向下比较,与左右子节点中的最大值交换,直到满足大顶堆性质。- 与标准库接口保持一致,替换后业务逻辑完全不变,符合"开闭原则"。
复杂度分析
- 时间复杂度:O(nlogn)。n是石头的数量,每个石头入堆、出堆各一次,每次堆操作的时间复杂度为O(logn),因此整体为O(nlogn)。
- 空间复杂度:O(n)。堆需要存储所有石头的重量,空间复杂度为O(n)。
五、总结
关键点回顾
- 核心逻辑:大顶堆 + 模拟是解决本题的最优方案,堆的"快速获取最大值"特性完美适配"每次选最重石头"的需求。
- 工具使用:笔试可直接用C++
priority_queue(默认大顶堆),但面试一定必须掌握手写大顶堆的核心逻辑(上浮、下沉)。 - 细节处理:差值为0时无需入堆,堆为空时返回0,避免空指针/越界问题。
六、下题预告
下一篇我们一起学习堆的进阶应用,攻克 力扣 703. 数据流中的第K大元素。这道题会用到小顶堆的核心特性,是面试中考察堆的"进阶高频题",带你从"用大顶堆"过渡到"灵活选择堆类型解决问题"~
😜 能搞定这道堆的入门题,你已经迈出了掌握优先级队列的关键一步!如果对手写堆的上浮/下沉逻辑还有疑问,或者有更简洁的实现思路,都可以在评论区交流~
别忘了点个赞、关个注~(๑˃̵ᴗ˂̵)و 你的支持就是继续肝优质算法内容的最大动力~我们下道题,不见不散~
