LeetCode 经典算法题解析:优先队列与广度优先搜索的巧妙应用

引言

算法题是检验程序员基础数据结构和算法掌握程度的重要途径。本文将深入分析 LeetCode 的两道经典题目:1046. 最后一块石头的重量752. 打开转盘锁 。这两道题分别展示了优先队列和**广度优先搜索(BFS)**这两种核心算法思想在实际问题中的精妙应用。

第一题:1046. 最后一块石头的重量

题目解析

这是一个模拟类问题。每次需要选择重量最大的两块石头进行碰撞,直到只剩一块或没有石头为止。核心在于每次都要找到当前重量最大的两块石头

JavaScript 解法分析

javascript 复制代码
/**
 * @param {number[]} stones - 石头重量数组
 * @return {number} - 最后剩余石头的重量
 */
var lastStoneWeight = function(stones) {
    // 循环直到只剩0或1块石头
    while(stones.length > 1) {
        // 每次排序,使最大元素在前面
        stones.sort((a, b) => b - a);
        
        // 取出最大的两块石头
        let a = stones.shift(); // 最大
        let b = stones.shift(); // 第二大
        
        // 如果重量不同,产生新的石头
        if (a !== b) {
            stones.push(a - b);
        }
        // 如果重量相同,两块都被摧毁,不产生新石头
    }
    
    // 返回最后的石头重量,如果没石头了返回0
    return stones[0] || 0;
};

算法复杂度:

  • 时间复杂度:O(n² log n) - 每次循环都要排序
  • 空间复杂度:O(1) - 原地操作

C++ 解法分析(优化版)

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

class Solution {
public:
    int lastStoneWeight(vector<int>& stones) {
        // 使用优先队列(最大堆),自动维护最大元素在顶部
        priority_queue<int> pq;
        
        // 将所有石头放入优先队列
        for(int x : stones) {
            pq.push(x);
        }
        
        // 当还有至少两块石头时继续
        while(pq.size() > 1) {
            // 取出两块最大石头
            int a = pq.top(); pq.pop(); // 最大
            int b = pq.top(); pq.pop(); // 第二大
            
            // 碰撞后的新石头重量
            int result = abs(a - b);
            
            // 将新石头放回队列
            pq.push(result);
        }
        
        // 返回最后的石头重量
        return pq.empty() ? 0 : pq.top();
    }
};

算法复杂度:

  • 时间复杂度:O(n log n) - 每次插入删除操作都是 O(log n)
  • 空间复杂度:O(n) - 优先队列存储

核心思想: 优先队列自动维护元素顺序,避免了每次手动排序的开销。

第二题:752. 打开转盘锁

题目解析

这是一个最短路径问题。从初始状态 "0000" 出发,每次可以改变一个数字的一位(向上或向下),目标是到达 target。中间不能经过 deadends 中的状态。这是典型的无权图最短路径问题。

JavaScript 解法分析

ini 复制代码
/**
 * 打开转盘锁
 * @param {string[]} deadends - 死亡数字列表
 * @param {string} target - 目标数字
 * @return {number} - 最小旋转次数
 */
function openLock(deadends, target) {
    // 特殊情况:目标就是起点
    if (target === "0000") return 0;
    // 特殊情况:起点就是死亡数字
    if (deadends.includes("0000")) return -1;
    
    // 将死亡数字转换为 Set 以提高查找效率
    const deadSet = new Set(deadends);
    
    // BFS 队列和访问标记集合(合并了死亡数字和已访问数字)
    const queue = ["0000"];
    const forbidden = new Set(deadends);
    forbidden.add("0000"); // 起点加入禁止访问集合
    
    let steps = 0;
    
    // BFS 主循环
    while (queue.length > 0) {
        const size = queue.length; // 当前层的节点数量
        steps++; // 步数增加
        
        // 处理当前层的所有节点
        for (let i = 0; i < size; i++) {
            const current = queue.shift();
            
            // 尝试每一位数字的两种转动方式
            for (let j = 0; j < 4; j++) {
                // 向上转动
                const nextUp = turnUp(current, j);
                if (nextUp === target) return steps; // 找到目标
                
                // 如果未访问且不是死亡数字,则加入队列
                if (!forbidden.has(nextUp)) {
                    forbidden.add(nextUp);
                    queue.push(nextUp);
                }
                
                // 向下转动
                const nextDown = turnDown(current, j);
                if (nextDown === target) return steps; // 找到目标
                
                if (!forbidden.has(nextDown)) {
                    forbidden.add(nextDown);
                    queue.push(nextDown);
                }
            }
        }
    }
    
    return -1; // 无法到达目标
}

/**
 * 向上转动一位数字(0->1, 9->0)
 * @param {string} str - 当前数字字符串
 * @param {number} index - 要转动的位置
 * @return {string} - 转动后的字符串
 */
