大顶堆实战:C++ priority_queue解决LeetCode 1046最后一块石头的重量

发布时间:2026/8/10 3:41:57
大顶堆实战:C++ priority_queue解决LeetCode 1046最后一块石头的重量 这次我们来看力扣LeetCode第1046题“最后一块石头的重量”。这道题本身并不复杂但它是一个绝佳的窗口让我们能深入理解“大顶堆”Max Heap这一数据结构的核心思想与实战应用。很多人在学习数据结构时感觉理论枯燥做题时又不知如何下手这正是“知”与“行”的脱节。本文将带你用C的STL标准模板库工具直接解决这道题并在此过程中让你真正领悟如何将数据结构的理论知识转化为解决实际问题的代码能力。本文将重点关注如何用最直接的方式解题并深入分析背后的原理。我们会先看题目能不能用大顶堆解决再详细拆解怎么用。内容涵盖从问题理解、核心数据结构选择priority_queue、C代码实现、复杂度分析到如何将这种解题思维迁移到其他问题上。无论你是正在准备算法面试还是希望巩固数据结构知识这篇文章都能提供一条清晰的“知行合一”的实践路径。1. 核心能力速览大顶堆与力扣1046题在深入代码之前我们先快速把握解决这个问题的核心工具和思路。能力项说明问题类型模拟、贪心、堆优先队列应用核心数据结构大顶堆 (Max Heap)STL实现工具std::priority_queueT(默认即为大顶堆)时间复杂度O(n log n)其中 n 为石头数量空间复杂度O(n)解题关键每次从堆中取出两个最重的石头进行碰撞将剩余重量如果有放回堆中重复此过程直至堆中元素少于2个。适合读者算法初学者、准备技术面试者、希望理解堆数据结构实战应用的学习者这道题完美诠释了“选择合适的数据结构问题就解决了一半”。大顶堆能让我们在O(1)时间内获取当前最重的石头这是高效解题的关键。2. 适用场景与使用边界2.1 这道题解决了什么问题力扣1046题描述了一个简单的物理模拟过程有一堆石头每块石头的重量都是正整数。每一回合选出两块最重的石头进行碰撞。假设石头重量分别为x和y且x y碰撞结果如下如果x y两块石头都会完全粉碎。如果x ! y重量为x的石头会粉碎重量为y的石头新重量为y - x。 重复这个过程直到最多剩下一块石头。返回此石头的重量如果没有石头剩下就返回0。这个问题本质上是一个持续动态获取最大值并更新集合的过程。手动排序或每次遍历查找最大值都会导致超时或代码低效而大顶堆正是为此类场景量身定做。2.2 大顶堆为什么是首选动态维护最值堆可以在插入和删除元素时以O(log n)的代价维护最大或最小元素在堆顶避免每次O(n)的查找。操作高效对于本题的模拟过程取两个最大可能插入一个差值堆的操作序列两次pop一次push在时间复杂度上是最优的。STL直接支持C的std::priority_queue开箱即用无需手动实现堆的调整算法让开发者能聚焦于问题逻辑本身。2.3 思维迁移哪些问题也适用这种模式当你遇到问题描述中包含“每次取最大/最小的几个元素进行处理并可能将处理结果放回”时就应该立即想到堆优先队列。典型场景包括任务调度总是先执行优先级最高的任务。合并K个有序链表每次从K个链表的头节点中取最小的。数据流的中位数使用一个大顶堆和一个小顶堆共同维护。哈夫曼编码每次合并频率最小的两个节点。理解1046题的解法就掌握了解决这一类问题的通用钥匙。3. 环境准备与前置条件在开始编码之前确保你的开发环境已经就绪。本题不涉及复杂的依赖或硬件要求重点在于编程语言和工具链。3.1 软件环境准备C编译器支持C11或更高版本。常见选择有GCC(MinGW-w64) 适用于Windows可通过MSYS2或MinGW安装、Linux和macOS。Clang 在macOS和Linux上常见。Microsoft Visual C 在Windows上使用Visual Studio或VS Code配合MSVC工具链。代码编辑器或IDEVisual Studio Code 轻量级需安装C/C扩展。CLion JetBrains出品功能强大的跨平台C IDE。Visual Studio Windows平台功能最全面的IDE。调试工具 熟悉使用IDE内置调试器或GDB/LLDB进行单步调试、查看变量对于理解程序运行过程至关重要。3.2 知识前置条件基础C语法 了解vector、循环、条件判断等。STL容器基本概念 知道vector、queue等容器的用途。堆Heap的概念 至少理解堆是一种特殊的完全二叉树父节点的值总是大于大顶堆或小于小顶堆其子节点的值。无需手动实现但需理解其特性。3.3 力扣平台准备如果你选择在力扣官网直接解题拥有一个力扣账户。在题目页面语言选择C。系统会自动提供一个函数签名作为起点你只需要在函数体内实现逻辑。4. 核心解法拆解与C实现现在我们进入核心环节如何用C STL中的priority_queue来解决这个问题。4.1 解题思路步骤化初始化大顶堆 将所有石头的重量放入一个大顶堆中。在C中std::priority_queueint默认就是大顶堆。模拟碰撞循环 当堆中的石头数量大于1时持续进行以下操作 a.取出最重的两块石头 通过top()和pop()操作获取并移除堆顶元素即当前最重的石头连续进行两次。 b.计算碰撞结果 比较两块石头的重量。 c.处理剩余重量 如果碰撞后剩下重量即y - x且y x将该重量作为新石头放回堆中push操作。返回最终结果 循环结束后如果堆为空返回0。如果堆中剩下一块石头返回该石头的重量。4.2 完整C代码实现#include queue #include vector using namespace std; class Solution { public: int lastStoneWeight(vectorint stones) { // 1. 初始化大顶堆 priority_queueint max_heap; for (int weight : stones) { max_heap.push(weight); } // 2. 模拟碰撞过程 while (max_heap.size() 1) { // 取出最重的两块石头 int stone1 max_heap.top(); max_heap.pop(); int stone2 max_heap.top(); max_heap.pop(); // 碰撞并处理剩余部分 if (stone1 ! stone2) { // 注意stone1和stone2是从堆顶取出的但stone1不一定是较大的那个。 // 因为默认是大顶堆先取出的是最大值后取出的是次大值。 // 所以这里我们直接计算差值差值一定为正。 int newWeight stone1 - stone2; // 或者 abs(stone1 - stone2) max_heap.push(newWeight); } // 如果相等则两块石头都粉碎无需任何操作 } // 3. 返回最终结果 return max_heap.empty() ? 0 : max_heap.top(); } };4.3 代码逐行解析priority_queueint max_heap; 声明一个存储int类型的大顶堆。模板默认为lessint即最大元素在顶部。for (int weight : stones) { max_heap.push(weight); } 使用范围for循环将输入数组的所有元素依次插入堆中。每次push操作的时间复杂度为O(log n)。while (max_heap.size() 1) 循环继续的条件是至少还有两块石头可以碰撞。int stone1 max_heap.top(); max_heap.pop(); 这是获取并移除堆顶元素的标准操作。top()获取但不移除pop()移除但不返回。必须分两步。if (stone1 ! stone2) 判断两块石头重量是否相等。注意由于我们先取stone1再取stone2且堆顶是最大值因此stone1 stone2。所以stone1 - stone2一定非负无需使用abs。max_heap.push(newWeight); 将碰撞后剩余的新石头重量放回堆中。return max_heap.empty() ? 0 : max_heap.top(); 三目运算符简洁地处理了可能返回0或剩余石头重量的情况。5. 复杂度分析与性能观察理解算法效率是“知行合一”的重要部分。我们不仅要知道代码怎么写还要知道它为什么好。5.1 时间复杂度O(n log n)建堆操作 将n个元素依次插入空堆每次插入是O(log n)总成本约为O(n log n)。更精确的建堆方式可以从底向上heapify复杂度为O(n)但STL的priority_queue构造函数通常采用依次插入的方式。模拟碰撞过程 在最坏情况下每次碰撞都产生一个新石头即每次重量都不相等。那么总共需要进行(n-1)次碰撞每次碰撞涉及两次popO(log n)和一次pushO(log n)。因此循环内的操作总时间复杂度也是O(n log n)。主导项 O(n log n)是主导项因此算法总时间复杂度为O(n log n)。5.2 空间复杂度O(n)我们使用了一个priority_queue来存储所有石头在最坏情况下需要存储n个元素。因此空间复杂度为O(n)。5.3 与暴力排序法的对比一种直观的暴力解法是每次碰撞前都对当前石头数组进行排序例如使用sortO(n log n)然后取最大的两个。这样每次循环都需要O(n log n)的排序总时间复杂度将高达O(n^2 log n)在n较大时如力扣的测试用例极易超时。大顶堆的方案将“维护有序性”的成本从每次O(n log n)降到了每次O(log n)是质的飞跃。6. 功能测试与效果验证编写完代码后必须进行测试来验证其正确性。我们设计几个典型的测试用例。6.1 测试用例设计// 可以在本地main函数中测试也可以在力扣的自定义测试用例中验证 int main() { Solution sol; vectorint test1 {2,7,4,1,8,1}; // 经典示例 cout Test1 [2,7,4,1,8,1]: sol.lastStoneWeight(test1) endl; // 应输出 1 vectorint test2 {1}; // 单块石头 cout Test2 [1]: sol.lastStoneWeight(test2) endl; // 应输出 1 vectorint test3 {1, 1}; // 两块相同石头 cout Test3 [1,1]: sol.lastStoneWeight(test3) endl; // 应输出 0 vectorint test4 {10,10,10,10}; // 多块相同石头 cout Test4 [10,10,10,10]: sol.lastStoneWeight(test4) endl; // 应输出 0 vectorint test5 {9,3,2,10}; // 随机顺序 // 过程: (10,9)-剩1, 堆变为[3,2,1]; (3,2)-剩1,堆变为[1,1]; (1,1)-剩0。 cout Test5 [9,3,2,10]: sol.lastStoneWeight(test5) endl; // 应输出 0 return 0; }6.2 测试执行与结果判断编译运行 将Solution类和测试代码放在同一个文件中编译并运行。观察输出 程序应依次输出1,1,0,0,0。调试观察 对于复杂用例如test1可以在循环中打印堆的状态直观观察碰撞过程加深对算法流程的理解。while (max_heap.size() 1) { // ... 取出stone1, stone2 ... cout 碰撞: stone1 和 stone2; if (stone1 ! stone2) { max_heap.push(newWeight); cout , 放入新石头: newWeight; } cout endl; // 可以打印当前堆内容需要额外操作因为priority_queue不能直接遍历 }6.3 力扣提交验证将Solution类的代码复制到力扣题目编辑器中点击“执行代码”或“提交”。系统会运行多组隐藏的测试用例。如果所有用例都通过你会看到“通过”的提示并附有运行时间和内存消耗的统计。这是最终的验收标准。7. 常见问题与排查方法在实现和测试过程中你可能会遇到以下问题。问题现象可能原因排查方式解决方案编译错误‘priority_queue’ was not declared未包含必要的头文件。检查代码开头是否#include queue。添加#include queue。运行时错误或逻辑错误结果不对1. 错误理解了top()和pop()的顺序。2. 碰撞后处理逻辑有误比如把差值算反了。1. 使用小型测试用例如{2,2}单步调试。2. 在碰撞逻辑后打印stone1,stone2和newWeight。1. 牢记top()获取值pop()移除值两者需分开调用。2. 确认stone1是第一次pop出来的stone2是第二次pop出来的且stone1 stone2所以剩余重量是stone1 - stone2。时间超限 (TLE)使用了低效算法如每次碰撞前都排序。审查代码确认是否使用了priority_queue。必须使用堆优先队列来维护最大值确保时间复杂度为O(n log n)。内存消耗过大可能使用了额外的、不必要的容器来复制数据。检查是否除了priority_queue外还保留了原始的stones向量副本。算法只需要一个priority_queue输入向量stones可以直接遍历使用无需额外拷贝。对于{1}的输入返回0循环条件或最终返回逻辑有误。测试单元素输入跟踪代码流程。循环条件是while (max_heap.size() 1)对于单元素不会进入循环。最终返回前应判断堆是否为空。使用return max_heap.empty() ? 0 : max_heap.top();。8. 扩展思考与最佳实践解决一个问题后进行扩展思考是提升能力的关键。8.1 如果要求返回碰撞过程记录怎么办有时面试官会问如何记录每一次碰撞这需要我们在模拟过程中保存状态。可以定义一个结构体或使用pair来记录。vectorpairint, int collisionRecord; // 记录每次碰撞的两块石头重量 while (max_heap.size() 1) { int y max_heap.top(); max_heap.pop(); int x max_heap.top(); max_heap.pop(); collisionRecord.emplace_back(y, x); // 记录 if (y x) { max_heap.push(y - x); } } // 最终collisionRecord保存了所有碰撞历史8.2 如何用小顶堆解决这个问题虽然本题用大顶堆最直观但使用小顶堆通过传入greaterint比较器也可以解决只是逻辑上需要一点转换将所有石头重量的负值放入小顶堆。这样绝对值最大的负数即原最大的正数会在堆顶。priority_queueint, vectorint, greaterint min_heap; // 小顶堆 for (int w : stones) { min_heap.push(-w); // 存入负值 } while (min_heap.size() 1) { int stone1 -min_heap.top(); min_heap.pop(); // 取出并转回正值 int stone2 -min_heap.top(); min_heap.pop(); if (stone1 ! stone2) { min_heap.push(-(stone1 - stone2)); // 将差值的负值存回 } } return min_heap.empty() ? 0 : -min_heap.top();这种方法有助于理解堆的比较器本质。8.3 工程实践中的建议优先使用STL 在面试或实际项目中除非有特殊性能定制需求否则应优先使用std::priority_queue而非手写堆。它经过充分测试正确且高效。理解抽象而非死记 记住“动态求极值用堆”而不是死记1046题的代码。遇到新问题时先抽象出核心操作再匹配数据结构。复杂度分析是必备技能 写完代码后养成分析时间、空间复杂度的习惯并能向他人清晰解释。测试驱动 先写几个简单的测试用例边界条件、特殊情况再实现代码最后用更复杂的用例验证。9. 总结力扣1046题“最后一块石头的重量”是一个绝佳的数据结构教学案例。它表面上是一个简单的模拟题但深层次考察的是你是否能为“频繁获取最大值并更新集合”这一核心操作选择最高效的数据结构——大顶堆。通过本文的拆解我们不仅得到了一个简洁的Cpriority_queue解法更完成了一次“知行合一”的实践知 理解大顶堆的特性快速取最值、插入删除O(log n)。行 应用std::priority_queue解决具体问题完成从问题分析、代码实现、测试验证到复杂度分析的完整闭环。掌握这种从问题特征到数据结构选择的思维模式远比背下十道题的答案更有价值。当下次遇到“数据流的中位数”、“任务调度器”、“合并K个排序链表”等问题时你会自然而然地想到“这里是不是该用堆了”建议你将此题的代码和思路作为模板收藏并尝试用同样的思维去解决力扣第215题“数组中的第K个最大元素”堆的另一个经典应用巩固这一重要的数据结构实战能力。