前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库Dijkstra 复杂度 堆优化
算法算法图算法

Dijkstra 用二叉堆和朴素数组实现的复杂度分别是多少,各自适合什么图?

Dijkstra 用二叉堆优化后复杂度为 O((V+E)logV),适合稀疏图;朴素数组实现为 O(V²),适合稠密图。

前端进阶之旅 · 一题精讲更新于 2026.09.05
算法#图算法
先看核心答案读代码示例
理解线索

两种实现的核心差异

  1. 朴素数组选最小线性扫描 O(V) 找最小距离
  2. 二叉堆选最小堆顶 O(logV) 弹出最小距离
  3. 松弛操作朴素 O(1) 更新,堆需 O(logV)

当 V 约为 10^4 且 E 接近 V² 时,O(V²) 实际耗时可能更短。

核心回答

先记住这个答案

朴素实现每轮扫描所有未标记顶点找最小值,共 V 轮,每轮 O(V),加松弛 O(E),总计 O(V²+E)。二叉堆实现则用优先队列维护未确定顶点,每次弹出最小值 O(logV),每条边可能被松弛一次并推入堆,总计 O((V+E)logV)。因此稠密图(E≈V²)用朴素数组更快,稀疏图用堆更优。

  • 朴素数组实现 O(V²),稠密图更优
  • 二叉堆实现 O((E+V)logV),稀疏图更优
  • 当 V 大且 E 接近 V² 时选朴素数组

复杂度从何而来

朴素实现用数组 d[] 存储当前距离,每轮线性扫描找出未标记顶点中 d 最小者,故每轮开销 O(V),共 V 轮,总 O(V²)。每条边松弛一次 O(1),总 O(V²+E)。该实现不利用距离分布,即使 E 很小也要做满 V² 扫描。

二叉堆实现将候选顶点按距离存入最小堆,堆操作 O(logV)。若不使用 decrease-key,每个顶点可能因距离更新而多次入堆,堆中元素最多 O(E)。每次弹出和压入各 O(logV)。松弛总数 E,总时间复杂度 O((V+E)logV)。注意实际实现常使用 decrease-key 或直接插入,直接插入后堆元素可达 E。

朴素数组与二叉堆 Dijkstra 的 JavaScript 实现及复杂度演示JavaScript
// 朴素数组实现 O(V^2)
function dijkstraArray(adj, V, s) {
    let dist = Array(V).fill(Infinity);
    let used = Array(V).fill(false);
    dist[s] = 0;
    for (let i = 0; i < V; i++) {
        let v = -1;
        for (let j = 0; j < V; j++) {
            if (!used[j] && (v === -1 || dist[j] < dist[v])) v = j;
        }
        if (dist[v] === Infinity) break;
        used[v] = true;
        for (let [to, w] of adj[v]) {
            if (dist[v] + w < dist[to]) dist[to] = dist[v] + w;
        }
    }
    return dist;
}

// 二叉堆实现 O((V+E)logV)
function dijkstraHeap(adj, V, s) {
    class MinHeap {
        constructor() { this.heap = []; }
        push(x) { this.heap.push(x); this._up(this.heap.length-1); }
        pop() { let top = this.heap[0]; let last = this.heap.pop(); if (this.heap.length > 0) { this.heap[0] = last; this._down(0); } return top; }
        _up(i) { while (i > 0) { let p = (i-1)>>1; if (this.heap[p][1] <= this.heap[i][1]) break; [this.heap[p], this.heap[i]] = [this.heap[i], this.heap[p]]; i = p; } }
        _down(i) { let n = this.heap.length; while (true) { let l = i*2+1, r = i*2+2, m = i; if (l < n && this.heap[l][1] < this.heap[m][1]) m = l; if (r < n && this.heap[r][1] < this.heap[m][1]) m = r; if (m === i) break; [this.heap[i], this.heap[m]] = [this.heap[m], this.heap[i]]; i = m; } }
    }
    let dist = Array(V).fill(Infinity);
    let heap = new MinHeap();
    dist[s] = 0;
    heap.push([s, 0]);
    while (heap.heap.length) {
        let [v, d] = heap.pop();
        if (d > dist[v]) continue;
        for (let [to, w] of adj[v]) {
            if (dist[v] + w < dist[to]) {
                dist[to] = dist[v] + w;
                heap.push([to, dist[to]]);
            }
        }
    }
    return dist;
}