function turnUp(str, index) {
    const chars = str.split('');
    const digit = parseInt(chars[index]);
    chars[index] = (digit + 1) % 10;
    return chars.join('');
}

/**
 * 向下转动一位数字(0->9, 1->0)
 * @param {string} str - 当前数字字符串
 * @param {number} index - 要转动的位置
 * @return {string} - 转动后的字符串
 */
function turnDown(str, index) {
    const chars = str.split('');
    const digit = parseInt(chars[index]);
    chars[index] = (digit + 9) % 10; // 等同于 (digit - 1 + 10) % 10
    return chars.join('');
}

C++ 解法分析

arduino 复制代码
#include <vector>
#include <string>
#include <unordered_set>
#include <queue>
using namespace std;

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        if(target == "0000") return 0;
        
        // 创建死亡数字集合
        unordered_set<string> deadset(deadends.begin(), deadends.end());
        if(deadset.count("0000")) return -1;
        
        // BFS
        unordered_set<string> visited;
        for(const string &dead : deadends) {
            visited.insert(dead);
        }
        
        int steps = 0;
        queue<string> bq;
        bq.push("0000");
        visited.insert("0000");
        
        while(!bq.empty()) {
            int number = bq.size(); // 当前层节点数
            steps++; // 步数增加
            
            while(number--) { // 处理当前层所有节点
                string result = bq.front();
                bq.pop();
              
                // 尝试每一位数字的两种转动方式
                for(int i = 0; i < 4; i++) {
                    string s = turnup(result, i); // 向上转动
                    if(s == target) return steps; // 找到目标
                    
                    // 如果未访问,则加入队列
                    if(visited.find(s) == visited.end()) {
                        visited.insert(s);
                        bq.push(s);
                    }
                    
                    string y = turndown(result, i); // 向下转动
                    if(y == target) return steps; // 找到目标
                    
                    if(visited.find(y) == visited.end()) {
                        visited.insert(y);
                        bq.push(y);
                    }
                }
            }
        }
        
        return -1; // 无法到达
    }
    
private:
    // 向上转动
    string turnup(const string& s, int index) {
        string result = s;
        if(result[index] == '9') {
            result[index] = '0';
        } else {
            result[index]++;
        }
        return result;
    }
    
    // 向下转动
    string turndown(const string& s, int index) {
        string result = s;
        if(result[index] == '0') {
            result[index] = '9';
        } else {
            result[index]--;
        }
        return result;
    }
};

算法复杂度:

  • 时间复杂度:O(10⁴ × 8) - 最多 10000 个状态,每个状态最多 8 个邻居
  • 空间复杂度:O(10⁴) - 存储访问状态和队列

算法思想对比

特征 1046题(优先队列) 752题(BFS)
核心数据结构 优先队列(堆) 队列 + 哈希集合
应用场景 需要快速获取最大/最小元素 最短路径/层次遍历
时间复杂度优化 从 O(n² log n) 优化到 O(n log n) 保证找到最短路径
空间复杂度 通常较低 需要存储访问状态

总结与启示

  1. 选择合适的数据结构:对于需要频繁获取最值的问题,优先队列是理想选择;对于最短路径问题,BFS 是标准解法。
  2. 算法优化的重要性:JavaScript 版本的排序方法时间复杂度较高,而 C++ 的优先队列版本显著提升了效率。
  3. BFS 的层次性质:通过逐层扩展,BFS 天然保证了第一次到达目标时路径最短。
  4. 实际应用价值:这类算法在游戏 AI、路径规划、状态搜索等领域有着广泛的应用。

通过对这两道题的深入分析,我们可以看到基础算法在解决实际问题中的强大力量,也体现了掌握核心数据结构和算法的重要性。

相关推荐
Gorway1 小时前
解析残差网络 (ResNet)
算法
Wect1 小时前
LeetCode 207. 课程表:两种解法(BFS+DFS)详细解析
前端·算法·typescript
灵感__idea15 小时前
Hello 算法:众里寻她千“百度”
前端·javascript·算法
Wect1 天前
LeetCode 130. 被围绕的区域:两种解法详解(BFS/DFS)
前端·算法·typescript
NAGNIP2 天前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
颜酱2 天前
单调栈:从模板到实战
javascript·后端·算法
CoovallyAIHub2 天前
仿生学突破:SILD模型如何让无人机在电力线迷宫中发现“隐形威胁”
深度学习·算法·计算机视觉
CoovallyAIHub2 天前
从春晚机器人到零样本革命:YOLO26-Pose姿态估计实战指南
深度学习·算法·计算机视觉
CoovallyAIHub2 天前
Le-DETR:省80%预训练数据,这个实时检测Transformer刷新SOTA|Georgia Tech & 北交大
深度学习·算法·计算机视觉