Sorting algorithms visualized[source]
xml
<glacius:metadata> | |
<title>Sorting algorithms visualized</title> | |
<description>JavaScript animations of various sorting algorithms</description> | |
<category>Programming</category> | |
<category>JavaScript</category> | |
<category>Algorithms</category> | |
</glacius:metadata> | |
<style> | |
.sortable-container { | |
position: relative; | |
border: 1px solid rgba(255, 255, 255, 0.25); | |
height: 300px; | |
background-color: rgba(0, 0, 0, 0.2); | |
max-width: 650px; | |
border-radius: 2px; | |
margin: 0 auto 1.25rem auto; | |
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5); | |
} | |
.sortable-item { | |
transition: left 250ms ease-in-out; | |
position: absolute; | |
width: 20px; | |
display: flex; | |
flex-direction: column; | |
} | |
.sortable-bar { | |
border: 1px solid rgba(255, 255, 255, 0.35); | |
background-color: rgba(0, 0, 0, 0.25); | |
flex: 1; | |
} | |
.sortable-label { | |
text-align: center; | |
font-size: 12px; | |
font-weight: bold; | |
font-family: monospace; | |
} | |
</style> | |
<div class="sortable-container"></div> | |
<div style="display: flex; justify-content: center; gap: .625rem;"> | |
<select class="form-control sort-algo" style="width: auto"> | |
<option value="bubbleSort">Bubble sort</option> | |
<option value="mergeSort">Merge sort</option> | |
<option value="insertionSort">Insertion sort</option> | |
<option value="quickSort">Quick sort</option> | |
<option value="selectionSort">Selection sort</option> | |
</select> | |
<button class="btn btn-primary start-random">Sort random</button> | |
<button class="btn btn-primary start-reversed">Sort reversed</button> | |
</div> | |
<script> | |
//<![CDATA[ | |
(() => { | |
const runSort = async (unsortedValues, onStep, sort) => { | |
const sortables = unsortedValues.map((value, i) => { return {value, index: i} }); | |
let prev = null; | |
const step = async () => { | |
const sortedByIndex = sortables.concat([]) | |
.sort((a, b) => a.index === b.index ? 0 : (a.index < b.index ? -1 : 1)); | |
const hash = sortedByIndex.map(s => s.value).join(','); | |
if (prev === hash) { | |
return; | |
} | |
prev = hash; | |
return await onStep(sortedByIndex); | |
}; | |
await sort(sortables, step); | |
return sortables; | |
}; | |
const swap = (list, i, j) => { | |
const temp = list[i]; | |
const oldIndex = temp.index; | |
list[i].index = list[j].index; | |
list[j].index = oldIndex; | |
list[i] = list[j]; | |
list[j] = temp; | |
}; | |
window.mergeSort = async (unsortedValues, onStep) => { | |
const merge = async (left, right, step, offset, fullList) => { | |
const sorted = []; | |
const push = (list) => { | |
const item = list.shift(); | |
sorted.push(item); | |
const oldIndex = item.index; | |
item.index = offset + sorted.length - 1; | |
if (item.index > oldIndex) { | |
fullList.filter(x => x !== item && x.index > oldIndex && x.index <= item.index) | |
.forEach(item => item.index--); | |
} else if (item.index < oldIndex) { | |
fullList.filter(x => x !== item && x.index < oldIndex && x.index >= item.index) | |
.forEach(item => item.index++); | |
} | |
} | |
while (left.length && right.length) { | |
push(left[0].value <= right[0].value ? left : right); | |
await step(); | |
} | |
while (left.length) { | |
push(left); | |
await step(); | |
} | |
while (right.length) { | |
push(right); | |
await step(); | |
} | |
await step(); | |
return sorted; | |
}; | |
const mergeSort = async (list, step, fullList = null, offset = 0) => { | |
if (list.length <= 1) { | |
return list; | |
} | |
await step(); | |
fullList = fullList || list; | |
const mid = Math.floor(list.length / 2); | |
const left = await mergeSort(list.slice(0, mid), step, fullList, offset); | |
const right = await mergeSort(list.slice(mid), step, fullList, offset + mid); | |
return await merge(left, right, step, offset, fullList); | |
}; | |
return runSort(unsortedValues, onStep, mergeSort); | |
}; | |
window.insertionSort = async (unsortedValues, onStep) => { | |
const insertionSort = async (list, step) => { | |
for (let i = 1; i < list.length; i++) { | |
let j = i; | |
while (j > 0 && list[j - 1].value > list[j].value) { | |
const temp = list[j]; | |
list[j].index = j - 1; | |
list[j - 1].index = j; | |
list[j] = list[j - 1]; | |
list[j - 1] = temp; | |
j--; | |
await step(); | |
} | |
} | |
return list; | |
}; | |
return runSort(unsortedValues, onStep, insertionSort); | |
}; | |
window.bubbleSort = async (unsortedValues, onStep) => { | |
const bubbleSort = async (list, step) => { | |
let swapped; | |
do { | |
swapped = false; | |
for (let i = 1; i < list.length; i++) { | |
if (list[i - 1].value > list[i].value) { | |
const temp = list[i]; | |
list[i].index = i - 1; | |
list[i - 1].index = i; | |
list[i] = list[i - 1]; | |
list[i - 1] = temp; | |
swapped = true; | |
await step(); | |
} | |
} | |
} while (swapped); | |
}; | |
return runSort(unsortedValues, onStep, bubbleSort); | |
}; | |
window.quickSort = async (unsortedValues, onStep) => { | |
const quickSort = async (list, step) => { | |
const partition = async (list, lo, hi) => { | |
const pivot = list[hi]; | |
let i = lo - 1; | |
for (let j = lo; j <= hi - 1; j++) { | |
if (list[j].value <= pivot.value) { | |
i++; | |
swap(list, i, j); | |
await step(); | |
} | |
} | |
i++; | |
swap(list, i, hi); | |
await step(); | |
return i; | |
}; | |
const sort = async (list, lo, hi) => { | |
if (lo >= hi || lo < 0) { | |
return; | |
} | |
await step(); | |
const pivot = await partition(list, lo, hi); | |
await sort(list, lo, pivot - 1); | |
await sort(list, pivot + 1, hi); | |
}; | |
return sort(list, 0, list.length - 1); | |
}; | |
return runSort(unsortedValues, onStep, quickSort); | |
}; | |
window.selectionSort = async (unsortedValues, onStep) => { | |
const selectionSort = async (list, step) => { | |
await step(); | |
for (let i = 0; i < list.length - 1; i++) { | |
let minIndex = i; | |
for (let j = i + 1; j < list.length; j++) { | |
if (list[j].value < list[minIndex].value) { | |
minIndex = j; | |
} | |
} | |
if (minIndex !== i) { | |
// swap | |
swap(list, i, minIndex); | |
await step(); | |
} | |
} | |
}; | |
return runSort(unsortedValues, onStep, selectionSort); | |
}; | |
})(); | |
//]]> | |
</script> | |
<script> | |
//<![CDATA[ | |
(() => { | |
const startRandom = document.querySelector('.start-random'); | |
const startReversed = document.querySelector('.start-reversed'); | |
const sortableContainer = document.querySelector('.sortable-container'); | |
const algoSelector = document.querySelector('.sort-algo'); | |
const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); | |
const containerHeight = sortableContainer.clientHeight; | |
const doSort = (values) => { | |
startRandom.disabled = true; | |
startReversed.disabled = true; | |
const sortableMap = new Map(); | |
const containerWidth = sortableContainer.clientWidth; | |
const containerPadding = containerWidth / 60; | |
const itemGutter = containerWidth / 120; | |
const itemOuterWidth = (containerWidth - (containerPadding * 2)) / values.length; | |
const step = async (sortables) => { | |
const first = sortables[0]; | |
if (!sortableMap.has(first)) { | |
let min = Infinity; | |
let max = -Infinity; | |
for (const sortable of sortables) { | |
const el = document.createElement('div'); | |
el.className = 'sortable-item'; | |
el.style.left = containerPadding + (sortable.index * itemOuterWidth) + 'px'; | |
el.style.width = itemOuterWidth + 'px'; | |
el.style.padding = `0 ${itemGutter}px`; | |
sortableContainer.appendChild(el); | |
const bar = document.createElement('div'); | |
bar.className = 'sortable-bar'; | |
const label = document.createElement('div'); | |
label.className = 'sortable-label'; | |
label.appendChild(document.createTextNode(sortable.value.toString())); | |
el.appendChild(bar); | |
el.appendChild(label); | |
min = Math.min(min, sortable.value); | |
max = Math.max(max, sortable.value); | |
sortableMap.set(sortable, el); | |
} | |
for (const sortable of sortables) { | |
const range = Math.abs(max - min); | |
const pctOfRange = (sortable.value - min) / range; | |
const minItemHeight = 40; | |
const maxHeight = containerHeight - (containerPadding * 2) - minItemHeight; | |
const height = (pctOfRange * maxHeight) + minItemHeight; | |
const el = sortableMap.get(sortable); | |
el.style.height = height + 'px'; | |
el.style.bottom = containerPadding + 'px'; | |
} | |
} else { | |
for (const sortable of sortables) { | |
const el = sortableMap.get(sortable); | |
el.style.left = containerPadding + (sortable.index * itemOuterWidth) + 'px'; | |
} | |
} | |
await wait(250); | |
}; | |
sortableContainer.innerHTML = ''; | |
const sortAlgoFn = window[algoSelector.value]; | |
if (typeof(sortAlgoFn) !== 'function') { | |
throw new Error('unknown sort algo'); | |
} | |
sortAlgoFn(values, step) | |
.catch((err) => { | |
console.error(err); | |
}) | |
.finally(() => { | |
startRandom.disabled = false; | |
startReversed.disabled = false; | |
}); | |
}; | |
const shuffle = (arr) => { | |
const remaining = arr.concat([]); | |
let count = 0; | |
while (remaining.length) { | |
const index = Math.floor(Math.random() * remaining.length); | |
const item = remaining[index]; | |
remaining.splice(index, 1); | |
arr[count] = item; | |
count++; | |
} | |
return arr; | |
}; | |
const defaultValues = []; | |
for (let i = 1; i <= 20; i++) { | |
defaultValues.push(i); | |
} | |
startRandom.addEventListener('click', () => doSort(shuffle(defaultValues.concat([])))); | |
startReversed.addEventListener('click', () => doSort(defaultValues.concat([]).reverse())); | |
})(); | |
//]]> | |
</script> | |