// 测试:无向图顶点 0-2
let adj = [
    [[1, 1], [2, 4]],
    [[0, 1], [2, 1]],
    [[0, 4], [1, 1]]
];
console.log('朴素:', JSON.stringify(dijkstraArray(adj, 3, 0)));
console.log('堆:', JSON.stringify(dijkstraHeap(adj, 3, 0)));
// 稀疏图:顶点 5 边 5
let adj2 = [
    [[1, 2]],
    [[2, 3]],
    [[3, 1]],
    [[4, 5]],
    []
];
console.log('稀疏朴素:', JSON.stringify(dijkstraArray(adj2, 5, 0)));
console.log('稀疏堆:', JSON.stringify(dijkstraHeap(adj2, 5, 0)));
查看输出与解释
朴素: [0,1,2]
堆: [0,1,2]
稀疏朴素: [0,2,5,6,11]
稀疏堆: [0,2,5,6,11]

JavaScript 实现展示两种精度的 Dijkstra。数组实现每次 O(V) 搜索,堆实现用最小堆保证每次取出当前最小距离。测试了完全图和稀疏图,结果一致。

社交网络好友推荐的最短路建模

假设有 V=10^5 个用户,每个用户平均关注 20 人,总边 E≈2×10^6。要计算某个用户到其他所有人的最短路径长度,这是稀疏图。此时朴素数组每轮 O(V) 扫描需 10^10 次操作,完全不可行;堆实现 O((V+E)logV)≈(2.1×10^6)×17≈3.6×10^7 操作,毫秒级完成。

如果换成稠密图,例如航线网络中 V=10^3 城市两两之间有直飞,E≈5×10^5,则朴素 O(V²)=10^6 比堆 O((V+E)logV)≈(5×10^5)×10≈5×10^6 更优。实际工程中需要根据边数选择实现。

复杂度陷阱与前提

二叉堆实现的 O((V+E)logV) 是在没有负权边的前提下的。若允许边权更新频繁导致堆中冗余元素,堆大小可能达到 O(E),但 E 本身小于 V²,理论复杂度不变。不过当 E 接近 V² 时,log V 系数使得常数增大,可能不如 O(V²)。

当图特别稀疏(E=O(V))时,堆实现退化为 O(V log V),而朴素数组仍是 O(V²)。但若 V 很小(如 V≤1000),O(V²) 和 O(V log V) 差距不大,此时实现简单即可。注意堆实现每次松弛推送新距离,实际堆操作次数为 E 次,内存占用 O(E)。

回答前,多想一步

容易答错的地方

认为堆优化总是更快
实际上当图接近完全图时,O(V²) 的朴素实现比 O((V+E)logV) 更快,因为后者堆操作 logV 因子在 E≈V² 时膨胀为 V² logV,远大于 V²。要依据边密度选择。
混淆二叉堆与斐波那契堆复杂度
有些资料称 Dijkstra 最优为 O(E+V logV),那是斐波那契堆的理论复杂度。二叉堆版本是 O((V+E)logV),两者不同。本文只讨论二叉堆和朴素数组。
试着用自己的话回答

面试官还会怎么问?

用 `priority_queue` 时顶点可能重复入队,会影响复杂度吗?

不会,重复入队总数不超过 E 次,堆大小 O(E),每次操作 O(logE)=O(logV),总复杂度仍为 O((V+E)logV),但常数较大。可加跳过过期元素的时间检查。

图非常稀疏且 V 巨大时,还能无限优化吗?

可以改用斐波那契堆达到 O(E+V logV),但实现复杂。若度数接近 1 且 V 达 10^7,O(V logV) 也可能吃紧。若边权仅为 0 或 1,可使用双端队列 BFS(0-1 BFS)将复杂度降至 O(V+E);若边权为非负整数,也可用 Dial 桶优化;但一般非负实权时没有特别简单的优化。

为什么堆实现中松弛操作需要 O(logV),而朴素数组是 O(1)?

堆需要维持候选集有序以快速弹最小值,插入或更新后要上浮/下沉。朴素数组更新距离直接改 d[to],但查找最小值时得线性扫描。两种操作权衡不同。

从一道题,走向一组知识

把知识连起来

图算法

为什么 BFS 能在无权图中求出单源最短路径?

同属「图算法」专题,接着看 BFS 无权图 最短路 在具体场景中的处理方式。

字符串算法

为什么 KMP 字符串匹配的整体时间复杂度是 O(n+m),文本指针为什么从不回退?

继续了解 KMP 时间复杂度 文本指针不回退,补充本题涉及的 算法 相关知识。

参考资料

  • Dijkstra Algorithm¶

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 复杂度从何而来
  3. 社交网络好友推荐的最短路建模
  4. 复杂度陷阱与前提
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