Sorting Algorithm Visualization: How to Create Mesmerizing Algorithm Animations With Code
There is something hypnotic about watching a sorting algorithm work. An array of random values — visualized as bars of varying heights — gradually transforms into a perfect staircase. Each algorithm has its own personality: bubble sort patiently bubbles large values to the right, quicksort aggressively partitions around pivots, merge sort methodically divides and conquers, and heap sort builds an invisible tree structure before extracting elements one by one.
This article builds eight sorting algorithm visualizations from scratch using JavaScript and Canvas. Every example is a self-contained HTML file — no libraries, no dependencies. Each visualization shows comparisons in yellow, swaps in red, and sorted elements in green. You can see (and hear, if you enable sound) exactly what each algorithm does at every step.
How the Visualizations Work
Every example follows the same architecture: an array of values is rendered as vertical bars on a Canvas. The sorting algorithm is written as an async generator — each yield pauses execution and triggers a visual frame. This approach means we do not need timers or callbacks; the algorithm code reads naturally while producing smooth animation.
Color coding:
- White — unsorted element
- Yellow — currently being compared
- Red — being swapped
- Green — confirmed in final sorted position
- Cyan — special role (pivot in quicksort, min in selection sort)
Each visualization counts comparisons and swaps, displayed in real time so you can see the efficiency difference between O(n²) and O(n log n) algorithms.
1. Bubble Sort — the gentle giant
Bubble sort is the simplest sorting algorithm: repeatedly walk through the array, compare adjacent pairs, and swap them if they are in the wrong order. Large values "bubble" to the end. It is O(n²) — painfully slow on large arrays — but watching it work is oddly satisfying. Each pass guarantees the next-largest element reaches its final position, so the green zone grows from right to left.
<!DOCTYPE html>
<html><head><title>Bubble Sort Visualization</title></head>
<body style="margin:0;background:#000;display:flex;justify-content:center;align-items:center;height:100vh">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
c.width=800;c.height=500;
const N=80,arr=Array.from({length:N},(_,i)=>i+1);
for(let i=N-1;i>0;i--){const j=Math.random()*i|0;[arr[i],arr[j]]=[arr[j],arr[i]]}
let colors=new Array(N).fill('#fff'),comps=0,swaps=0,done=false;
function draw(){
x.fillStyle='#000';x.fillRect(0,0,800,500);
const w=800/N,maxH=460;
for(let i=0;i<N;i++){
x.fillStyle=colors[i];
const h=arr[i]/N*maxH;
x.fillRect(i*w+1,500-h,w-2,h);
}
x.fillStyle='#fff';x.font='14px monospace';
x.fillText('Bubble Sort — Comparisons: '+comps+' Swaps: '+swaps,10,20);
if(done)x.fillText('SORTED!',350,250);
}
async function*bubbleSort(){
for(let i=N-1;i>0;i--){
for(let j=0;j<i;j++){
colors[j]='#ff0';colors[j+1]='#ff0';comps++;yield;
if(arr[j]>arr[j+1]){
colors[j]='#f44';colors[j+1]='#f44';
[arr[j],arr[j+1]]=[arr[j+1],arr[j]];swaps++;yield;
}
colors[j]='#fff';colors[j+1]='#fff';
}
colors[i]='#4f4';
}
colors[0]='#4f4';done=true;
}
const gen=bubbleSort();
let speed=4;
function step(){
for(let i=0;i<speed;i++){const r=gen.next();if(r.done)break}
draw();if(!done)requestAnimationFrame(step);else draw();
}
c.onclick=()=>{speed=speed===4?20:speed===20?1:4};
step();
</script>
</body></html>
Notice how bubble sort makes many redundant comparisons. Even after an element reaches its correct position early, the algorithm still compares it in subsequent passes. This is why it averages n(n-1)/2 comparisons. Click the canvas to change speed — slow mode reveals the individual comparisons beautifully.
2. Selection Sort — find the minimum
Selection sort divides the array into sorted (left) and unsorted (right) regions. Each pass scans the unsorted region for the minimum value, then swaps it into position. It always makes exactly n(n-1)/2 comparisons regardless of input — no best or worst case, just relentless scanning. The cyan highlight shows the current minimum candidate.
<!DOCTYPE html>
<html><head><title>Selection Sort Visualization</title></head>
<body style="margin:0;background:#000;display:flex;justify-content:center;align-items:center;height:100vh">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
c.width=800;c.height=500;
const N=80,arr=Array.from({length:N},(_,i)=>i+1);
for(let i=N-1;i>0;i--){const j=Math.random()*i|0;[arr[i],arr[j]]=[arr[j],arr[i]]}
let colors=new Array(N).fill('#fff'),comps=0,swaps=0,done=false;
function draw(){
x.fillStyle='#000';x.fillRect(0,0,800,500);
const w=800/N,maxH=460;
for(let i=0;i<N;i++){
x.fillStyle=colors[i];
const h=arr[i]/N*maxH;
x.fillRect(i*w+1,500-h,w-2,h);
}
x.fillStyle='#fff';x.font='14px monospace';
x.fillText('Selection Sort — Comparisons: '+comps+' Swaps: '+swaps,10,20);
if(done)x.fillText('SORTED!',350,250);
}
async function*selectionSort(){
for(let i=0;i<N-1;i++){
let minIdx=i;colors[minIdx]='#0ff';
for(let j=i+1;j<N;j++){
colors[j]='#ff0';comps++;yield;
if(arr[j]<arr[minIdx]){
if(minIdx!==i||colors[minIdx]==='#0ff')colors[minIdx]='#fff';
minIdx=j;colors[minIdx]='#0ff';
}else{colors[j]='#fff'}
}
if(minIdx!==i){
colors[i]='#f44';colors[minIdx]='#f44';
[arr[i],arr[minIdx]]=[arr[minIdx],arr[i]];swaps++;yield;
}
colors[minIdx]='#fff';colors[i]='#4f4';
}
colors[N-1]='#4f4';done=true;
}
const gen=selectionSort();
let speed=4;
function step(){
for(let i=0;i<speed;i++){const r=gen.next();if(r.done)break}
draw();if(!done)requestAnimationFrame(step);else draw();
}
c.onclick=()=>{speed=speed===4?20:speed===20?1:4};
step();
</script>
</body></html>
Selection sort makes far fewer swaps than bubble sort — at most n-1 swaps total. This makes it surprisingly efficient when swap cost is high (for example, moving large records on disk). Watch how the cyan highlight jumps when a new minimum is found, then confidently places it at the boundary.
3. Insertion Sort — the card player
Insertion sort works the way most people sort playing cards: pick the next unsorted card and slide it into the correct position among the already-sorted cards. It is O(n²) worst case but O(n) on nearly-sorted data — making it the fastest simple sort for real-world nearly-ordered inputs. The shifting animation shows elements sliding right to make room.
<!DOCTYPE html>
<html><head><title>Insertion Sort Visualization</title></head>
<body style="margin:0;background:#000;display:flex;justify-content:center;align-items:center;height:100vh">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
c.width=800;c.height=500;
const N=80,arr=Array.from({length:N},(_,i)=>i+1);
for(let i=N-1;i>0;i--){const j=Math.random()*i|0;[arr[i],arr[j]]=[arr[j],arr[i]]}
let colors=new Array(N).fill('#fff'),comps=0,swaps=0,done=false;
function draw(){
x.fillStyle='#000';x.fillRect(0,0,800,500);
const w=800/N,maxH=460;
for(let i=0;i<N;i++){
x.fillStyle=colors[i];
const h=arr[i]/N*maxH;
x.fillRect(i*w+1,500-h,w-2,h);
}
x.fillStyle='#fff';x.font='14px monospace';
x.fillText('Insertion Sort — Comparisons: '+comps+' Swaps: '+swaps,10,20);
if(done)x.fillText('SORTED!',350,250);
}
async function*insertionSort(){
colors[0]='#4f4';
for(let i=1;i<N;i++){
let key=arr[i],j=i-1;
colors[i]='#0ff';yield;
while(j>=0){
colors[j]='#ff0';comps++;yield;
if(arr[j]>key){
arr[j+1]=arr[j];colors[j+1]='#f44';colors[j]='#4f4';swaps++;yield;
colors[j+1]='#4f4';j--;
}else{colors[j]='#4f4';break}
}
arr[j+1]=key;colors[j+1]='#4f4';
}
for(let i=0;i<N;i++)colors[i]='#4f4';done=true;
}
const gen=insertionSort();
let speed=4;
function step(){
for(let i=0;i<speed;i++){const r=gen.next();if(r.done)break}
draw();if(!done)requestAnimationFrame(step);else draw();
}
c.onclick=()=>{speed=speed===4?20:speed===20?1:4};
step();
</script>
</body></html>
The visual signature of insertion sort is the green sorted region growing from left to right, with each new element sliding backward to find its place. On already-sorted input it finishes in O(n) — just one comparison per element. On reverse-sorted input it degrades to O(n²). This adaptive behavior makes it the go-to algorithm for small arrays; most real-world sorting libraries (including V8's Array.sort) switch to insertion sort below ~10-20 elements.
4. Merge Sort — divide and conquer
Merge sort splits the array in half, recursively sorts each half, and merges the results. It always runs in O(n log n) — no worst case. The merge step is where the magic happens: two sorted halves zip together like a zipper. The visualization shows the recursive subdivision as pairs of colored regions being merged together, bottom-up.
<!DOCTYPE html>
<html><head><title>Merge Sort Visualization</title></head>
<body style="margin:0;background:#000;display:flex;justify-content:center;align-items:center;height:100vh">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
c.width=800;c.height=500;
const N=80,arr=Array.from({length:N},(_,i)=>i+1);
for(let i=N-1;i>0;i--){const j=Math.random()*i|0;[arr[i],arr[j]]=[arr[j],arr[i]]}
let colors=new Array(N).fill('#fff'),comps=0,writes=0,done=false;
function draw(){
x.fillStyle='#000';x.fillRect(0,0,800,500);
const w=800/N,maxH=460;
for(let i=0;i<N;i++){
x.fillStyle=colors[i];
const h=arr[i]/N*maxH;
x.fillRect(i*w+1,500-h,w-2,h);
}
x.fillStyle='#fff';x.font='14px monospace';
x.fillText('Merge Sort — Comparisons: '+comps+' Writes: '+writes,10,20);
if(done)x.fillText('SORTED!',350,250);
}
async function*mergeSort(lo,hi){
if(hi-lo<2)return;
const mid=(lo+hi)>>1;
yield*mergeSort(lo,mid);
yield*mergeSort(mid,hi);
const tmp=[];
let i=lo,j=mid;
while(i<mid&&j<hi){
colors[i]='#ff0';colors[j]='#ff0';comps++;yield;
if(arr[i]<=arr[j]){tmp.push(arr[i]);colors[i]='#fff';i++}
else{tmp.push(arr[j]);colors[j]='#fff';j++}
}
while(i<mid){tmp.push(arr[i]);i++}
while(j<hi){tmp.push(arr[j]);j++}
for(let k=0;k<tmp.length;k++){
arr[lo+k]=tmp[k];colors[lo+k]='#0ff';writes++;yield;
colors[lo+k]='#fff';
}
}
async function*run(){
yield*mergeSort(0,N);
for(let i=0;i<N;i++){colors[i]='#4f4';yield}
done=true;
}
const gen=run();
let speed=4;
function step(){
for(let i=0;i<speed;i++){const r=gen.next();if(r.done)break}
draw();if(!done)requestAnimationFrame(step);else draw();
}
c.onclick=()=>{speed=speed===4?20:speed===20?1:4};
step();
</script>
</body></html>
Merge sort's visual pattern is distinctive: you see small sorted runs forming at the bottom of the recursion, then progressively merging into larger runs. The final merge sweeps across the entire array. Notice how the comparison count is dramatically lower than the O(n²) algorithms — merge sort makes roughly n×log₂(n) comparisons. The tradeoff: it needs O(n) extra memory for the temporary array.
5. Quicksort — the king of practical sorting
Quicksort picks a pivot element (shown in cyan), partitions the array so everything less than the pivot goes left and everything greater goes right, then recursively sorts both sides. Average case O(n log n), worst case O(n²) — but with a good pivot strategy (median-of-three, random), the worst case is extremely rare. This visualization uses Lomuto partitioning with the last element as pivot.
<!DOCTYPE html>
<html><head><title>Quicksort Visualization</title></head>
<body style="margin:0;background:#000;display:flex;justify-content:center;align-items:center;height:100vh">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
c.width=800;c.height=500;
const N=80,arr=Array.from({length:N},(_,i)=>i+1);
for(let i=N-1;i>0;i--){const j=Math.random()*i|0;[arr[i],arr[j]]=[arr[j],arr[i]]}
let colors=new Array(N).fill('#fff'),comps=0,swaps=0,done=false;
function draw(){
x.fillStyle='#000';x.fillRect(0,0,800,500);
const w=800/N,maxH=460;
for(let i=0;i<N;i++){
x.fillStyle=colors[i];
const h=arr[i]/N*maxH;
x.fillRect(i*w+1,500-h,w-2,h);
}
x.fillStyle='#fff';x.font='14px monospace';
x.fillText('Quicksort — Comparisons: '+comps+' Swaps: '+swaps,10,20);
if(done)x.fillText('SORTED!',350,250);
}
async function*quicksort(lo,hi){
if(lo>=hi)return;
const pivot=arr[hi];colors[hi]='#0ff';
let i=lo;
for(let j=lo;j<hi;j++){
colors[j]='#ff0';comps++;yield;
if(arr[j]<pivot){
colors[i]='#f44';colors[j]='#f44';
[arr[i],arr[j]]=[arr[j],arr[i]];swaps++;yield;
colors[i]='#fff';colors[j]='#fff';i++;
}else{colors[j]='#fff'}
}
colors[hi]='#f44';colors[i]='#f44';
[arr[i],arr[hi]]=[arr[hi],arr[i]];swaps++;yield;
colors[i]='#4f4';colors[hi]='#fff';
yield*quicksort(lo,i-1);
yield*quicksort(i+1,hi);
}
async function*run(){
yield*quicksort(0,N-1);
for(let i=0;i<N;i++){colors[i]='#4f4';yield}
done=true;
}
const gen=run();
let speed=4;
function step(){
for(let i=0;i<speed;i++){const r=gen.next();if(r.done)break}
draw();if(!done)requestAnimationFrame(step);else draw();
}
c.onclick=()=>{speed=speed===4?20:speed===20?1:4};
step();
</script>
</body></html>
Watch how quicksort creates a "divide" pattern: the pivot (cyan) ends up in its final position (green), and the array splits into two independent subproblems. With a good pivot, each partition roughly halves the array, giving O(n log n). With a bad pivot (already-sorted input + last-element pivot), one partition has n-1 elements and the other has 0, degrading to O(n²). Modern implementations randomize the pivot to avoid this.
6. Heap Sort — the invisible tree
Heap sort treats the flat array as a binary heap (a complete binary tree stored in an array: children of index i are at 2i+1 and 2i+2). It builds a max-heap, then repeatedly extracts the maximum to the end. The result: guaranteed O(n log n) with O(1) extra memory. The visualization shows the sift-down operations as comparisons ripple through the "tree" structure hidden in the array.
<!DOCTYPE html>
<html><head><title>Heap Sort Visualization</title></head>
<body style="margin:0;background:#000;display:flex;justify-content:center;align-items:center;height:100vh">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
c.width=800;c.height=500;
const N=80,arr=Array.from({length:N},(_,i)=>i+1);
for(let i=N-1;i>0;i--){const j=Math.random()*i|0;[arr[i],arr[j]]=[arr[j],arr[i]]}
let colors=new Array(N).fill('#fff'),comps=0,swaps=0,done=false;
function draw(){
x.fillStyle='#000';x.fillRect(0,0,800,500);
const w=800/N,maxH=460;
for(let i=0;i<N;i++){
x.fillStyle=colors[i];
const h=arr[i]/N*maxH;
x.fillRect(i*w+1,500-h,w-2,h);
}
x.fillStyle='#fff';x.font='14px monospace';
x.fillText('Heap Sort — Comparisons: '+comps+' Swaps: '+swaps,10,20);
if(done)x.fillText('SORTED!',350,250);
}
function*siftDown(n,i){
while(true){
let largest=i,l=2*i+1,r=2*i+2;
if(l<n){colors[l]='#ff0';colors[largest]='#ff0';comps++;yield;
if(arr[l]>arr[largest])largest=l;
colors[l]='#fff';colors[i]='#fff';
}
if(r<n){colors[r]='#ff0';colors[largest]='#ff0';comps++;yield;
if(arr[r]>arr[largest])largest=r;
colors[r]='#fff';colors[largest==='#ff0'?largest:i]='#fff';
}
if(largest===i)break;
colors[i]='#f44';colors[largest]='#f44';
[arr[i],arr[largest]]=[arr[largest],arr[i]];swaps++;yield;
colors[i]='#fff';colors[largest]='#fff';
i=largest;
}
}
function*heapSort(){
for(let i=(N>>1)-1;i>=0;i--)yield*siftDown(N,i);
for(let i=N-1;i>0;i--){
colors[0]='#f44';colors[i]='#f44';
[arr[0],arr[i]]=[arr[i],arr[0]];swaps++;yield;
colors[i]='#4f4';colors[0]='#fff';
yield*siftDown(i,0);
}
colors[0]='#4f4';done=true;
}
const gen=heapSort();
let speed=4;
function step(){
for(let i=0;i<speed;i++){const r=gen.next();if(r.done)break}
draw();if(!done)requestAnimationFrame(step);else draw();
}
c.onclick=()=>{speed=speed===4?20:speed===20?1:4};
step();
</script>
</body></html>
Heap sort's visual pattern is unique: during the build-heap phase, you see values rippling upward as the max-heap forms. During extraction, the rightmost green region grows as the maximum is repeatedly moved to the end. The comparison count is consistently O(n log n) — no variance. The downside: poor cache locality (the parent-child jumps in the array access distant memory), making it slower than quicksort in practice despite the same asymptotic complexity.
7. Radix Sort — no comparisons needed
Radix sort is fundamentally different: it never compares elements. Instead, it distributes values into buckets based on individual digits, starting from the least significant digit. For d-digit numbers with base b, it makes d passes of O(n+b) work — effectively O(n) for fixed-width integers. The visualization shows elements being redistributed by digit, creating a wave-like pattern as values flow into buckets and back.
<!DOCTYPE html>
<html><head><title>Radix Sort (LSD) Visualization</title></head>
<body style="margin:0;background:#000;display:flex;justify-content:center;align-items:center;height:100vh">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
c.width=800;c.height=500;
const N=80,arr=Array.from({length:N},(_,i)=>i+1);
for(let i=N-1;i>0;i--){const j=Math.random()*i|0;[arr[i],arr[j]]=[arr[j],arr[i]]}
let colors=new Array(N).fill('#fff'),writes=0,pass=0,done=false;
const hues=['#f44','#f80','#ff0','#4f4','#0ff','#48f','#80f','#f4f','#f88','#8ff'];
function draw(){
x.fillStyle='#000';x.fillRect(0,0,800,500);
const w=800/N,maxH=460;
for(let i=0;i<N;i++){
x.fillStyle=colors[i];
const h=arr[i]/N*maxH;
x.fillRect(i*w+1,500-h,w-2,h);
}
x.fillStyle='#fff';x.font='14px monospace';
x.fillText('Radix Sort (LSD, base 10) — Digit pass: '+pass+' Writes: '+writes,10,20);
if(done)x.fillText('SORTED!',350,250);
}
function*radixSort(){
const max=Math.max(...arr);
let exp=1;
while(exp<=max){
pass++;
const buckets=Array.from({length:10},()=>[]);
for(let i=0;i<N;i++){
const digit=Math.floor(arr[i]/exp)%10;
colors[i]=hues[digit];yield;
buckets[digit].push(arr[i]);
}
let idx=0;
for(let d=0;d<10;d++){
for(const v of buckets[d]){
arr[idx]=v;colors[idx]=hues[d];writes++;idx++;yield;
}
}
exp*=10;
yield;
}
for(let i=0;i<N;i++){colors[i]='#4f4';yield}
done=true;
}
const gen=radixSort();
let speed=2;
function step(){
for(let i=0;i<speed;i++){const r=gen.next();if(r.done)break}
draw();if(!done)requestAnimationFrame(step);else draw();
}
c.onclick=()=>{speed=speed===2?8:speed===8?1:2};
step();
</script>
</body></html>
The color coding in radix sort shows each digit bucket. Watch how after the first pass (ones digit), elements are partially ordered. After the second pass (tens digit), order improves dramatically. For values 1-80, just two passes suffice. The linear time complexity is visible: the total number of writes is exactly n × number_of_digits, independent of the initial order. Radix sort is the secret weapon for sorting integers, strings, and fixed-length records.
8. Algorithm Race — all seven at once
The ultimate comparison: all seven algorithms race on identical shuffled arrays. Each gets its own lane. You can see at a glance which algorithms finish first (merge sort and quicksort), which plod along (bubble sort), and which have unique visual signatures. The comparison and swap counters reveal the quantitative story behind the visual drama.
<!DOCTYPE html>
<html><head><title>Sorting Algorithm Race</title></head>
<body style="margin:0;background:#000;overflow:hidden">
<canvas id="c"></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d');
c.width=900;c.height=700;
const N=60,algos=[];
const names=['Bubble','Selection','Insertion','Merge','Quick','Heap','Radix'];
const base=Array.from({length:N},(_,i)=>i+1);
const seed=[];for(let i=N-1;i>0;i--){const j=Math.random()*i|0;[base[i],base[j]]=[base[j],base[i]]}
for(let i=0;i<N;i++)seed[i]=base[i];
function makeAlgo(name){
const a=[...seed],cl=new Array(N).fill('#fff');
let comps=0,swaps=0,done=false;
function*bubble(){
for(let i=N-1;i>0;i--){for(let j=0;j<i;j++){cl[j]='#ff0';cl[j+1]='#ff0';comps++;yield;if(a[j]>a[j+1]){[a[j],a[j+1]]=[a[j+1],a[j]];swaps++;cl[j]='#f44';cl[j+1]='#f44';yield}cl[j]='#fff';cl[j+1]='#fff'}cl[i]='#4f4'}cl[0]='#4f4';done=true;
}
function*selection(){
for(let i=0;i<N-1;i++){let m=i;for(let j=i+1;j<N;j++){comps++;if(a[j]<a[m])m=j}if(m!==i){[a[i],a[m]]=[a[m],a[i]];swaps++}cl[i]='#4f4';yield}cl[N-1]='#4f4';done=true;
}
function*insertion(){
for(let i=1;i<N;i++){let k=a[i],j=i-1;while(j>=0&&a[j]>k){a[j+1]=a[j];comps++;swaps++;j--}a[j+1]=k;comps++;cl[i]='#4f4';yield}for(let i=0;i<N;i++)cl[i]='#4f4';done=true;
}
function*merge(lo,hi){
if(hi-lo<2)return;const mid=(lo+hi)>>1;yield*merge(lo,mid);yield*merge(mid,hi);
const t=[];let i=lo,j=mid;while(i<mid&&j<hi){comps++;if(a[i]<=a[j])t.push(a[i++]);else t.push(a[j++])}
while(i<mid)t.push(a[i++]);while(j<hi)t.push(a[j++]);
for(let k=0;k<t.length;k++){a[lo+k]=t[k];swaps++}yield;
}
function*mergeW(){yield*merge(0,N);for(let i=0;i<N;i++)cl[i]='#4f4';done=true}
function*quick(lo,hi){
if(lo>=hi)return;const p=a[hi];let i=lo;
for(let j=lo;j<hi;j++){comps++;if(a[j]<p){[a[i],a[j]]=[a[j],a[i]];swaps++;i++}}
[a[i],a[hi]]=[a[hi],a[i]];swaps++;cl[i]='#4f4';yield;
yield*quick(lo,i-1);yield*quick(i+1,hi);
}
function*quickW(){yield*quick(0,N-1);for(let i=0;i<N;i++)cl[i]='#4f4';done=true}
function*sift(n,i){
while(true){let lg=i,l=2*i+1,r=2*i+2;
if(l<n){comps++;if(a[l]>a[lg])lg=l}if(r<n){comps++;if(a[r]>a[lg])lg=r}
if(lg===i)break;[a[i],a[lg]]=[a[lg],a[i]];swaps++;i=lg}
}
function*heap(){
for(let i=(N>>1)-1;i>=0;i--)yield*sift(N,i);
for(let i=N-1;i>0;i--){[a[0],a[i]]=[a[i],a[0]];swaps++;cl[i]='#4f4';yield*sift(i,0);yield}
cl[0]='#4f4';done=true;
}
function*radix(){
const mx=Math.max(...a);let e=1;
while(e<=mx){const b=Array.from({length:10},()=>[]);
for(let i=0;i<N;i++)b[Math.floor(a[i]/e)%10].push(a[i]);
let idx=0;for(const bk of b)for(const v of bk){a[idx++]=v;swaps++}e*=10;yield}
for(let i=0;i<N;i++)cl[i]='#4f4';done=true;
}
const gens={Bubble:bubble,Selection:selection,Insertion:insertion,Merge:mergeW,Quick:quickW,Heap:heap,Radix:radix};
return{name,arr:a,colors:cl,comps:()=>comps,swaps:()=>swaps,done:()=>done,gen:gens[name]()};
}
for(const n of names)algos.push(makeAlgo(n));
function draw(){
x.fillStyle='#000';x.fillRect(0,0,900,700);
const laneH=90,w=900/N;
for(let a=0;a<algos.length;a++){
const al=algos[a],y=a*laneH+30;
x.fillStyle='#fff';x.font='11px monospace';
x.fillText(al.name+(al.done()?' DONE':'')+' C:'+al.comps()+' S:'+al.swaps(),4,y-4);
for(let i=0;i<N;i++){
x.fillStyle=al.colors[i];
const h=al.arr[i]/N*(laneH-20);
x.fillRect(i*w+0.5,y+laneH-20-h,w-1,h);
}
}
}
let allDone=false;
function step(){
for(const al of algos){if(!al.done()){for(let i=0;i<3;i++){const r=al.gen.next();if(r.done)break}}}
draw();
allDone=algos.every(a=>a.done());
if(!allDone)requestAnimationFrame(step);else draw();
}
step();
</script>
</body></html>
The race reveals the truth that complexity analysis predicts: merge sort, quicksort, and heap sort finish while bubble sort is still laboring through its first few passes. Radix sort finishes almost instantly — its linear time complexity is unbeatable for integer data. Selection sort and insertion sort fall in the middle. The comparison counters tell the story: O(n²) algorithms accumulate thousands of comparisons while O(n log n) algorithms stay in the hundreds.
Complexity Cheat Sheet
| Algorithm | Best | Average | Worst | Memory | Stable |
|---|---|---|---|---|---|
| Bubble Sort | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Selection Sort | O(n²) | O(n²) | O(n²) | O(1) | No |
| Insertion Sort | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Merge Sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| Quicksort | O(n log n) | O(n log n) | O(n²) | O(log n) | No |
| Heap Sort | O(n log n) | O(n log n) | O(n log n) | O(1) | No |
| Radix Sort | O(nk) | O(nk) | O(nk) | O(n+k) | Yes |
Stable means equal elements preserve their original relative order. This matters when sorting by multiple keys (for example, sorting by name then by age — a stable sort preserves the name order within each age group).
When to Use What
- Small arrays (<20 elements): insertion sort — minimal overhead, adaptive
- General purpose: quicksort (with randomized pivot) — fastest in practice due to cache locality
- Guaranteed worst-case: merge sort — always O(n log n), stable, but needs extra memory
- Memory-constrained: heap sort — O(n log n) with O(1) extra space
- Integer/fixed-width data: radix sort — linear time, unbeatable for the right data types
- Nearly sorted data: insertion sort — adapts to O(n) when input is almost sorted
- Educational purposes: bubble sort — easy to understand, terrible to use in production
Most modern language runtimes use hybrid sorts: Python uses Timsort (merge sort + insertion sort), V8 uses TimSort, Java uses dual-pivot quicksort for primitives and TimSort for objects. These combine the strengths of multiple algorithms.
Sorting algorithm visualization transforms abstract computer science into something you can see, hear, and feel. The patterns are beautiful: bubble sort's gentle waves, quicksort's aggressive partitioning, merge sort's fractal subdivision, radix sort's digit-based rainbow. Each algorithm is a different philosophy of imposing order on chaos.
Want to explore more algorithm-driven art? Visit Lumitree to discover unique generative micro-worlds, or browse the full collection of creative coding tutorials for more visual programming guides.