距离上次写博客已有很长的时间我也已经近一年没学算法了。最近期末考突然捡起算法与数据结构以往的痛苦又开始折磨我使我意识到算法可视化的迫切需求。于是我建了一个这样的项目——一个能让我看见算法执行过程的平台。学算法的新手大概都有这种感觉看书上的代码脑子里推演着循环和递归脑海里理解的很模糊实际实现一堆错。指针怎么跳的、数组怎么换的、队列怎么进的——这些过程光靠想象总归是差了点什么。所以我就想能不能做一个东西让算法真的能被看见而且不是那种柱状图往上冒一下的看见是把算法映射到真实世界场景里——比如Dijkstra算法在城市街道上跑排序算法在工厂流水线上走并查集在一片社区里建连接。于是就有了这个项目。项目已经开源在GitHub上地址见文末完整代码、全部场景、所有算法模板都在里面。项目概览先放一张整体的效果整个平台基于React Three.js TypeScript构建支持JavaScript和Python双语言执行。核心功能一句话说就是左侧跑朴素算法右侧跑优化算法两边的每一步都在3D场景里呈现出来你可以逐帧播放对比。例如左边是冒泡排序在笨拙地来回搬运右边是快速排序在利落分治。这就是我想写的东西算法的执行不只是时间复杂度的改进更是一种过程化的升维。技术栈项目依赖的核心库{react:^18.3.1,three:^0.169.0,react-three/fiber:^8.17.0,react-three/drei:^9.114.0,zustand:^5.0.0,codemirror:^6.0.1}React 18Three.js(通过 react-three/fiber) 构建3D场景Zustand管理播放状态步骤进退、速度控制CodeMirror 6作为代码编辑器支持JS/Python语法高亮Web Worker沙箱执行用户代码不阻塞UITailwind CSS做暗色主题UI架构设计整个项目分了几个核心层1. 执行引擎层用户点击运行后代码会被送到Web Worker里执行。执行过程中代码里的trace()调用会把每一步的快照发回主线程// src/core/types.ts — 核心类型定义exportinterfaceTraceSnapshot{label:string;data:Recordstring,unknown;highlights?:number[];pointers?:Recordstring,number;description?:string;timestamp:number;}exportinterfaceAlgorithmTemplate{id:string;name:string;category:string;language:js|python;naiveCode:string;// 朴素实现optimizedCode:string;// 优化实现defaultData:unknown;}执行引擎通过Web Worker做沙箱隔离// src/core/executor.ts — 沙箱执行exportfunctionexecuteCode(code:string,data:unknown,timeoutMs:number5000):Promise{success:boolean;traces:TraceSnapshot[];error?:string}{returnnewPromise((resolve,reject){constwgetWorker();constrequestIdreq_${requestIdCounter};// ... 通过 postMessage 通信});}这套架构支持JavaScript原生沙箱执行和Python通过Pyodide在Worker中运行未来要扩展C语言也不难加个executor就行。2. 场景映射层每个算法类别都对应一个或多个3D场景。比如图算法映射到城市场景街道边路口节点排序算法映射到工厂场景传送带数组。系统会自动检测当前运行的数据结构类型切换到对应的场景。场景分为两类抽象场景柱状图、节点连线图——清晰展示算法逻辑工程场景城市、工厂、森林、图书馆——沉浸式体验这也是这个项目最耗时的部分——光城市场景就写了5000多行代码。算法模板一览目前内置了六大类40多种算法的朴素vs优化对比模板每种都有JavaScript和Python双版本排序类朴素优化改进点冒泡排序 O(n²)快速排序(三数取中) O(n log n)分治尾递归选择排序 O(n²)堆排序 O(n log n)二叉堆插入排序 O(n²)希尔排序步长分组归并排序(递归)归并排序(迭代)栈溢出规避计数排序基数排序(LSD)多关键字图算法朴素优化改进点Dijkstra 朴素 O(V²)Dijkstra 优先队列 O(E log V)最小堆Prim 朴素 O(V²)Prim 堆优化 O(E log V)最小堆QuickFind路径压缩按秩合并摊还α(n)字符串朴素优化朴素匹配 O(nm)KMP O(nm)最长回文暴力 O(n³)中心扩展 O(n²)异位词排序异位词哈希无重复子串暴力 O(n²)滑动窗口 O(n)还有动态规划0-1背包、LCS、编辑距离、Coin Change等、树算法BFS/DFS、验证BST、LCA、迭代遍历、搜索算法二分搜索、Two Sum哈希、旋转数组二分——总共约40种对比模板。关键算法代码贴几个算法实现。1. KMP字符串匹配KMP的核心是那张LPS表最长公共前后缀。理解了LPS怎么构建就理解了KMP。// 朴素 → KMP 的优化版本functionkmpSearch(input){consttexttypeofinputobjectinput.text?input.text:input;constpatterntypeofinputobjectinput.pattern?input.pattern:;constntext.length,mpattern.length;// 构建 LPS 表constlpsnewArray(m).fill(0);letlen0,i1;while(im){if(pattern[i]pattern[len]){len;lps[i]len;i;}else{if(len!0)lenlps[len-1];else{lps[i]0;i;}}}trace(lps,{data:{array:lps},description:LPS表: [${lps}]});// KMP 搜索letj0;i0;while(in){if(pattern[j]text[i]){i;j;}if(jm){trace(found,{data:{text,pattern},highlights:[i-j],description:在位置${i-j}找到匹配!});jlps[j-1];}elseif(inpattern[j]!text[i]){if(j!0){jlps[j-1];// 利用LPS跳过已匹配部分trace(skip,{data:{text,pattern},highlights:[i],description:失配, j-${j}});}else{i;}}}}LPS表的构建是整个KMP最巧妙的部分——它利用前缀的自相似性避免了朴素匹配中匹配到一半发现不对从头再来的悲剧。2. Dijkstra 优先队列朴素Dijkstra每次都要线性扫描找最小dist节点用堆优化后效率翻倍// 优先队列优化的 DijkstrafunctiondijkstraPQ(graph,start){const{nodes,edges}graph;constnnodes.length;constdistnewArray(n).fill(Infinity);dist[start]0;constadjArray.from({length:n},()[]);for(consteofedges)adj[e.from].push({to:e.to,w:e.weight});// 用数组模拟最小堆实际生产环境用二叉堆constpq[[0,start]];while(pq.length0){pq.sort((a,b)a[0]-b[0]);// 按距离排序const[d,u]pq.shift();if(ddist[u])continue;// 惰性删除trace(visit,{data:{...graph},highlights:[u],description:弹出${nodes[u].label}, dist${d}});for(const{to,w}ofadj[u]){if(dist[u]wdist[to]){dist[to]dist[u]w;pq.push([dist[to],to]);trace(relax,{data:{...graph},highlights:[u,to],description:更新${nodes[to].label}-${dist[to]}});}}}}在3D场景中Dijkstra算法的执行过程是这样的一辆货车从起点出发每到一个路口节点所有相邻路口的路标会更新距离。货车总是沿着当前已知最短的路走——这就是贪心的具象化。3. 并查集 路径压缩从QuickFind到路径压缩代码量没增加多少性能却从O(n)降到了摊还α(n)// 路径压缩 按秩合并functionufPC(ops){constn8;constparentArray.from({length:n},(_,i)i);constranknewArray(n).fill(0);functionfind(x){if(parent[x]!x){parent[x]find(parent[x]);// 路径压缩trace(compress,{data:{array:parent},highlights:[x],description:路径压缩:${x}-${parent[x]}});}returnparent[x];}functionunion(x,y){letrxfind(x),ryfind(y);if(rxry)return;if(rank[rx]rank[ry])[rx,ry][ry,rx];parent[ry]rx;if(rank[rx]rank[ry])rank[rx];trace(union,{data:{array:parent},highlights:[x,y],description:合并${x}和${y}});}for(const[a,b]ofops)union(a,b);}在城市场景中并查集的每个集合对应一个社区。两个社区合并时会有一座桥或一条路把两个区域连接起来。路径压缩的过程看起来像是直接拉了一条捷径直通社区中心——非常直观。4. 快速排序 — 三数取中优化版的快排用三数取中避免选到最差pivotfunctionquickSort(arr,low,high){if(lowhigh)returnarr;// 三数取中arr[low]、arr[mid]、arr[high] 的中位数做 pivotconstmidlow((high-low)1);if(arr[mid]arr[low])[arr[low],arr[mid]][arr[mid],arr[low]];if(arr[high]arr[low])[arr[low],arr[high]][arr[high],arr[low]];if(arr[high]arr[mid])[arr[mid],arr[high]][arr[high],arr[mid]];constpivotarr[high];letilow-1;for(letjlow;jhigh;j){if(arr[j]pivot){i;[arr[i],arr[j]][arr[j],arr[i]];}}[arr[i1],arr[high]][arr[high],arr[i1]];// 尾递归优化优先处理较短的子数组quickSort(arr,low,i);// 左半部分quickSort(arr,i2,high);// 右半部分}在工厂场景中快排的pivot就像传送带上的分拣标准——所有小于pivot的往左送大于pivot的往右送然后左右两条传送带各自递归。冒泡排序则是两个工人来来回回地对比交换——你一眼就能看出哪个效率高。3D城市场景这部分是我花时间最多的。整个城市由以下几个部分组成街道网格构成了图的边建筑物/路口构成了图的节点Dijkstra园区有专门的公园路径算法运行时长椅上会出现路径标记Prim变电站电网拓扑结构展示最小生成树的构建并查集社区一片住宅区合并时破墙通路排序分拣中心传送带和货架构成数组森林苗圃树结构可视化每个节点是一棵树图书馆字符串匹配的场景书架上的书就是字符每个场景都配了完整的相机控制——可以用鼠标旋转、缩放、平移。算法运行时相机会自动对准当前操作的元素。底部工具栏运行算法后底部的控制面板支持▶️逐帧播放一步步看算法的每一次比较和交换⏸️自动播放连续播放速度可调0.5x ~ 4x⏪回退回到上一步统计面板显示朴素vs优化的步骤数、执行时间、加速比以及算法改进必备的时间执行耗时对比开发感受这个项目从一开始的我就试试Three.js能不能做算法可视化一步步变成了一个完整的平台。中间经历了无数次场景太卡 → 优化Draw CallsPython执行器跑不起来 → 换Pyodide城市太空旷 → 加建筑、路灯、河流、桥截图太丑 → 重写材质和光照我每次启动项目看3D动画时都会感到震惊并不是我写的算法有多好而是意识到算法真正的的作用——并不是单纯为了刷题而是应用于实际。你可以任意修改算法然后现实就会为你改变。我想写的从来不是单纯的炫酷场景而是让算法与现实碰撞出火花。不管是在城市地铁还是警察抓小偷亦或者是叉车装货又或者是电网连接等等等等我想要构建的是一个思维改变世界的项目。当然不得不承认我的城市建模还是有点粗糙因此我开源出来希望大家能共同努力。项目地址GitHubhttps://github.com/gu860/dsa-improvement-range最后算法学习没有捷径但有更好的方式。光看书上的O(n²)和O(n log n)你可能知道快排比冒泡快但你不会真正感受到这种差距。当你在3D场景里看到冒泡排序的元素像蜗牛一样一格一格挪动而快速排序像流水线一样干脆利落地分治时——那种体验是完全不一样的。