
拆解 Convert to it 的 PriorityQueueA* 转换路径搜索为什么离不开手写最小堆【免费下载链接】convertTruly universal online file converter项目地址: https://gitcode.com/GitHub_Trending/convert7/convert【场景切入】为什么 A* 转换路径搜索需要手写 PriorityQueue把 PNG 转成 MP3没有任何 handler 能一步到位——中间必须经 WAV 或 MP4 这类格式接力。找到代价最小的接力链是 Dijkstra 族算法的活而算法每一步都要从候选集合里弹出当前总代价最小的节点候选是十万级时每次 poll 线性扫一遍就是 O(n²)浏览器主线程直接卡死。Convert to it 的解法是在 src/PriorityQueue.ts 手写一个约 200 行的二叉最小堆数组堆用下标算术代替树节点指针把 poll 压到 O(log n)。这个类本身不认识任何文件格式它只回答一个问题给我一堆东西谁能最快拿到代价最低的那个——而什么算低由外部比较函数注入于是它既能服务正向的 A* 搜索也能服务反向的代价预计算。下面这段是 A* 主搜索建队列的入口说明最小代价优先出队如何被注入堆里// src/TraversionGraph.ts#L491-L495 const queue new PriorityQueueQueueNode( 1000, (a, b) a.cost remainingCosts[a.index] - (b.cost remainingCosts[b.index]), ); queue.add({ index: fromIndex, cost: 0, path: [from] });【设计决策拆解】堆的三处取舍挖洞、最小堆方向、分段扩容挖洞上移还是交换上浮siftup 的写操作省在哪add不在插入点原地反复交换而是先沿父链挖洞最后把新元素一次性落位——这段在 src/PriorityQueue.tswhile (k 0) { const parent (k - 1) 1; const e this._queue[parent] as T; if (this._comparator!(item, e) 0) break; this._queue[k] e; // 父节点下移洞继续上移 k parent; } this._queue[k] item; // 洞填上对比常见的交换式上浮每一步 3 次内存写两次交换挖洞式每跨一层只写 1 次。在 A* 里每次 poll 之后往往立刻批量 add 出边候选写操作高频发生这个常数差会真实累积。代价是语义不直观函数执行期间洞的位置上读到的值是中间态调试时把中途状态当最终状态读就会错。还有个容易忽略的细节——父节点用(k - 1) 1无符号右移而不是除法天然取整且保证非负左孩子则是(k 1) 1这套位运算只在非负下标成立是隐性约束。比较器方向与 heap 序谁先出队真的反直觉堆维护的是最小堆比较结果 0就停止上浮即新元素不小于父节点才停所以先出队的永远是代价最小的元素。而 A* 注入的比较器是a.cost - b.cost优先级高等于总代价低——读到优先级队列四个字时很容易以为是数字大的先出方向正好相反。sink 侧src/PriorityQueue.ts对称half _size 1划出还有孩子的范围每步先取较小孩子与待沉元素比 0即停。sink里this._comparator!(object, right) 0表示左孩子更大、改选右孩子堆序始终指向较小的一侧。容量翻倍还是加半64 这个分界线从哪来grow采用分段策略src/PriorityQueue.tsconst newCapacity oldCapacity (oldCapacity 64 ? oldCapacity 2 : oldCapacity 1); // 小于 64 翻倍否则加半小于 64 翻倍避免小堆阶段反复重分配64 及以上只加 50%因为搜索主队列初始容量就是 1000、最坏可逼近MAX_QUEUED_PATHS 15_000_000src/TraversionGraph.ts到那个量级再翻倍要预分配数千万对象槽。保险措施是新容量不是安全整数时直接抛RangeError(OOM: ...)。这里有个源码级细节基数取的是this._size而不是this._queue.length——正常路径下两者相等grow只在_size length时触发但clear()会把_size归零并保留容量若复用同一实例再次撑大扩容基数就错了。当前调用方都是一次性建队、用完即弃这条边界没人踩到属于未引爆的隐患。【实战调用链】一次 searchPath 的完整执行流反向预计算先建一个堆对目标节点沿入边反向松弛src/TraversionGraph.ts得到每个格式到目标的剩余代价remainingCosts当作 A* 的启发式建主队列元素是{index, cost, path}比较函数使堆顶永远是 f 值实际代价 剩余代价最小的候选src/TraversionGraph.ts主循环src/TraversionGraph.ts每次poll()若已是目标格式就yield整条 handler 链路给 UI否则沿全部出边生成新候选代价 当前代价 边代价 自适应惩罚如图片→音频经视频罚 10000再add回堆。与项目的耦合点搜索经searchPathProxiedsrc/TraversionGraph.ts被 comlink 代理进 workersearchPath每 1000 次迭代await isCancelled?.()让 UI 线程能在找到路径之前取消搜索代价记账由costFunction负责基础步 1、有损 ×1.4、handler 注册序加成等src/TraversionGraph.ts。如果这里出错用户侧的现象很具体队列积压超 1500 万条时看到 Conversion path search limit exceeded. Try a different output format.反向 Dijkstra 算不出有限启发式时searchPath静默空返回界面上就是找不到路径比较器若与代价不一致A* 的最优性不成立表现为拿到一条能跑但绕远路明显有损的转换链。【权衡与开放问题】重写 PriorityQueue 时会动哪几刀没有 decreaseKey同一节点反复入队靠calculateRemainingCosts里 poll 后的 stale checksrc/TraversionGraph.ts兜底堆里实际存着大量过期副本队列元素携带整条path数组每次入队concat复制一份15M 上限下内存压力不小存 parent 指针、出队时回溯重建能省大量内存不传比较器时默认对toString()做localeComparesrc/PriorityQueue.ts——对排对象来说这是个魔法行为更合理的默认是构造时强制要求比较器grow基数用_size而非_queue.length配合clear()复用实例会出边界问题toArray直接返回内部数组引用尾部空洞原样暴露现有调用链没人用它。想继续挖可以看反向 Dijkstra 的完整实现src/TraversionGraph.ts以及自适应代价如何参与每次入队src/TraversionGraph.ts。【免费下载链接】convertTruly universal online file converter项目地址: https://gitcode.com/GitHub_Trending/convert7/convert创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考