sorting/sorting_visualization.html

2986 lines
159 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="de" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#0b0d14">
<title>Sortier-Algorithmen Visualisierung</title>
<!-- Tailwind Play CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
bg: 'var(--c-bg)',
surface: 'var(--c-surface)',
surface2: 'var(--c-surface2)',
border: 'var(--c-border)',
accent: 'var(--c-accent)',
muted: 'var(--c-muted)',
cNormal: 'var(--c-normal)',
cCompare: 'var(--c-compare)',
cSwap: 'var(--c-swap)',
cMove: 'var(--c-move)',
cSorted: 'var(--c-sorted)',
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
mono: ['JetBrains Mono', 'Fira Code', 'monospace'],
},
},
},
};
</script>
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<style>
/* ── Theme Custom Properties ── */
:root {
--c-bg: #f5f6fa; --c-surface: #ffffff; --c-surface2: #eef0f5;
--c-border: #d5d8e3; --c-text: #1a1d2e; --c-muted: #6b7394;
--c-accent: #4a7cff;
--c-normal: #4a7cff; --c-compare: #ff9a3c; --c-swap: #ff3c5a;
--c-move: #b94aff; --c-sorted: #3cffa0; --c-axis: #d5d8e3; --c-bartext: #1a1d2e;
}
.dark {
--c-bg: #0b0d14; --c-surface: #141720; --c-surface2: #1c2030;
--c-border: #262a3d; --c-text: #e0e4f0; --c-muted: #6b7394;
--c-accent: #4a7cff;
--c-normal: #4a7cff; --c-compare: #ff9a3c; --c-swap: #ff3c5a;
--c-move: #b94aff; --c-sorted: #3cffa0; --c-axis: #262a3d; --c-bartext: #e0e4f0;
}
/* ── Viewport-relative Fonts ── */
html, body {
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
overscroll-behavior: contain;
}
body { font-size: clamp(14px, 1.6vw, 16px); overflow-x: hidden; }
.title-text { font-size: clamp(1.25rem, 3.5vw, 2.25rem); }
.stat-value { font-size: clamp(1.5rem, 4vw, 2.25rem); }
.label-text { font-size: clamp(9px, 1.2vw, 11px); }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--c-bg); }
::-webkit-scrollbar-thumb { background: var(--c-border); border-radius: 3px; }
input[type="range"] {
-webkit-appearance: none;
appearance: none;
background: transparent;
cursor: pointer;
touch-action: none;
}
input[type="range"]::-webkit-slider-runnable-track {
height: 6px;
background: var(--c-surface2);
border-radius: 3px;
border: 1px solid var(--c-border);
}
input[type="range"]::-moz-range-track {
height: 6px;
background: var(--c-surface2);
border-radius: 3px;
border: 1px solid var(--c-border);
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px; height: 22px;
border-radius: 50%;
background: var(--c-accent);
border: 3px solid var(--c-bg);
margin-top: -9px;
box-shadow: 0 0 10px rgba(74,124,255,0.4);
transition: box-shadow 0.15s;
}
input[type="range"]::-moz-range-thumb {
width: 22px; height: 22px;
border-radius: 50%;
background: var(--c-accent);
border: 3px solid var(--c-bg);
box-shadow: 0 0 10px rgba(74,124,255,0.4);
}
input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 16px rgba(74,124,255,0.6); }
input[type="range"]:hover::-moz-range-thumb { box-shadow: 0 0 16px rgba(74,124,255,0.6); }
input[type="range"]:disabled { cursor: not-allowed; opacity: 0.4; }
input[type="range"]:disabled::-webkit-slider-thumb { background: var(--c-muted); box-shadow: none; }
input[type="range"]:disabled::-moz-range-thumb { background: var(--c-muted); box-shadow: none; }
select {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%236b7394' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 12px center;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
input[type="number"] { -moz-appearance: textfield; }
/* Details/Summary styling */
details summary::-webkit-details-marker { display: none; }
details > summary { list-style: none; }
details[open] .chevron-icon { transform: rotate(180deg); }
.chevron-icon { transition: transform 0.2s ease; display: inline-block; }
/* Light mode: override Tailwind grays used in info section */
:root:not(.dark) .text-gray-200 { color: var(--c-text) !important; }
:root:not(.dark) .text-gray-300 { color: #374151 !important; }
:root:not(.dark) .text-gray-400 { color: #4b5563 !important; }
:root:not(.dark) .text-white { color: var(--c-text) !important; }
:root:not(.dark) .hover\:text-white:hover { color: var(--c-accent) !important; }
:root:not(.dark) .bg-bg\/50 { background: color-mix(in srgb, var(--c-bg) 50%, transparent) !important; }
/* ── Mobile-first responsive tweaks ── */
@media (max-width: 640px) {
.stat-desc { display: none; }
.stat-card { padding: 0.5rem !important; }
.info-card { padding: 0.75rem !important; }
}
/* ── Sticky Sidebar (Desktop ≥1024px) ── */
@media (min-width: 1024px) {
.app-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 1rem;
align-items: start;
}
.app-layout > main {
min-width: 0; /* prevent grid blowout */
}
.app-sidebar {
position: sticky;
top: 1rem;
max-height: calc(100vh - 2rem);
overflow-y: auto;
}
}
/* ── Entry Animations ── */
@keyframes fadeSlideDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeSlideUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
@keyframes popIn { from { opacity: 0; transform: scale(0.92); } to { opacity: 1; transform: scale(1); } }
.anim-header { animation: fadeSlideDown 0.5s ease-out both; }
.anim-section { animation: fadeSlideUp 0.45s ease-out both; }
.anim-stat { animation: popIn 0.35s cubic-bezier(0.34,1.56,0.64,1) both; }
/* ── Reduced Motion ── */
@media (prefers-reduced-motion: reduce) {
.anim-header, .anim-section, .anim-stat { animation: none !important; }
}
/* ── Progress Bar ── */
.progress-track {
height: 4px;
background: var(--c-surface2);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--c-accent);
border-radius: 2px;
transition: width 0.1s linear;
}
</style>
</head>
<body class="bg-bg min-h-screen antialiased" style="color: var(--c-text);">
<div class="max-w-7xl mx-auto px-4 py-4 space-y-4">
<!-- Title + Theme Toggle -->
<header class="flex items-center justify-between pb-1">
<div class="flex-1"></div>
<div class="text-center">
<h1 class="title-text font-bold tracking-tight" style="color: var(--c-text); text-shadow: 0 0 40px rgba(74,124,255,0.3);">
2026-04-07 03:50:24 +02:00
Sortier-Algorithmen <span style="font-size: 0.45em; font-weight: 400; opacity: 0.5; vertical-align: middle;">v0.3.1</span>
</h1>
<p class="text-muted text-sm mt-0.5">Interaktive Visualisierung mit schrittweiser Animation</p>
</div>
<div class="flex-1 flex justify-end">
<button id="btnTheme" class="p-2 rounded-lg border border-border hover:bg-surface2 transition-colors" aria-label="Theme wechseln" title="Light / Dark Mode">
<i data-lucide="moon" class="w-5 h-5 text-muted dark-icon"></i>
<i data-lucide="sun" class="w-5 h-5 text-muted light-icon hidden"></i>
</button>
</div>
</header>
<!-- App Layout: Sidebar + Main -->
<div class="app-layout">
<!-- ═══ SIDEBAR ═══ -->
<aside class="app-sidebar space-y-3">
<!-- Algorithm -->
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
<div class="space-y-1.5">
<label for="algoSelect" class="label-text uppercase tracking-wider text-muted font-medium flex items-center gap-1.5">
<i data-lucide="settings-2" class="w-3.5 h-3.5"></i> Algorithmus
</label>
<select id="algoSelect"
class="w-full bg-surface2 border border-border rounded-lg px-3 py-2.5 text-sm outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition appearance-none pr-8" style="color: var(--c-text);">
<optgroup label="O(n²) — Einfache Verfahren">
<option value="bubble">Bubble Sort</option>
<option value="selection">Selection Sort</option>
<option value="insertion">Insertion Sort</option>
<option value="cocktail">Cocktail Shaker Sort</option>
</optgroup>
<optgroup label="O(n log n) — Effiziente Verfahren">
<option value="merge">Merge Sort</option>
<option value="heap">Heap Sort</option>
<option value="quick">Quick Sort</option>
<option value="quick3way">3-Way Quick Sort</option>
<option value="dualpivot">Dual-Pivot Quick Sort</option>
<option value="introsort">Introsort</option>
<option value="shell">Shell Sort</option>
<option value="tree">Tree Sort (BST)</option>
<option value="timsort">Timsort</option>
</optgroup>
<optgroup label="O(n·k) — Nicht-vergleichsbasiert">
<option value="counting">Counting Sort</option>
<option value="radix">Radix Sort (LSD)</option>
<option value="bucket">Bucket Sort</option>
</optgroup>
<optgroup label="Spezial">
<option value="pancake">Pancake Sort</option>
<option value="cycle">Cycle Sort</option>
<option value="bogo">Bogo Sort</option>
<option value="shuffle">Fisher-Yates Shuffle</option>
</optgroup>
</select>
</div>
<!-- Preset -->
<div class="space-y-1.5">
<label for="presetSelect" class="label-text uppercase tracking-wider text-muted font-medium flex items-center gap-1.5">
<i data-lucide="list" class="w-3.5 h-3.5"></i> Daten-Vorlage
</label>
<select id="presetSelect"
class="w-full bg-surface2 border border-border rounded-lg px-3 py-2 text-sm outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition appearance-none pr-8" style="color: var(--c-text);">
<option value="random">Zufall</option>
<option value="sorted">Sortiert</option>
<option value="reversed">Umgekehrt</option>
<option value="nearly">Fast sortiert</option>
<option value="duplicates">Duplikate</option>
</select>
</div>
<!-- Custom Array Input -->
<div class="space-y-1.5">
<label for="customArray" class="label-text uppercase tracking-wider text-muted font-medium flex items-center gap-1.5">
<i data-lucide="pencil" class="w-3.5 h-3.5"></i> Eigene Werte
<span id="customArrayCount" class="text-[10px] text-muted ml-auto normal-case tracking-normal font-mono"></span>
</label>
<div class="flex gap-1.5">
<input type="text" id="customArray" placeholder="z.B. 5, 3, 8, 1 oder Mehrfachpaste"
class="flex-1 min-w-0 bg-surface2 border border-border rounded-lg px-2 py-2 text-sm outline-none focus:border-accent focus:ring-1 focus:ring-accent/30 transition" style="color: var(--c-text);">
<button id="btnPasteCustom" class="px-2.5 min-w-[36px] flex items-center justify-center rounded-lg border border-border bg-surface2 hover:bg-accent hover:border-accent hover:text-white transition-colors" style="color: var(--c-text);" title="Aus Zwischenablage einfügen" aria-label="Aus Zwischenablage einfügen">
<i data-lucide="clipboard-paste" class="w-4 h-4"></i>
</button>
<button id="btnClearCustom" class="px-2.5 min-w-[36px] flex items-center justify-center rounded-lg border border-border bg-surface2 hover:bg-accent hover:border-accent hover:text-white transition-colors" style="color: var(--c-text);" title="Eigene Werte leeren">
<i data-lucide="x" class="w-4 h-4"></i>
</button>
</div>
</div>
</section>
<!-- Sliders -->
<section class="bg-surface border border-border rounded-xl p-4 space-y-3">
<!-- Array Size -->
<div class="space-y-1.5">
<label for="sizeSlider" class="label-text uppercase tracking-wider text-muted font-medium flex items-center gap-1.5">
<i data-lucide="bar-chart-3" class="w-3.5 h-3.5"></i> Array-Größe
<span id="sizeVal" class="text-accent font-mono ml-auto">50</span>
</label>
<input type="range" id="sizeSlider" min="5" max="200" value="50" class="w-full h-6">
</div>
<!-- Speed -->
<div class="space-y-1.5">
<label for="speedSlider" class="label-text uppercase tracking-wider text-muted font-medium flex items-center gap-1.5">
<i data-lucide="gauge" class="w-3.5 h-3.5"></i> Geschw.
<span id="speedVal" class="text-accent font-mono ml-auto" style="font-size:11px;">500 ms/Schritt</span>
</label>
<input type="range" id="speedSlider" min="1" max="100" value="50" class="w-full h-6">
</div>
</section>
<!-- Transport Buttons -->
<section class="bg-surface border border-border rounded-xl p-4">
<div class="grid grid-cols-5 gap-2">
<button id="btnStepBack"
class="flex items-center justify-center bg-surface2 border border-border text-muted rounded-lg py-2.5 text-sm font-medium hover:bg-accent hover:border-accent hover:text-white active:scale-95 transition-all min-h-[44px]"
aria-label="Schritt zurück" title="Schritt zurück">
<i data-lucide="skip-back" class="w-4 h-4"></i>
</button>
<button id="btnPlayPause"
class="flex items-center justify-center bg-accent/10 border border-accent text-accent rounded-lg py-2.5 text-sm font-medium hover:bg-accent hover:text-white active:scale-95 transition-all min-h-[44px]"
aria-label="Start / Pause" title="Start / Pause">
<i data-lucide="play" class="w-4 h-4 pp-play"></i>
<i data-lucide="pause" class="w-4 h-4 pp-pause hidden"></i>
</button>
<button id="btnStep"
class="flex items-center justify-center bg-surface2 border border-border text-muted rounded-lg py-2.5 text-sm font-medium hover:bg-accent hover:border-accent hover:text-white active:scale-95 transition-all min-h-[44px]"
aria-label="Einzelschritt vorwärts" title="Einzelschritt">
<i data-lucide="skip-forward" class="w-4 h-4"></i>
</button>
<button id="btnShuffle"
class="flex items-center justify-center bg-surface2 border border-border text-muted rounded-lg py-2.5 text-sm font-medium hover:bg-accent hover:border-accent hover:text-white active:scale-95 transition-all min-h-[44px]"
2026-04-06 21:22:56 +02:00
aria-label="Mischen" title="Mischen (Fisher-Yates)">
<i data-lucide="shuffle" class="w-4 h-4"></i>
</button>
<button id="btnReset"
class="flex items-center justify-center bg-surface2 border border-border text-muted rounded-lg py-2.5 text-sm font-medium hover:bg-accent hover:border-accent hover:text-white active:scale-95 transition-all min-h-[44px]"
aria-label="Zurücksetzen" title="Reset">
<i data-lucide="rotate-ccw" class="w-4 h-4"></i>
</button>
</div>
</section>
<!-- Legend (in sidebar) -->
<section class="bg-surface border border-border rounded-xl px-4 py-3 flex flex-wrap gap-x-4 gap-y-1.5 items-center">
<div class="flex items-center gap-2 text-xs text-muted"><span class="w-3 h-3 rounded-sm bg-cNormal inline-block"></span> Unsortiert</div>
<div class="flex items-center gap-2 text-xs text-muted"><span class="w-3 h-3 rounded-sm bg-cCompare inline-block"></span> Vergleich</div>
<div class="flex items-center gap-2 text-xs text-muted"><span class="w-3 h-3 rounded-sm bg-cSwap inline-block"></span> Tausch</div>
<div class="flex items-center gap-2 text-xs text-muted"><span class="w-3 h-3 rounded-sm bg-cMove inline-block"></span> Verschiebung</div>
<div class="flex items-center gap-2 text-xs text-muted"><span class="w-3 h-3 rounded-sm bg-cSorted inline-block"></span> Sortiert</div>
</section>
</aside>
<!-- ═══ MAIN CONTENT ═══ -->
<main class="space-y-3">
<!-- Phase Label + Step Explanation (fixed height to prevent layout shift) -->
<div class="bg-surface border border-border rounded-xl px-4 py-2 space-y-0.5" style="min-height: 52px;">
<div id="phaseLabel" aria-live="polite" class="text-xs font-semibold text-accent truncate" style="min-height: 20px;">&nbsp;</div>
<div id="stepExplanation" aria-live="polite" class="text-xs text-muted truncate font-mono" style="min-height: 18px;">&nbsp;</div>
</div>
<!-- Canvas -->
<section class="bg-surface border border-border rounded-xl p-3 sm:p-4 overflow-hidden">
<canvas id="sortCanvas" role="img" aria-label="Visualisierung des Sortieralgorithmus als Balkendiagramm" class="w-full block rounded-lg"></canvas>
<!-- Memory View Container -->
<div id="memViewContainer" class="mt-3 pt-3 border-t border-border hidden">
<div id="memViewLabel" class="label-text uppercase tracking-wider text-muted font-medium mb-2 flex items-center gap-1.5">
<i data-lucide="hard-drive" class="w-3.5 h-3.5"></i> Arbeitsspeicher
</div>
<canvas id="memCanvas" role="img" aria-label="Speicher-Visualisierung" class="w-full block rounded-lg" style="background: color-mix(in srgb, var(--c-bg) 50%, transparent);"></canvas>
</div>
</section>
<!-- Progress Bar -->
<div class="flex items-center gap-3 px-1" style="min-height: 24px;">
<div class="progress-track flex-1">
<div id="progressFill" class="progress-fill" style="width: 0%;"></div>
</div>
<span id="stepCounter" aria-live="polite" class="text-xs text-muted font-mono whitespace-nowrap">0 / 0</span>
</div>
<!-- Statistics -->
<section class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
<div class="stat-value font-bold text-accent font-mono leading-none" id="statCompares">0</div>
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
<i data-lucide="git-compare" class="w-3.5 h-3.5"></i> Vergleiche
</div>
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft zwei Elemente miteinander verglichen wurden.</div>
</div>
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
<div class="stat-value font-bold text-accent font-mono leading-none" id="statSwaps">0</div>
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
<i data-lucide="arrow-left-right" class="w-3.5 h-3.5"></i> Tausche
</div>
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft zwei Elemente ihre Plätze getauscht haben.</div>
</div>
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
<div class="stat-value font-bold text-accent font-mono leading-none" id="statMoves">0</div>
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
<i data-lucide="move" class="w-3.5 h-3.5"></i> Verschiebungen
</div>
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Wie oft ein Wert auf eine Position geschrieben oder verschoben wurde.</div>
</div>
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
<div class="stat-value font-bold text-accent font-mono leading-none" id="statStep">0</div>
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
<i data-lucide="footprints" class="w-3.5 h-3.5"></i> Schritt
</div>
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Der aktuell ausgeführte Visualisierungsschritt.</div>
</div>
<div class="bg-surface border border-border rounded-xl p-4 text-center stat-card">
<div class="stat-value font-bold text-accent font-mono leading-none" id="statTime">0 ms</div>
<div class="label-text uppercase tracking-wider text-muted font-medium mt-2 flex items-center justify-center gap-1.5">
<i data-lucide="clock" class="w-3.5 h-3.5"></i> Zeit
</div>
<div class="stat-desc text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Vergangene Echtzeit seit Start der Animation.</div>
</div>
</section>
</main>
</div><!-- /app-layout -->
<!-- Algorithm Info (collapsible) -->
<details class="bg-surface border border-border rounded-xl group">
<summary class="p-4 sm:p-5 cursor-pointer select-none flex items-center gap-2 text-sm font-semibold text-gray-200 hover:text-white transition-colors">
<i data-lucide="book-open" class="w-4 h-4 text-accent"></i>
Über die Algorithmen
<i data-lucide="chevron-down" class="w-4 h-4 text-muted ml-auto chevron-icon"></i>
</summary>
<div class="px-4 sm:px-5 pb-4 sm:pb-5 pt-0 space-y-5">
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Speicher-Ansicht:</strong> Bei Tree Sort, Merge Sort, Timsort, Heap Sort, Counting Sort, Radix Sort und Bucket Sort wird der Arbeitsspeicher unterhalb des Arrays visualisiert. Sie sehen genau, wo Daten zwischengespeichert werden &mdash; ob als Baumstruktur (BST, Heap), Hilfs-Arrays (Merge), H&auml;ufigkeitstabelle (Counting) oder Bucket-Verteilung (Radix, Bucket).</p>
<!-- Legende -->
<div class="bg-surface2/50 rounded-lg p-3 border border-border/50">
<p class="text-[11px] uppercase tracking-wider text-muted font-medium mb-2">Legende der Eigenschaften</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1.5 text-xs text-muted leading-relaxed">
<div><span class="inline-block px-1.5 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20 text-[10px] mr-1.5">Stabil</span> Gleiche Werte behalten ihre urspr&uuml;ngliche Reihenfolge bei.</div>
<div><span class="inline-block px-1.5 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20 text-[10px] mr-1.5">Nicht stabil</span> Die Reihenfolge gleicher Werte kann sich &auml;ndern.</div>
<div><span class="inline-block px-1.5 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20 text-[10px] mr-1.5">In-place</span> Sortiert im vorhandenen Array, braucht nur O(1) zus&auml;tzlichen Speicher.</div>
<div><span class="inline-block px-1.5 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20 text-[10px] mr-1.5">O(n) Speicher</span> Ben&ouml;tigt ein zus&auml;tzliches Hilfsarray proportional zur Eingabegr&ouml;&szlig;e.</div>
<div><span class="inline-block px-1.5 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20 text-[10px] mr-1.5">Adaptiv</span> Nutzt vorhandene Ordnung aus &mdash; bei fast sortierten Daten schneller.</div>
<div><span class="inline-block px-1.5 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20 text-[10px] mr-1.5">Hybrid</span> Kombiniert mehrere Strategien und wechselt je nach Situation.</div>
<div class="sm:col-span-2 mt-1 pt-1 border-t border-border/30"><span class="inline-block px-1.5 py-0.5 rounded-full bg-surface border border-border text-gray-400 text-[10px] mr-1.5">O(n&sup2;)</span> Zeitkomplexit&auml;t &mdash; beschreibt, wie die Laufzeit mit der Eingabegr&ouml;&szlig;e w&auml;chst. <strong class="text-gray-400">O(n&sup2;)</strong> = quadratisch (langsam bei gro&szlig;en Daten), <strong class="text-gray-400">O(n log n)</strong> = quasi-linear (effizient), <strong class="text-gray-400">O(n+k)</strong> = linear (schnellstm&ouml;glich). Stehen zwei Werte (z.B. &bdquo;O(n&sup2;) worst &middot; O(n) best&ldquo;), beschreibt der erste den schlechtesten, der zweite den besten Fall.</div>
</div>
</div>
<div class="bg-surface2/50 rounded-lg p-3 border border-border/50">
<p class="text-[11px] uppercase tracking-wider text-muted font-medium mb-2">Farbcode der Visualisierung</p>
<div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-muted leading-relaxed">
<div class="flex items-center gap-1.5"><span class="inline-block w-3 h-3 rounded-sm bg-cCompare"></span> <strong class="text-gray-400">Orange</strong> = Vergleich</div>
<div class="flex items-center gap-1.5"><span class="inline-block w-3 h-3 rounded-sm bg-cSwap"></span> <strong class="text-gray-400">Rot</strong> = Tausch</div>
<div class="flex items-center gap-1.5"><span class="inline-block w-3 h-3 rounded-sm bg-cMove"></span> <strong class="text-gray-400">Lila</strong> = Verschiebung</div>
<div class="flex items-center gap-1.5"><span class="inline-block w-3 h-3 rounded-sm bg-green-500"></span> <strong class="text-gray-400">Gr&uuml;n</strong> = Sortiert / Fertig</div>
<div class="flex items-center gap-1.5"><span class="inline-block w-3 h-3 rounded-sm bg-accent"></span> <strong class="text-gray-400">Blau</strong> = Unsortiert</div>
</div>
</div>
<!-- O(n²) -->
<div>
<h3 class="text-xs uppercase tracking-wider text-muted font-medium mb-3">O(n²) — Einfache Verfahren</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Bubble Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n²)</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Durchläuft das Array wiederholt von links nach rechts und vergleicht jeweils zwei benachbarte Elemente. Ist das linke Element größer als das rechte, werden sie getauscht. Nach jedem vollständigen Durchlauf „blubbert" das größte noch unsortierte Element an seine endgültige Position am Ende. Das wird so lange wiederholt, bis ein Durchlauf ohne Tausch stattfindet.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Ein orange leuchtendes Paar wandert von links nach rechts — ein geschwungener Bogen über den Balken verbindet die beiden verglichenen Elemente und macht die Paarung auf einen Blick klar. Wird getauscht, werden beide Balken rot und der Bogen wechselt ebenfalls die Farbe. Mit jedem Durchlauf wächst der bereits sortierte Bereich am rechten Rand: er ist grünlich hinterlegt und dehnt sich Schritt für Schritt aus. Unter jedem Balken zeigt eine kleine Zahl den aktuellen Wert. Die Zeile über dem Canvas benennt den aktuellen Schritt (z.B. „Vergleiche arr[3]=42.1 mit arr[4]=17.5").</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Hauptsächlich zu Lehrzwecken, da er der intuitivste Sortieralgorithmus ist. Bei fast sortierten Daten kann er mit der „swapped"-Optimierung in O(n) terminieren. Für produktiven Einsatz aber zu langsam — Insertion Sort ist in derselben Komplexitätsklasse stets überlegen.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Selection Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n²)</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Teilt das Array gedanklich in einen sortierten (links) und unsortierten (rechts) Bereich. In jedem Schritt wird das Minimum des unsortierten Bereichs gesucht und mit dem ersten unsortierten Element getauscht. So wächst der sortierte Bereich um eins pro Durchlauf.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Ein oranger Bogen verbindet den aktuellen Minimum-Kandidaten mit dem nächsten Vergleichselement. Wird ein kleineres Element gefunden, springt der Bogen dorthin. Am Ende der Suche setzt ein roter Tausch (roter Bogen) das Minimum an die linke Grenze des unsortierten Bereichs. Die linke Seite des Arrays ist grünlich hinterlegt und wächst pro Durchlauf um genau einen Balken. Kleine Wertemarken unter den Balken erleichtern das Ablesen.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Nützlich wenn die Anzahl der Schreiboperationen (Swaps) minimiert werden soll — Selection Sort führt maximal O(n) Tauschvorgänge aus, unabhängig von den Daten. Relevant z.B. bei Flash-Speicher mit begrenzten Schreibzyklen. Für allgemeine Zwecke ist Insertion Sort vorzuziehen.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Insertion Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Adaptiv</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n²) worst · O(n) best</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Arbeitet wie das Sortieren von Spielkarten auf der Hand: Nimmt das nächste Element aus dem unsortierten Bereich und schiebt es im bereits sortierten Bereich so lange nach links, bis die richtige Position gefunden ist. Alle größeren Elemente werden dabei um eine Position nach rechts verschoben.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Ein oranger Bogen verbindet das einzufügende Element mit seinem linken Nachbarn. Ist es kleiner, wird es lila nach links verschoben — der Bogen wandert mit. Die lilafarbenen Verschiebungen zeigen, wie das Element schrittweise durch den bereits sortierten (grünlich hinterlegten) Bereich rutscht, bis es seine korrekte Position findet. Kleine Wertemarken unter den Balken helfen beim Verfolgen der Werte.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Der beste O(n²)-Algorithmus in der Praxis. Bei fast sortierten Daten läuft er nahezu in O(n). Wird oft als Basis-Algorithmus in hybriden Verfahren eingesetzt (z.B. Timsort, Introsort), die bei kleinen Teilarrays (n &lt; 16) auf Insertion Sort umschalten, weil sein geringer Overhead den asymptotischen Nachteil wettmacht.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Cocktail Shaker Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n²)</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Eine Variante von Bubble Sort, die abwechselnd in beide Richtungen durchläuft — erst von links nach rechts (große Elemente aufsteigen lassen), dann von rechts nach links (kleine Elemente absinken lassen). Die Grenzen des unsortierten Bereichs werden dabei von beiden Seiten eingeengt.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Ein oranger Bogen wandert abwechselnd nach rechts und nach links — bei einem Tausch wechselt er rot. Links und rechts wächst gleichzeitig je ein grünlich hinterlegter sortierter Bereich, der den unsortierten Mittelteil von beiden Seiten einengt. Die Bogenrichtung macht den Wechsel der Durchlaufrichtung unmittelbar sichtbar.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Löst das sogenannte „Schildkröten-Problem" von Bubble Sort: Kleine Werte am Ende des Arrays wandern bei normalem Bubble Sort nur eine Position pro Durchlauf nach vorne. Durch den Rückwärtsdurchlauf werden sie viel schneller an die richtige Position befördert. Besonders effektiv bei Arrays, die „fast sortiert" sind, aber einzelne Ausreißer in beide Richtungen haben.</p>
</div>
</div>
</div>
<!-- O(n log n) -->
<div>
<h3 class="text-xs uppercase tracking-wider text-muted font-medium mb-3">O(n log n) — Effiziente Verfahren</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Merge Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">O(n) Speicher</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log n) garantiert</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Klassisches Divide-and-Conquer: Das Array wird rekursiv in zwei Hälften geteilt, bis nur noch Einzelelemente übrig sind. Dann werden die sortierten Hälften schrittweise wieder zusammengeführt (gemergt), indem jeweils das kleinere der beiden vordersten Elemente übernommen wird. Dieser Merge-Schritt ist der Kern des Algorithmus.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Das Phase-Label über dem Canvas zeigt „Merge", sobald zwei Hälften zusammengeführt werden. Ein oranger Bogen verbindet die aktuell verglichenen Elemente aus den beiden Hälften; blaue Klammern am oberen Rand markieren den aktiven Teilbereich. Unterhalb erscheint die Speicher-Ansicht mit den beiden Hilfs-Arrays: links (blau) und rechts (lila) — ein hervorgehobener Balken zeigt den aktuellen Lesezeiger. Lila Verschiebungen im Haupt-Array zeigen, wohin die Elemente geschrieben werden. Kleine Wertemarken unter den Balken helfen, den Überblick zu behalten.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Ideal wenn eine garantierte O(n log n)-Laufzeit und Stabilität erforderlich sind — z.B. beim Sortieren von Datensätzen mit mehreren Schlüsseln oder bei verketteten Listen (dort ohne zusätzlichen Speicher möglich). Bildet die Basis von Pythons Timsort und Javas Arrays.sort() für Objekte. Der zusätzliche O(n)-Speicherbedarf ist der Hauptnachteil gegenüber Quick Sort.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Heap Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log n) garantiert</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Nutzt die Heap-Datenstruktur (Max-Heap), die direkt im Array repräsentiert wird. Phase 1 (Build-Heap): Das Array wird in O(n) in einen Max-Heap umgewandelt, bei dem jeder Elternknoten größer ist als seine Kinder. Phase 2 (Extract): Das Maximum (Wurzel) wird wiederholt mit dem letzten Element getauscht und der Heap um eins verkleinert. Dann wird die Heap-Eigenschaft durch „Heapify" (Absinken) wiederhergestellt.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Das Phase-Label wechselt sichtbar von „Heap aufbauen" zu „Extrahieren". Ein oranger Bogen verbindet den jeweils verglichenen Eltern- und Kind-Knoten im Array — die weiten Sprünge machen die Baumstruktur im flachen Array erlebbar. Beim Extrahieren wächst der grünlich hinterlegte sortierte Bereich von rechts. Unterhalb erscheint der Max-Heap als Baumdiagramm: Orange hervorgehobene Knoten zeigen den aktuellen Vergleich, der Baum schrumpft nach jedem Extract-Schritt. Kleine Wertemarken unter den Balken helfen, Eltern-Kind-Beziehungen nachzuvollziehen.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Bietet die einzigartige Kombination aus garantierter O(n log n)-Laufzeit und O(1)-Speicher — also echtes In-place ohne zusätzlichen Speicher. Ideal für speicherkritische Systeme oder als Fallback in hybriden Algorithmen (z.B. Introsort wechselt auf Heap Sort, wenn Quick Sorts Rekursionstiefe zu groß wird). In der Praxis langsamer als Quick Sort wegen schlechter Cache-Lokalität (springt weit im Array).</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Quick Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log n) avg · O(n²) worst</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Ebenfalls Divide-and-Conquer, aber anders als Merge Sort: Zuerst wird ein Pivot-Element gewählt. Dann wird das Array so umgeordnet (partitioniert), dass alle Elemente kleiner als der Pivot links stehen und alle größeren rechts. Der Pivot ist danach an seiner endgültigen Position. Dieser Vorgang wird rekursiv auf die beiden Teilarrays angewendet.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Das Phase-Label zeigt „Partition". Das Pivot-Element (letztes Element des Teilarrays) trägt einen weißen Rahmen als Markierung. Blaue Klammern am oberen Rand begrenzen den aktiven Teilbereich. Orange Bögen verbinden den aktuellen Vergleichspartner mit dem Pivot; rote Bögen markieren Tausche. Sobald der Pivot an seine endgültige Position gesetzt wird, beginnt das Muster in einem kleineren Teilbereich von vorn — so wird der Divide-and-Conquer-Charakter visuell greifbar.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> In der Praxis meistens der schnellste vergleichsbasierte Algorithmus, weil er hervorragende Cache-Lokalität hat (arbeitet sequentiell im Speicher). Wird in C's qsort(), Javas Arrays.sort() für Primitives und vielen Standard-Libraries eingesetzt. Die O(n²)-Worst-Case-Laufzeit tritt bei ungünstiger Pivot-Wahl auf (z.B. bereits sortiertes Array mit erstem Element als Pivot). Randomisierte oder Median-of-Three-Pivot-Wahl eliminiert dieses Problem praktisch.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">3-Way Quick Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Duplikateneffizient</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log n) avg · O(n) bei vielen Duplikaten</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Auch bekannt als „Dutch National Flag"-Algorithmus (Dijkstra). Im Gegensatz zum klassischen Quick Sort, der zwei Bereiche erzeugt (&lt; Pivot und &gt; Pivot), teilt diese Variante das Array in drei Bereiche: Elemente kleiner als der Pivot (links), gleich dem Pivot (Mitte) und größer als der Pivot (rechts). Alle Duplikate des Pivots landen sofort in der Mitte und werden nie wieder verglichen. Dadurch wird das Problem vieler gleicher Werte elegant gelöst — klassischer Quick Sort degeneriert dabei zu O(n²).</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Drei farbige Hintergrundbereiche machen die Dreiteilung sichtbar: links (blau) Elemente kleiner als Pivot, Mitte (orange) gleich dem Pivot, rechts (lila) größer als Pivot. Orange Bögen zeigen Vergleiche gegen den Pivot; rote Bögen markieren Tausche, die Elemente in den richtigen Bereich befördern. Der mittlere Gleichheits-Bereich wächst bei Duplikaten spürbar schnell — er wird bei der Rekursion komplett übersprungen.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Immer wenn die Daten viele Duplikate enthalten — z.B. beim Sortieren nach Ländercodes, Booleschen Werten, Altersgruppen oder Bewertungsstufen. Klassischer Quick Sort braucht bei n identischen Elementen O(n²), 3-Way Quick Sort nur O(n). Die Median-of-Three-Pivot-Wahl verhindert zusätzlich den Worst Case bei bereits sortierten Daten.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Dual-Pivot Quick Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Java Standard</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log n) avg</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Entwickelt von Vladimir Yaroslavskiy (2009), seit Java 7 der Standard-Algorithmus für Arrays.sort() bei primitiven Typen. Verwendet zwei Pivot-Elemente (P1 ≤ P2) und teilt das Array in drei Bereiche: Elemente &lt; P1 (links), Elemente zwischen P1 und P2 (Mitte), Elemente &gt; P2 (rechts). Die drei Bereiche werden dann rekursiv sortiert. Durch die feinere Aufteilung werden die Teilarrays kleiner als bei einfachem Quick Sort.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Beide Pivot-Elemente (P1 links, P2 rechts) tragen einen weißen Rahmen. Blaue Klammern markieren den aktiven Teilbereich. Drei farbige Hintergrundbereiche entstehen schrittweise: links (blau) kleiner P1, Mitte (orange) zwischen P1 und P2, rechts (lila) größer P2. Orange Bögen zeigen Vergleiche, rote Bögen die Tausche in die richtigen Bereiche. Dass der aktive Bereich sich durch die Dreiteilung deutlich schneller verkleinert als beim klassischen Quick Sort, ist am Fortschrittsbalken ablesbar.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Der Industriestandard in Java für primitive Typen — in der Praxis oft 1020 % schneller als klassischer Quick Sort, weil die feinere Partitionierung die Cache-Ausnutzung verbessert. Besonders effektiv bei großen Arrays mit gleichmäßig verteilten Werten. Die Implementierung ist komplexer als klassischer Quick Sort, lohnt sich aber bei performancekritischen Anwendungen.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Introsort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Hybrid</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">C++/Rust Standard</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log n) garantiert</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Ein hybrider Algorithmus (David Musser, 1997), der das Beste dreier Welten kombiniert: Er startet als Quick Sort mit Median-of-Three-Pivot für die durchschnittlich beste Performance. Überschreitet die Rekursionstiefe 2·log₂(n), wechselt er auf Heap Sort — das garantiert O(n log n) auch im Worst Case. Für kleine Teilarrays (≤ 16 Elemente) wird Insertion Sort verwendet, da dieser bei wenigen Elementen wegen geringerem Overhead schneller ist. So wird Quick Sorts O(n²)-Schwäche eliminiert, ohne auf seine Durchschnitts-Performance zu verzichten.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Das Phase-Label über dem Canvas verrät stets, welche Strategie gerade aktiv ist: „Quick Sort" bei der Partitionierung (Pivot mit weißem Rahmen, blaue Bereichsklammern, orange/rote Bögen), „Insertion Sort" bei kleinen Teilarrays ≤ 16 (lila Verschiebungen benachbarter Elemente ohne Pivot), „Heap Sort (Fallback)" wenn die Rekursionstiefe überschritten wird (weite Sprünge, kein Pivot-Marker). Dieser automatische Strategiewechsel, der in der Phase-Anzeige direkt ablesbar ist, macht Introsort visuell einzigartig unter allen hier gezeigten Algorithmen.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Der Standard-Algorithmus in C++ (std::sort), Rust (slice::sort_unstable) und vielen weiteren Systemen. Introsort ist die Antwort auf die Frage „Welchen Algorithmus nehme ich, wenn ich nicht weiß, wie die Daten aussehen?" — er ist universell einsetzbar mit garantierter O(n log n)-Laufzeit und minimalem Speicherbedarf. Zusammen mit Timsort (Python, Java für Objekte) deckt er die beiden wichtigsten Industrie-Standards ab.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Shell Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Adaptiv</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log²n) mit Knuth</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Eine Verallgemeinerung von Insertion Sort. Statt nur benachbarte Elemente zu vergleichen, werden zunächst weit auseinanderliegende Elemente verglichen und sortiert (mit großem „Gap"). Der Gap wird schrittweise verkleinert (hier nach Knuth: ..., 40, 13, 4, 1). In jeder Runde wird ein Insertion Sort auf die Elemente mit dem jeweiligen Abstand ausgeführt. Beim letzten Durchlauf (Gap=1) ist es ein normaler Insertion Sort — aber auf fast sortierte Daten, was ihn extrem schnell macht.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Oben rechts im Canvas zeigt eine Beschriftung den aktuellen Gap-Wert. Zu Beginn überspannen die orangen Bögen weite Distanzen — ein großer Gap bedeutet, dass weit auseinanderliegende Balken verglichen werden. Mit jedem Durchlauf schrumpft der Gap (und damit die Bogenlänge) sichtbar: 40 → 13 → 4 → 1. Im letzten Durchlauf (Gap=1) sind die Bögen kurz wie bei Insertion Sort — aber die Daten sind bereits so gut vorsortiert, dass kaum noch lila Verschiebungen nötig sind.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Ideal wenn ein einfacher, rekursionsfreier Algorithmus benötigt wird, der deutlich besser als O(n²) performt. Kein Stack-Overhead, wenig Code, exzellente Cache-Lokalität. Wird häufig in Embedded Systems und in der Linux-Kernel-Implementierung verwendet. Die exakte Komplexität hängt von der Gap-Sequenz ab — kein Beweis für eine optimale Folge ist bekannt, was Shell Sort theoretisch besonders interessant macht.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Tree Sort (BST)</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil (bei gleichen rechts)</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">O(n) Speicher</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Baumbasiert</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log n) avg · O(n²) worst</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Baut einen binären Suchbaum (BST) auf, indem jedes Array-Element nacheinander eingefügt wird. Im BST gilt: Alle Werte im linken Teilbaum eines Knotens sind kleiner, alle im rechten sind größer oder gleich. Zum Einfügen wird der Baum von der Wurzel abwärts durchlaufen — bei jedem Knoten wird entschieden, ob links oder rechts weitergesucht wird. Nach dem Aufbau werden die Elemente durch eine In-Order-Traversierung (links → Knoten → rechts) in sortierter Reihenfolge zurück ins Array geschrieben.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Oben wird das aktuelle Array-Element lila hervorgehoben, das gerade in den Baum eingefügt wird. Unterhalb wächst der Binäre Suchbaum Knoten für Knoten — der orange leuchtende Pfad zeigt die Suchroute von der Wurzel bis zur Einfügeposition. In der zweiten Phase (In-Order-Traversal) werden die Knoten nacheinander orange markiert und ihre Werte lila ins Haupt-Array zurückgeschrieben. Die Schritt-Erklärung über dem Canvas gibt bei jeder Verschiebung die genaue Zielposition an.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Zeigt den fundamentalen Zusammenhang zwischen Datenstrukturen und Sortieren — ein BST ist im Kern ein „lebendiger" Sortieralgorithmus. Bei balancierten Daten O(n log n), aber bei bereits sortierten Eingaben degeneriert der Baum zu einer linearen Kette → O(n²). Selbstbalancierende Bäume (AVL, Rot-Schwarz) lösen dieses Problem, sind aber komplexer. In der Praxis wird Tree Sort selten direkt verwendet, aber das Verständnis ist essentiell für Datenbanken (B-Bäume), Sets und Maps.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Timsort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Adaptiv</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Hybrid</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">O(n) Speicher</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log n) garantiert</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Vereint das Beste aus Insertion Sort und Merge Sort. Phase 1: Das Array wird in „Runs" (Teilstücke der Größe 32) aufgeteilt und jeder Run mit Insertion Sort sortiert — das ist bei kleinen Datenmengen schneller als Merge Sort wegen geringerem Overhead. Phase 2: Die sortierten Runs werden dann bottom-up mit einem optimierten Merge-Verfahren zusammengeführt, wobei die Run-Größe in jeder Runde verdoppelt wird (32 → 64 → 128 → ...). Die echte Timsort-Implementierung erkennt zusätzlich natürlich vorhandene sortierte Teilstücke (natürliche Runs) und nutzt sie aus.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Das Phase-Label wechselt zwischen den beiden Phasen: In Phase 1 sehen Sie nur das Haupt-Array mit kurzen orangen Bögen und lila Verschiebungen innerhalb kleiner 32-Elemente-Blöcke (Insertion Sort). Dünne vertikale Linien im Balkendiagramm markieren die Run-Grenzen und machen sichtbar, wie das Array in sortierte Blöcke aufgeteilt wird. Sobald Phase 2 beginnt, erscheint die Speicher-Ansicht mit den Hilfs-Arrays (blau links, lila rechts) — orangere Bögen über größere Distanzen zeigen, wie die Blöcke von 32 auf 64, 128 usw. anwachsen.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Der Standard-Sortieralgorithmus in Python (seit 2002), Java (Arrays.sort für Objekte), Android, Swift und V8 (JavaScript-Engine von Chrome). Wurde von Tim Peters speziell für reale Daten entwickelt, die oft bereits teilweise sortiert sind. Bei fast sortierten Daten läuft Timsort nahezu in O(n), bei zufälligen Daten zuverlässig in O(n log n). Wenn man nur einen einzigen Algorithmus kennen sollte, der „in der echten Welt" verwendet wird, ist es Timsort.</p>
</div>
</div>
</div>
<!-- Parallel -->
<!-- O(n·k) -->
<div>
<h3 class="text-xs uppercase tracking-wider text-muted font-medium mb-3">O(n·k) — Nicht-vergleichsbasiert</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Counting Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">O(n + k) Speicher</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n + k)</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Durchbricht die theoretische Untergrenze O(n log n) für vergleichsbasierte Verfahren, indem er gar nicht vergleicht. Stattdessen wird ein Hilfsarray (Größe k = Wertebereich) angelegt und für jeden Wert gezählt, wie oft er vorkommt. Aus den Häufigkeiten lässt sich direkt die sortierte Reihenfolge ableiten: Jeder Wert wird entsprechend seiner Häufigkeit zurückgeschrieben.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Oben werden Elemente lila hervorgehoben, wenn sie gezählt bzw. zurückgeschrieben werden — da Counting Sort nicht vergleicht, gibt es keine orangen Bögen. Unterhalb erscheint das Häufigkeits-Array als grünes Balkendiagramm: Jeder Balken repräsentiert einen Wert, die Höhe zeigt die Häufigkeit. Der aktuell bearbeitete Wert leuchtet lila. Beim Zurückschreiben schrumpfen die Häufigkeitsbalken, während das Haupt-Array von links nach rechts sortiert befüllt wird. Kleine Wertemarken unter den Balken zeigen die ganzzahligen Werte, auf die die Eingabe intern gerundet wird.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Extrem schnell wenn der Wertebereich k klein ist relativ zu n (z.B. Noten 16, Alter 0150, Buchstaben). Wird linear langsamer wenn k wächst — bei k ≫ n ist Counting Sort ineffizienter als vergleichsbasierte Verfahren. Dient als stabile Unterroutine in Radix Sort. Kann nur ganzzahlige oder diskrete Werte sortieren.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Radix Sort (LSD)</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">O(n + k) Speicher</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(d · (n + k))</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Sortiert Zahlen stellenweise, beginnend bei der niederwertigsten Stelle (Least Significant Digit, LSD). In jeder Runde wird ein stabiler Sortieralgorithmus (hier Counting Sort mit Basis k=10) auf eine einzelne Dezimalstelle angewendet. Durch die Stabilität bleiben die Ergebnisse vorheriger Runden erhalten. Nach d Runden (d = Anzahl der Stellen der größten Zahl) ist das gesamte Array sortiert.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Das Phase-Label zeigt die aktuelle Stelle und Richtung, z.B. „Stelle 1 — Verteilen" und „Stelle 1 — Sammeln". Oben werden Elemente lila markiert (kein Vergleich → kein Bogen). Unterhalb zeigt die Speicher-Ansicht die 10 Radix-Buckets (Ziffern 09) als Spalten mit gestapelten Blöcken — jeder Block enthält den Zahlenwert. Der aktive Bucket leuchtet lila. Pro Stellendurchlauf füllen sich die Buckets und werden anschließend der Reihe nach ins Array zurückgeschrieben. Da intern mit ganzen Zahlen gearbeitet wird, zeigen die Wertemarken gerundete Werte.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Schlägt vergleichsbasierte Verfahren wenn die Zahl der Stellen d konstant oder klein ist — z.B. Postleitzahlen (5-stellig), IP-Adressen, Telefonnummern, Datums-Timestamps. Die effektive Laufzeit ist dann O(n). Kann auch auf Strings gleicher Länge angewandt werden (zeichenweises Sortieren). Nicht geeignet für Fließkommazahlen oder stark variierende Schlüssellängen.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Bucket Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil (bei stabilem Sub-Sort)</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">O(n + k) Speicher</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n + k) avg · O(n²) worst</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Teilt den Wertebereich in eine Anzahl gleich großer „Eimer" (Buckets) auf. Jedes Element wird anhand seines Wertes in den passenden Eimer einsortiert. Dann wird jeder Eimer einzeln sortiert (hier mit Insertion Sort, da die Eimer typischerweise klein sind). Zuletzt werden die Eimer der Reihe nach zusammengefügt. Die Anzahl der Buckets wird hier als √n gewählt.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Oben wird jedes Element lila markiert, wenn es in einen Bucket verteilt oder ins Array zurückgeschrieben wird (kein Vergleich → kein Bogen). Unterhalb zeigt die Speicher-Ansicht die k Buckets als Rahmen mit Balken darin — die Balkenhöhe entspricht dem Wert des Elements. Der aktive Bucket leuchtet lila. Sie sehen, wie sich die Elemente auf die Buckets verteilen, und beim Zurückschreiben, wie ein gleichmäßig befülltes Array entsteht. Kleine Wertemarken unter den Balken zeigen Fließkommazahlen, die direkt in Buckets einsortiert werden.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Am effizientesten wenn die Eingabedaten gleichmäßig über den Wertebereich verteilt sind — dann enthält jeder Bucket nur wenige Elemente und der Gesamt-Aufwand ist O(n). Ideal für Fließkommazahlen im Bereich [0, 1), Prüfungsnoten, Messwerte mit bekanntem Bereich. Wenn alle Elemente in denselben Bucket fallen (z.B. viele identische Werte), degeneriert Bucket Sort zum Sub-Sort-Algorithmus. Konzeptionell das intuitivste nicht-vergleichsbasierte Verfahren — man denkt automatisch an Schubladen oder Fächer.</p>
</div>
</div>
</div>
<!-- Spezial -->
<div>
<h3 class="text-xs uppercase tracking-wider text-muted font-medium mb-3">Spezial — Ungewöhnliche Verfahren</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-3">
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Pancake Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n²)</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Die einzige erlaubte Operation ist ein „Präfix-Flip": Man schiebt einen Pfannenwender an einer beliebigen Stelle unter den Pfannkuchenstapel und dreht alles darüber um. Das Verfahren arbeitet von hinten nach vorne: In jedem Schritt wird das Maximum im unsortierten Bereich gefunden. Dann wird zuerst ein Flip bis zu diesem Maximum ausgeführt (Maximum landet ganz vorne), dann ein Flip bis zur Endposition (Maximum landet an der richtigen Stelle). So wandert pro Runde ein Element an seine endgültige Position.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Zuerst wandert ein oranger Bogen durch den unsortierten Bereich auf der Suche nach dem Maximum. Dann folgt ein spektakulärer Flip: Mehrere Balken werden gleichzeitig rot — ein ganzer Präfix des Arrays wird auf einmal umgekehrt. Zwei solcher Flips pro Runde erzeugen das charakteristische Doppel-Umkehrmuster: erst bringt ein Flip das Maximum nach ganz links, dann schiebt ein zweiter es an seine Zielposition am rechten Ende des unsortierten Bereichs.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Löst ein berühmtes kombinatorisches Problem: „Wie viele Flips braucht man im schlimmsten Fall, um n Pfannkuchen zu sortieren?" (Pancake-Zahl). Bill Gates' erste wissenschaftliche Veröffentlichung (1979) bewies eine obere Schranke dafür. Praxisrelevant in der Bioinformatik: Genom-Umordnungen (Inversionen von DNA-Segmenten) entsprechen exakt Pancake-Flips. Auch in der Robotik relevant, wenn ein Roboter nur „Umdreh"-Operationen ausführen kann. Visuell besonders eindrucksvoll, weil die Flips ganze Abschnitte auf einmal umkehren.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Cycle Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Min. Schreibops</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n²)</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Basiert auf der Zerlegung einer Permutation in Zyklen. Für jedes Element wird berechnet, wo es in der sortierten Reihenfolge stehen müsste (durch Zählen aller kleineren Elemente). Dann wird es dorthin verschoben, wobei das verdrängte Element aufgenommen und ebenfalls an seine korrekte Position gebracht wird — bis der Zyklus sich schließt (man wieder am Startpunkt ankommt). Jedes Element wird dabei höchstens einmal an seine endgültige Position geschrieben.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Ein langer Scan: Viele orange Bögen erscheinen nacheinander, während der Algorithmus zählt, wie viele Elemente kleiner als das aktuelle sind. Dann folgt eine einzelne lila Verschiebung — das Element springt direkt an seine berechnete Zielposition, ohne einen Umweg. Dieser Wechsel aus vielen kurzen Bögen (Zählphase) und einem einzigen punktuellen Schreibvorgang macht die Eigenheit von Cycle Sort unmittelbar erlebbar: O(n²) Vergleiche für minimale Schreiboperationen. Bereits an ihrer Zielposition stehende Elemente sind grünlich hinterlegt; die Wertemarken unter den Balken helfen, die berechneten Zielpositionen nachzuvollziehen.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Der einzige Sortieralgorithmus, der die theoretisch minimale Anzahl an Schreiboperationen garantiert. Jedes Element wird höchstens einmal geschrieben (statt mehrfach verschoben wie bei anderen Algorithmen). Essenziell bei EEPROM oder Flash-Speicher, wo jeder Schreibvorgang den Speicher abnutzt und die Lebensdauer verkürzt. Auch nützlich zum Erkennen, ob eine Permutation aus einem einzigen Zyklus besteht. Die vielen Vergleiche (O(n²)) sind der Preis für die minimalen Schreiboperationen.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Bogo Sort</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Nicht stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-cSwap/10 text-cSwap border border-cSwap/20">Max. 8 Elemente</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n · n!) erwartet</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Der „dümmste" Sortieralgorithmus: Das Array wird zufällig gemischt (Fisher-Yates-Shuffle), dann wird geprüft, ob es jetzt zufällig sortiert ist. Wenn nicht, wird erneut gemischt. Das wird wiederholt, bis das Array durch Zufall in der richtigen Reihenfolge landet. Es gibt keine Garantie, dass das jemals passiert — im Erwartungswert braucht man n! Versuche (bei n = 8 sind das 40.320 Mischvorgänge).</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Bogo Sort erzeugt ein hektisches Blitz-Muster: Das gesamte Array blinkt rot auf (Shuffle), dann wandern kurze orange Bögen von links nach rechts durch das Array (Sortiertheitscheck). Scheitert die Prüfung, folgt sofort der nächste rote Shuffle. Die kleinen Wertemarken unter den Balken zeigen bei jedem Shuffle-Versuch eine neue zufällige Anordnung. Der Fortschrittsbalken bewegt sich kaum — tausende Schritte bei nur 8 Elementen zeigen eindrücklich, wie nutzlos rein zufälliges Probieren ist. Die Array-Größe ist automatisch auf 8 begrenzt.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Niemals in der Praxis. Bogo Sort dient als Lehr- und Abschreckungsbeispiel: Er zeigt eindrücklich, warum zufälliges Probieren exponentiell schlecht skaliert. Bei nur 8 Elementen braucht er im Schnitt ~40.000 Versuche, bei 10 Elementen schon ~3,6 Millionen. Der Name ist ein Wortspiel auf „bogus" (falsch/wertlos). In der theoretischen Informatik dient er als Referenz für die schlechteste denkbare Komplexitätsklasse. Die Array-Größe wird hier automatisch auf 8 begrenzt, um den Browser nicht einzufrieren.</p>
</div>
<div class="bg-surface2 rounded-lg p-4 info-card">
<h3 class="text-sm font-semibold text-accent mb-2">Fisher-Yates Shuffle</h3>
<div class="flex flex-wrap gap-2 mb-2">
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">Stabil</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-accent/10 text-accent border border-accent/20">In-place</span>
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n) Zeit · O(1) Speicher</span>
</div>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Der Fisher-Yates-Algorithmus (auch Knuth-Shuffle) mischt ein Array uniform zufällig. Er läuft rückwärts: Für jede Position i wird ein zufälliger Index j zwischen 0 und i gewählt und die beiden Elemente getauscht. Jede der n! Permutationen wird mit gleicher Wahrscheinlichkeit erzeugt.</p>
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Rote Bögen zeigen jeden Tausch — keine Vergleiche, nur Tausche. Die Anzahl der Tausche ist immer n-1 (bei n Elementen). Das macht die Effizienz des Mischens im Vergleich zum Sortieren direkt sichtbar.</p>
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Zum Erzeugen von Testdaten, zum „Nochmal-Mischen" eines sortierten Arrays, oder als Gegenbeweis: Ein einziger Shuffle macht jede Sortierung rückgängig. Mischen ist O(n), Sortieren ist O(n log n) — also ist Mischen immer schneller als Sortieren.</p>
</div>
</div>
</div>
</div>
</details>
<!-- Bedienung (collapsible) -->
<details class="bg-surface border border-border rounded-xl group">
<summary class="p-4 sm:p-5 cursor-pointer select-none flex items-center gap-2 text-sm font-semibold text-gray-200 hover:text-white transition-colors">
<i data-lucide="gamepad-2" class="w-4 h-4 text-accent"></i>
Bedienung
<i data-lucide="chevron-down" class="w-4 h-4 text-muted ml-auto chevron-icon"></i>
</summary>
<div class="px-4 sm:px-5 pb-4 sm:pb-5 pt-0 space-y-5">
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Was ist das?</strong> Dieses Programm visualisiert die Funktionsweise von 20 Sortieralgorithmen — von Bubble Sort bis Fisher-Yates Shuffle. Es zeigt Schritt für Schritt, wie ein Algorithmus ein Array sortiert, macht die Datenstrukturen und Speichernutzung sichtbar und vermittelt ein intuitives Verständnis dafür, warum der eine Algorithmus schneller ist als der andere.</p>
<!-- Quick-Start -->
<div class="bg-surface2/50 rounded-lg p-3 border border-border/50">
<p class="text-[11px] uppercase tracking-wider text-muted font-medium mb-2">Quick-Start</p>
<ol class="text-xs text-muted leading-relaxed space-y-1.5">
<li class="flex gap-2"><span class="text-accent font-bold shrink-0">1.</span> <span>Wähle einen Algorithmus aus dem Dropdown-Menü.</span></li>
<li class="flex gap-2"><span class="text-accent font-bold shrink-0">2.</span> <span>Stelle die Array-Größe und Geschwindigkeit ein.</span></li>
<li class="flex gap-2"><span class="text-accent font-bold shrink-0">3.</span> <span>Drücke <span class="inline-block px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20 text-[10px] font-mono">Space</span> zum Starten — oder nutze die Schritt-für-Schritt-Navigation.</span></li>
</ol>
</div>
<!-- Steuerung -->
<div class="bg-surface2/50 rounded-lg p-3 border border-border/50">
<p class="text-[11px] uppercase tracking-wider text-muted font-medium mb-2">Steuerung</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-2 text-xs text-muted leading-relaxed">
<div class="flex items-center gap-2">
<span class="inline-block px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20 text-[10px] font-mono shrink-0"></span>
<span>Play / Pause — Animation starten oder anhalten</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20 text-[10px] font-mono shrink-0"></span>
<span>Ein Schritt vor — nächste Operation</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20 text-[10px] font-mono shrink-0"></span>
<span>Ein Schritt zurück — vorherige Operation</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20 text-[10px] font-mono shrink-0"></span>
<span>Reset — Array in Ausgangszustand zurücksetzen</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20 text-[10px] font-mono shrink-0">🔀</span>
<span>Mischen — Fisher-Yates Shuffle auf aktuelles Array</span>
</div>
<div class="flex items-center gap-2">
<span class="inline-block px-1.5 py-0.5 rounded bg-accent/10 text-accent border border-accent/20 text-[10px] font-mono shrink-0">🌙/☀</span>
<span>Theme — zwischen hellem und dunklem Modus wechseln</span>
</div>
</div>
</div>
<!-- Tastaturkürzel -->
<div class="bg-surface2/50 rounded-lg p-3 border border-border/50">
<p class="text-[11px] uppercase tracking-wider text-muted font-medium mb-2">Tastaturkürzel</p>
<div class="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1.5 text-xs text-muted leading-relaxed">
<div class="flex items-center gap-1.5"><kbd class="px-1.5 py-0.5 rounded bg-surface border border-border text-[10px] font-mono text-gray-300">Space</kbd> <span>Play/Pause</span></div>
<div class="flex items-center gap-1.5"><kbd class="px-1.5 py-0.5 rounded bg-surface border border-border text-[10px] font-mono text-gray-300">K</kbd> <span>Play/Pause</span></div>
<div class="flex items-center gap-1.5"><kbd class="px-1.5 py-0.5 rounded bg-surface border border-border text-[10px] font-mono text-gray-300"></kbd> <span>Schritt zurück</span></div>
<div class="flex items-center gap-1.5"><kbd class="px-1.5 py-0.5 rounded bg-surface border border-border text-[10px] font-mono text-gray-300">J</kbd> <span>Schritt zurück</span></div>
<div class="flex items-center gap-1.5"><kbd class="px-1.5 py-0.5 rounded bg-surface border border-border text-[10px] font-mono text-gray-300"></kbd> <span>Schritt vor</span></div>
<div class="flex items-center gap-1.5"><kbd class="px-1.5 py-0.5 rounded bg-surface border border-border text-[10px] font-mono text-gray-300">L</kbd> <span>Schritt vor</span></div>
<div class="flex items-center gap-1.5"><kbd class="px-1.5 py-0.5 rounded bg-surface border border-border text-[10px] font-mono text-gray-300">R</kbd> <span>Reset</span></div>
<div class="flex items-center gap-1.5"><kbd class="px-1.5 py-0.5 rounded bg-surface border border-border text-[10px] font-mono text-gray-300">T</kbd> <span>Theme</span></div>
<div class="flex items-center gap-1.5"><kbd class="px-1.5 py-0.5 rounded bg-surface border border-border text-[10px] font-mono text-gray-300">S</kbd> <span>Shuffle</span></div>
</div>
</div>
<!-- Presets -->
<div class="bg-surface2/50 rounded-lg p-3 border border-border/50">
<p class="text-[11px] uppercase tracking-wider text-muted font-medium mb-2">Datensätze</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1.5 text-xs text-muted leading-relaxed">
<div><span class="text-gray-300 font-medium">Zufällig</span> — zufällig verteilte Werte, der Standard-Testfall.</div>
<div><span class="text-gray-300 font-medium">Sortiert</span> — bereits in aufsteigender Reihenfolge. Manche Algorithmen (z.B. Quick Sort mit erstem Element als Pivot) werden dadurch extrem langsam.</div>
<div><span class="text-gray-300 font-medium">Umgekehrt</span> — absteigend sortiert. Das Gegenstück zu „Sortiert" und ein公平er Test für die meisten Algorithmen.</div>
<div><span class="text-gray-300 font-medium">Fast sortiert</span> — nur wenige Elemente sind an der falschen Stelle. Adaptiven Algorithmen (Insertion Sort, Timsort) spielen hier ihre Stärke aus.</div>
<div><span class="text-gray-300 font-medium">Duplikate</span> — viele gleiche Werte. Das ist der Härtetest für Algorithmen wie Quick Sort (degeneriert zu O(n²)), während 3-Way Quick Sort hier brilliert.</div>
</div>
</div>
<!-- Warum Sortieralgorithmen? -->
<div class="bg-surface2/50 rounded-lg p-3 border border-border/50">
<p class="text-[11px] uppercase tracking-wider text-muted font-medium mb-2">Warum sind Sortieralgorithmen wichtig?</p>
<p class="text-xs text-muted leading-relaxed mb-2">Sortieren ist eines der am intensivsten erforschten Probleme der Informatik — nicht aus akademischer Überheblichkeit, sondern weil es in der Praxis überall vorkommt: Datenbankabfragen müssen Ergebnisse sortieren, Betriebssysteme verwalten Prioritätswarteschlangen, Compiler nutzen Sortieralgorithmen für die Optimierung. Die Wahl des richtigen Algorithmus kann den Unterschied ausmachen zwischen einer Antwort in Millisekunden und in Minuten.</p>
<p class="text-xs text-muted leading-relaxed mb-2">Jeder Algorithmus repräsentiert einen grundlegenden Kompromiss: Manche sind einfach zu verstehen (Bubble Sort), andere extrem schnell im Durchschnitt (Quick Sort), wieder andere garantieren auch im schlimmsten Fall gute Leistung (Heap Sort). Wer diese Kompromisse versteht, kann für jede Situation die richtige Wahl treffen — sei es beim Sortieren von 100 Datensätzen oder 100 Millionen.</p>
<p class="text-xs text-muted leading-relaxed">Dieses Programm hilft dabei, ein intuitives Gespür für diese Kompromisse zu entwickeln. Beobachten Sie, wie Bubble Sort bei sortierten Daten plötzlich in O(n) abschließt, während er bei umgekehrt sortierten Daten genauso langsam ist wie immer. Oder wie Timsort die Ordnung in fast-sortierten Daten sofort erkennt und ausnutzt. Visuelles Lernen macht sichtbar, was Paper und Pseudocode nur schwer vermitteln.</p>
</div>
</div>
</details>
</div>
<script>
// ================================================================
// Constants — dynamic from CSS Custom Properties
// ================================================================
function getCSS(prop) {
return getComputedStyle(document.documentElement).getPropertyValue(prop).trim();
}
function refreshColors() {
COLORS.normal = getCSS('--c-normal') || '#4a7cff';
COLORS.compare = getCSS('--c-compare') || '#ff9a3c';
COLORS.swap = getCSS('--c-swap') || '#ff3c5a';
COLORS.move = getCSS('--c-move') || '#b94aff';
COLORS.sorted = getCSS('--c-sorted') || '#3cffa0';
COLORS.axis = getCSS('--c-axis') || '#262a3d';
COLORS.text = getCSS('--c-bartext') || '#e0e4f0';
COLORS.muted = getCSS('--c-muted') || '#6b7394';
COLORS.bg = getCSS('--c-bg') || '#0b0d14';
}
const COLORS = {
normal: '#4a7cff',
compare: '#ff9a3c',
swap: '#ff3c5a',
move: '#b94aff',
sorted: '#3cffa0',
axis: '#262a3d',
text: '#e0e4f0',
muted: '#6b7394',
bg: '#0b0d14',
};
// ================================================================
// DOM References
// ================================================================
const canvas = document.getElementById('sortCanvas');
const ctx = canvas.getContext('2d');
const memCanvas = document.getElementById('memCanvas');
const memCtx = memCanvas.getContext('2d');
const $memContainer = document.getElementById('memViewContainer');
const $memLabel = document.getElementById('memViewLabel');
const $algoSelect = document.getElementById('algoSelect');
const $presetSelect = document.getElementById('presetSelect');
const $customArray = document.getElementById('customArray');
const $btnClearCustom = document.getElementById('btnClearCustom');
const $btnPasteCustom = document.getElementById('btnPasteCustom');
const $customArrayCount = document.getElementById('customArrayCount');
const $sizeSlider = document.getElementById('sizeSlider');
const $speedSlider = document.getElementById('speedSlider');
const $sizeVal = document.getElementById('sizeVal');
const $speedVal = document.getElementById('speedVal');
const $statComp = document.getElementById('statCompares');
const $statSwap = document.getElementById('statSwaps');
const $statMove = document.getElementById('statMoves');
const $statStep = document.getElementById('statStep');
const $statTime = document.getElementById('statTime');
const $btnPlayPause = document.getElementById('btnPlayPause');
const $btnStepBack = document.getElementById('btnStepBack');
const $btnStep = document.getElementById('btnStep');
const $btnReset = document.getElementById('btnReset');
const $btnShuffle = document.getElementById('btnShuffle');
const $btnTheme = document.getElementById('btnTheme');
const $progressFill = document.getElementById('progressFill');
const $stepCounter = document.getElementById('stepCounter');
const $phaseLabel = document.getElementById('phaseLabel');
const $stepExplanation = document.getElementById('stepExplanation');
// ================================================================
// Mutable State
// ================================================================
let array = [];
let steps = [];
let stepIndex = 0;
let animTimer = null;
let isRunning = false;
let isPaused = false;
let arrayFresh = false; // true after Reset generated a new array, reset to false when sort starts
let startTime = 0; // performance.now() when animation started
let elapsedMs = 0; // accumulated ms across pauses
// ================================================================
// UI Helpers
// ================================================================
function getDelay() {
const v = parseInt($speedSlider.value, 10);
// FIX: Logarithmische Skala für bessere UX im langsamen Bereich
return Math.round(1000 * Math.pow(0.001, (v - 1) / 99));
}
function updateSpeedLabel() {
$speedVal.textContent = getDelay() + ' ms/Schritt';
}
function updateSizeLabel() {
$sizeVal.textContent = $sizeSlider.value;
}
// ================================================================
// Algorithmen mit Memory-Visualisierung
// ================================================================
const MEM_ALGOS = new Set(['tree', 'merge', 'timsort', 'heap', 'counting', 'radix', 'bucket']);
function setStats(compares, swaps, moves, step) {
$statComp.textContent = String(compares);
$statSwap.textContent = String(swaps);
$statMove.textContent = String(moves);
$statStep.textContent = String(step);
}
function updateButtonStates() {
const playing = isRunning && !isPaused;
const ppPlay = $btnPlayPause.querySelector('.pp-play');
const ppPause = $btnPlayPause.querySelector('.pp-pause');
if (playing) {
// Show pause icon
ppPlay.classList.add('hidden');
ppPause.classList.remove('hidden');
$btnPlayPause.className = 'flex items-center justify-center bg-accent border border-accent text-white rounded-lg py-2.5 text-sm font-medium active:scale-95 transition-all min-h-[44px] shadow-lg shadow-accent/20';
} else {
// Show play icon
ppPlay.classList.remove('hidden');
ppPause.classList.add('hidden');
if (isRunning && isPaused) {
$btnPlayPause.className = 'flex items-center justify-center bg-cCompare/20 border border-cCompare text-cCompare rounded-lg py-2.5 text-sm font-medium hover:bg-accent hover:text-white active:scale-95 transition-all min-h-[44px] shadow-lg shadow-cCompare/15';
} else {
$btnPlayPause.className = 'flex items-center justify-center bg-accent/10 border border-accent text-accent rounded-lg py-2.5 text-sm font-medium hover:bg-accent hover:text-white active:scale-95 transition-all min-h-[44px]';
}
}
}
// ================================================================
// Theme Toggle
// ================================================================
function applyTheme(dark, skipRender) {
const root = document.documentElement;
if (dark) {
root.classList.add('dark');
} else {
root.classList.remove('dark');
}
// Update theme icon visibility
const darkIcon = $btnTheme.querySelector('.dark-icon');
const lightIcon = $btnTheme.querySelector('.light-icon');
if (darkIcon && lightIcon) {
if (dark) {
darkIcon.classList.remove('hidden');
lightIcon.classList.add('hidden');
} else {
darkIcon.classList.add('hidden');
lightIcon.classList.remove('hidden');
}
}
if (skipRender) return;
// Refresh canvas colors after CSS vars change
requestAnimationFrame(function() {
refreshColors();
if (isRunning && steps.length > 0 && stepIndex > 0) {
renderStep(steps[stepIndex - 1], stepIndex);
} else if (array.length > 0) {
renderCurrent();
}
});
}
// initTheme is called after lucide.createIcons() in initialization block
// ================================================================
// Progress, Phase, Explanation
// ================================================================
function updateProgress(current, total) {
const pct = total > 0 ? (current / total) * 100 : 0;
$progressFill.style.width = pct + '%';
$stepCounter.textContent = current + ' / ' + total;
}
function updatePhaseLabel(meta) {
if (meta && meta.phase) {
$phaseLabel.textContent = meta.phase;
} else {
$phaseLabel.innerHTML = '&nbsp;';
}
}
function buildExplanation(step) {
if (!step) return '\u00A0';
const idx = step.indices;
const arr = step.array;
function fmtVal(i) {
let v = arr[i];
return Number.isInteger(v) ? String(v) : v.toFixed(1);
}
switch (step.type) {
case 'compare':
if (idx.length >= 2)
return 'Vergleiche arr[' + idx[0] + ']=' + fmtVal(idx[0]) + ' mit arr[' + idx[1] + ']=' + fmtVal(idx[1]);
return 'Vergleich';
case 'swap':
if (idx.length >= 2)
return 'Tausche arr[' + idx[0] + ']=' + fmtVal(idx[0]) + ' \u2194 arr[' + idx[1] + ']=' + fmtVal(idx[1]);
return 'Tausch';
case 'move':
if (idx.length >= 1)
return 'Verschiebe \u2192 Position ' + idx[0];
return 'Verschiebung';
case 'done':
return 'Sortierung abgeschlossen \u2713';
default:
return '\u00A0';
}
}
// ================================================================
// Data Generation
// ================================================================
function generateArray() {
// Custom array input takes priority
const customText = $customArray.value.trim();
if (customText) {
const parsed = customText.split(/[,;\s]+/).map(Number).filter(function(v) { return !isNaN(v); });
if (parsed.length > 0) {
array = parsed;
$sizeSlider.disabled = true;
return;
}
}
$sizeSlider.disabled = false;
let n = parseInt($sizeSlider.value, 10);
// Bogo Sort: Array-Größe auf 8 begrenzen (O(n·n!) explodiert sonst)
if ($algoSelect.value === 'bogo' && n > 8) {
n = 8;
$sizeSlider.value = '8';
updateSizeLabel();
}
array = [];
for (let i = 0; i < n; i++) {
array.push(Math.round(Math.random() * 1000) / 10); // 0.0 100.0
}
// Apply preset
const preset = $presetSelect.value;
switch (preset) {
case 'sorted':
array.sort(function(a, b) { return a - b; });
break;
case 'reversed':
array.sort(function(a, b) { return b - a; });
break;
case 'nearly': {
array.sort(function(a, b) { return a - b; });
const swapCount = Math.max(1, Math.floor(n * 0.1));
for (let s = 0; s < swapCount; s++) {
const i = Math.floor(Math.random() * n);
const j = Math.floor(Math.random() * n);
const tmp = array[i]; array[i] = array[j]; array[j] = tmp;
}
break;
}
case 'duplicates': {
const numDistinct = Math.min(n, Math.floor(Math.random() * 4) + 5); // 5-8 distinct values
const vals = [];
for (let d = 0; d < numDistinct; d++) {
vals.push(Math.round(Math.random() * 1000) / 10);
}
for (let i = 0; i < n; i++) {
array[i] = vals[Math.floor(Math.random() * numDistinct)];
}
break;
}
// 'random' = default, no transformation needed
}
}
// ================================================================
// Sorting Algorithms (pre-computed step recording)
// ================================================================
function buildSteps(algoName) {
const arr = array.slice();
const out = [];
let compares = 0;
let swaps = 0;
let moves = 0;
function snap(type, indices, memState, meta) {
out.push({ type, indices: indices.slice(), array: arr.slice(), compares, swaps, moves, mem: memState || null, meta: meta || null });
}
switch (algoName) {
case 'bubble': {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
let swapped = false;
const m = { sortedFrom: n - i };
for (let j = 0; j < n - 1 - i; j++) {
compares++;
snap('compare', [j, j + 1], null, m);
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
swaps++;
swapped = true;
snap('swap', [j, j + 1], null, m);
}
}
if (!swapped) break;
}
break;
}
case 'selection': {
const n = arr.length;
for (let i = 0; i < n - 1; i++) {
let minIdx = i;
const m = { sortedTo: i - 1 };
for (let j = i + 1; j < n; j++) {
compares++;
snap('compare', [minIdx, j], null, m);
if (arr[j] < arr[minIdx]) minIdx = j;
}
if (minIdx !== i) {
[arr[i], arr[minIdx]] = [arr[minIdx], arr[i]];
swaps++;
snap('swap', [i, minIdx], null, m);
}
}
break;
}
case 'insertion': {
const n = arr.length;
for (let i = 1; i < n; i++) {
const key = arr[i];
let j = i - 1;
const m = { sortedTo: i - 1 };
while (j >= 0) {
compares++;
snap('compare', [j, j + 1], null, m);
if (arr[j] > key) {
arr[j + 1] = arr[j];
moves++;
snap('move', [j + 1], null, m);
j--;
} else {
break;
}
}
if (arr[j + 1] !== key) {
arr[j + 1] = key;
moves++;
snap('move', [j + 1], null, m);
}
}
break;
}
case 'merge': {
function mergePart(l, m, r) {
const left = arr.slice(l, m + 1);
const right = arr.slice(m + 1, r + 1);
let i = 0, j = 0, k = l;
while (i < left.length && j < right.length) {
compares++;
let oldI = arr[l + i], oldJ = arr[m + 1 + j];
arr[l + i] = left[i];
arr[m + 1 + j] = right[j];
const memState = { type: 'merge', left: left.slice(), right: right.slice(), lPtr: i, rPtr: j, targetPtr: k, l, m, r };
snap('compare', [l + i, m + 1 + j], memState, { phase: 'Merge', activeRange: { lo: l, hi: r } });
arr[l + i] = oldI;
arr[m + 1 + j] = oldJ;
if (left[i] <= right[j]) { arr[k] = left[i++]; }
else { arr[k] = right[j++]; }
moves++;
snap('move', [k], memState, { phase: 'Merge', activeRange: { lo: l, hi: r } });
k++;
}
while (i < left.length) { arr[k] = left[i++]; moves++; snap('move', [k], { type: 'merge', left: left.slice(), right: right.slice(), lPtr: i, rPtr: -1, targetPtr: k, l, m, r }); k++; }
while (j < right.length) { arr[k] = right[j++]; moves++; snap('move', [k], { type: 'merge', left: left.slice(), right: right.slice(), lPtr: -1, rPtr: j, targetPtr: k, l, m, r }); k++; }
}
function mergeSort(l, r) {
if (l >= r) return;
const m = (l + r) >> 1;
mergeSort(l, m);
mergeSort(m + 1, r);
mergePart(l, m, r);
}
mergeSort(0, arr.length - 1);
break;
}
case 'heap': {
const n = arr.length;
let heapSize = n;
let heapPhase = 'Heap aufbauen';
function getHeapState(highlight) {
let nodes = [];
for (let hi = 0; hi < heapSize; hi++) {
let depth = Math.floor(Math.log2(hi + 1));
nodes.push({ val: arr[hi], left: 2 * hi + 1 < heapSize ? 2 * hi + 1 : -1, right: 2 * hi + 2 < heapSize ? 2 * hi + 2 : -1, idx: hi, depth: depth });
}
// BFS-Positionen: In-Order ist hier nicht ideal, Level-Order-X-Positionen berechnen
let maxD = nodes.length > 0 ? nodes[nodes.length - 1].depth : 0;
let drawNodes = [];
let totalWidth = Math.pow(2, maxD);
for (let hi = 0; hi < nodes.length; hi++) {
let nd = nodes[hi], d = nd.depth;
// Position in der Ebene
let posInLevel = hi - (Math.pow(2, d) - 1);
let levelCount = Math.min(Math.pow(2, d), heapSize - (Math.pow(2, d) - 1));
let x = (posInLevel + 0.5) * (totalWidth / Math.pow(2, d));
drawNodes.push({ val: nd.val, left: nd.left, right: nd.right, depth: d, drawX: x, drawY: d });
}
return { type: 'heap', nodes: drawNodes, totalWidth: totalWidth, maxDepth: maxD, highlight: highlight || [] };
}
function heapify(size, root) {
heapSize = size;
let largest = root;
const l = 2 * root + 1;
const r = 2 * root + 2;
if (l < size) {
compares++;
snap('compare', [largest, l], getHeapState([root, l]), { phase: heapPhase });
if (arr[l] > arr[largest]) largest = l;
}
if (r < size) {
compares++;
snap('compare', [largest, r], getHeapState([root, r]), { phase: heapPhase });
if (arr[r] > arr[largest]) largest = r;
}
if (largest !== root) {
[arr[root], arr[largest]] = [arr[largest], arr[root]];
swaps++;
snap('swap', [root, largest], getHeapState([root, largest]), { phase: heapPhase });
heapify(size, largest);
}
}
for (let i = Math.floor(n / 2) - 1; i >= 0; i--) heapify(n, i);
heapPhase = 'Extrahieren';
for (let i = n - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]];
swaps++;
heapSize = i;
snap('swap', [0, i], getHeapState([0]), { phase: heapPhase, sortedFrom: i });
heapify(i, 0);
}
break;
}
case 'quick': {
function partition(lo, hi) {
const pivot = arr[hi];
const m = { pivots: [hi], activeRange: { lo, hi }, phase: 'Partition' };
let i = lo - 1;
for (let j = lo; j < hi; j++) {
compares++;
snap('compare', [j, hi], null, m);
if (arr[j] <= pivot) {
i++;
if (i !== j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
swaps++;
snap('swap', [i, j], null, m);
}
}
}
const pi = i + 1;
if (pi !== hi) {
[arr[pi], arr[hi]] = [arr[hi], arr[pi]];
swaps++;
snap('swap', [pi, hi], null, { pivots: [pi], activeRange: { lo, hi } });
}
return pi;
}
function quickSort(lo, hi) {
if (lo < hi) {
const p = partition(lo, hi);
quickSort(lo, p - 1);
quickSort(p + 1, hi);
}
}
quickSort(0, arr.length - 1);
break;
}
case 'quick3way': {
// 3-Way Quick Sort (Dutch National Flag) — handles duplicates efficiently
function quick3way(lo, hi) {
if (lo >= hi) return;
const ar = { activeRange: { lo, hi } };
// Median-of-three pivot selection
const mid = lo + Math.floor((hi - lo) / 2);
compares++;
snap('compare', [lo, mid], null, ar);
if (arr[lo] > arr[mid]) { [arr[lo], arr[mid]] = [arr[mid], arr[lo]]; swaps++; snap('swap', [lo, mid], null, ar); }
compares++;
snap('compare', [lo, hi], null, ar);
if (arr[lo] > arr[hi]) { [arr[lo], arr[hi]] = [arr[hi], arr[lo]]; swaps++; snap('swap', [lo, hi], null, ar); }
compares++;
snap('compare', [mid, hi], null, ar);
if (arr[mid] > arr[hi]) { [arr[mid], arr[hi]] = [arr[hi], arr[mid]]; swaps++; snap('swap', [mid, hi], null, ar); }
// Use median as pivot, move to lo
if (mid !== lo) { [arr[lo], arr[mid]] = [arr[mid], arr[lo]]; swaps++; snap('swap', [lo, mid], null, ar); }
const pivot = arr[lo];
let lt = lo, gt = hi, i = lo + 1;
while (i <= gt) {
const m = { pivots: [lt], activeRange: { lo, hi }, partitions: [{ lo: lo, hi: lt - 1 }, { lo: lt, hi: i - 1 }, { lo: gt + 1, hi: hi }] };
compares++;
snap('compare', [i, lt], null, m);
if (arr[i] < pivot) {
[arr[lt], arr[i]] = [arr[i], arr[lt]];
swaps++;
snap('swap', [lt, i], null, m);
lt++;
i++;
} else if (arr[i] > pivot) {
[arr[i], arr[gt]] = [arr[gt], arr[i]];
swaps++;
snap('swap', [i, gt], null, m);
gt--;
} else {
i++;
}
}
quick3way(lo, lt - 1);
quick3way(gt + 1, hi);
}
quick3way(0, arr.length - 1);
break;
}
case 'dualpivot': {
// Dual-Pivot Quick Sort (Yaroslavskiy) — Java's Arrays.sort for primitives
function dualPivotSort(lo, hi) {
if (lo >= hi) return;
// Ensure arr[lo] <= arr[hi]
compares++;
snap('compare', [lo, hi], null, { pivots: [lo, hi], activeRange: { lo, hi } });
if (arr[lo] > arr[hi]) {
[arr[lo], arr[hi]] = [arr[hi], arr[lo]];
swaps++;
snap('swap', [lo, hi], null, { pivots: [lo, hi], activeRange: { lo, hi } });
}
const p1 = arr[lo], p2 = arr[hi];
let lt = lo + 1, gt = hi - 1, k = lt;
while (k <= gt) {
const m = { pivots: [lo, hi], activeRange: { lo, hi }, partitions: [{ lo, hi: lt - 1 }, { lo: lt, hi: gt }, { lo: gt + 1, hi }] };
compares++;
snap('compare', [k, lo], null, m);
if (arr[k] < p1) {
[arr[k], arr[lt]] = [arr[lt], arr[k]];
swaps++;
snap('swap', [k, lt], null, m);
lt++;
k++;
} else {
compares++;
snap('compare', [k, hi], null, m);
if (arr[k] > p2) {
while (arr[gt] > p2 && k < gt) {
compares++;
snap('compare', [gt, hi], null, m);
gt--;
}
[arr[k], arr[gt]] = [arr[gt], arr[k]];
swaps++;
snap('swap', [k, gt], null, m);
gt--;
compares++;
snap('compare', [k, lo], null, m);
if (arr[k] < p1) {
[arr[k], arr[lt]] = [arr[lt], arr[k]];
swaps++;
snap('swap', [k, lt], null, m);
lt++;
}
k++;
} else {
k++;
}
}
}
lt--;
gt++;
// Move pivots to final positions
[arr[lo], arr[lt]] = [arr[lt], arr[lo]];
swaps++;
snap('swap', [lo, lt], null, { pivots: [lt, gt], activeRange: { lo, hi } });
[arr[hi], arr[gt]] = [arr[gt], arr[hi]];
swaps++;
snap('swap', [hi, gt], null, { pivots: [lt, gt], activeRange: { lo, hi } });
// Recurse on three partitions
dualPivotSort(lo, lt - 1);
dualPivotSort(lt + 1, gt - 1);
dualPivotSort(gt + 1, hi);
}
dualPivotSort(0, arr.length - 1);
break;
}
case 'introsort': {
// Introsort: Quick Sort + Heap Sort fallback + Insertion Sort for small arrays
const maxDepth = 2 * Math.floor(Math.log2(arr.length));
function insertionSort(lo, hi) {
const isMeta = { phase: 'Insertion Sort', activeRange: { lo, hi } };
for (let i = lo + 1; i <= hi; i++) {
const key = arr[i];
let j = i;
while (j > lo) {
compares++;
snap('compare', [j - 1, j], null, isMeta);
if (arr[j - 1] > key) {
arr[j] = arr[j - 1];
moves++;
snap('move', [j], null, isMeta);
j--;
} else {
break;
}
}
if (arr[j] !== key) {
arr[j] = key;
moves++;
snap('move', [j], null, isMeta);
}
}
}
function heapSortRange(lo, hi) {
const hsMeta = { phase: 'Heap Sort (Fallback)', activeRange: { lo, hi } };
const size = hi - lo + 1;
function siftDown(root, end) {
while (true) {
let largest = root;
const l = lo + 2 * (root - lo) + 1;
const r = lo + 2 * (root - lo) + 2;
if (l <= end) {
compares++;
snap('compare', [largest, l], null, hsMeta);
if (arr[l] > arr[largest]) largest = l;
}
if (r <= end) {
compares++;
snap('compare', [largest, r], null, hsMeta);
if (arr[r] > arr[largest]) largest = r;
}
if (largest !== root) {
[arr[root], arr[largest]] = [arr[largest], arr[root]];
swaps++;
snap('swap', [root, largest], null, hsMeta);
root = largest;
} else {
break;
}
}
}
// Build max-heap
for (let i = lo + Math.floor(size / 2) - 1; i >= lo; i--) siftDown(i, hi);
// Extract max
for (let i = hi; i > lo; i--) {
[arr[lo], arr[i]] = [arr[i], arr[lo]];
swaps++;
snap('swap', [lo, i], null, hsMeta);
siftDown(lo, i - 1);
}
}
function introSort(lo, hi, depthLimit) {
const size = hi - lo + 1;
const ar = { activeRange: { lo, hi }, phase: 'Quick Sort' };
if (size <= 16) {
insertionSort(lo, hi);
return;
}
if (depthLimit === 0) {
heapSortRange(lo, hi);
return;
}
// Median-of-three pivot
const mid = lo + Math.floor((hi - lo) / 2);
compares++;
snap('compare', [lo, mid], null, ar);
if (arr[lo] > arr[mid]) { [arr[lo], arr[mid]] = [arr[mid], arr[lo]]; swaps++; snap('swap', [lo, mid], null, ar); }
compares++;
snap('compare', [lo, hi], null, ar);
if (arr[lo] > arr[hi]) { [arr[lo], arr[hi]] = [arr[hi], arr[lo]]; swaps++; snap('swap', [lo, hi], null, ar); }
compares++;
snap('compare', [mid, hi], null, ar);
if (arr[mid] > arr[hi]) { [arr[mid], arr[hi]] = [arr[hi], arr[mid]]; swaps++; snap('swap', [mid, hi], null, ar); }
// Move pivot to hi-1
[arr[mid], arr[hi - 1]] = [arr[hi - 1], arr[mid]];
swaps++;
snap('swap', [mid, hi - 1], null, ar);
const pivot = arr[hi - 1];
let i = lo, j = hi - 1;
while (true) {
const pm = { pivots: [hi - 1], activeRange: { lo, hi } };
do { i++; compares++; snap('compare', [i, hi - 1], null, pm); } while (arr[i] < pivot);
do { j--; compares++; snap('compare', [j, hi - 1], null, pm); } while (arr[j] > pivot);
if (i >= j) break;
[arr[i], arr[j]] = [arr[j], arr[i]];
swaps++;
snap('swap', [i, j], null, pm);
}
[arr[i], arr[hi - 1]] = [arr[hi - 1], arr[i]];
swaps++;
snap('swap', [i, hi - 1], null, { pivots: [i], activeRange: { lo, hi } });
introSort(lo, i - 1, depthLimit - 1);
introSort(i + 1, hi, depthLimit - 1);
}
introSort(0, arr.length - 1, maxDepth);
break;
}
case 'cocktail': {
const n = arr.length;
let start = 0, end = n - 1;
let swapped = true;
while (swapped) {
swapped = false;
const m = { sortedTo: start - 1, sortedFrom: end + 1 };
for (let i = start; i < end; i++) {
compares++;
snap('compare', [i, i + 1], null, m);
if (arr[i] > arr[i + 1]) {
[arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
swaps++;
swapped = true;
snap('swap', [i, i + 1], null, m);
}
}
if (!swapped) break;
end--;
swapped = false;
const m2 = { sortedTo: start - 1, sortedFrom: end + 1 };
for (let i = end; i > start; i--) {
compares++;
snap('compare', [i - 1, i], null, m2);
if (arr[i - 1] > arr[i]) {
[arr[i - 1], arr[i]] = [arr[i], arr[i - 1]];
swaps++;
swapped = true;
snap('swap', [i - 1, i], null, m2);
}
}
start++;
}
break;
}
case 'shell': {
const n = arr.length;
// Knuth-Folge: 1, 4, 13, 40, 121, ...
let gap = 1;
while (gap < Math.floor(n / 3)) gap = gap * 3 + 1;
while (gap >= 1) {
const gm = { gap };
for (let i = gap; i < n; i++) {
const key = arr[i];
let j = i;
while (j >= gap) {
compares++;
snap('compare', [j - gap, j], null, gm);
if (arr[j - gap] > key) {
arr[j] = arr[j - gap];
moves++;
snap('move', [j], null, gm);
j -= gap;
} else {
break;
}
}
if (arr[j] !== key) {
arr[j] = key;
moves++;
snap('move', [j], null, gm);
}
}
gap = Math.floor(gap / 3);
}
break;
}
case 'counting': {
const n = arr.length;
if (n === 0) break;
// Counting Sort benötigt Integer-Indizes — Werte auf ganze Zahlen runden
for (let i = 0; i < n; i++) arr[i] = Math.round(arr[i]);
let minVal = arr[0], maxVal = arr[0];
for (let i = 1; i < n; i++) { if (arr[i] < minVal) minVal = arr[i]; if (arr[i] > maxVal) maxVal = arr[i]; }
const range = maxVal - minVal + 1;
const count = new Array(range).fill(0);
// Zähle Vorkommen
for (let i = 0; i < n; i++) {
count[arr[i] - minVal]++;
snap('move', [i], { type: 'counting', count: count.slice(), minVal: minVal, highlight: arr[i] - minVal });
}
// Schreibe sortiert zurück
let pos = 0;
for (let i = 0; i < range; i++) {
while (count[i] > 0) {
arr[pos] = i + minVal;
moves++;
count[i]--;
snap('move', [pos], { type: 'counting', count: count.slice(), minVal: minVal, highlight: i });
pos++;
}
}
break;
}
case 'radix': {
const n = arr.length;
if (n === 0) break;
// Radix Sort benötigt Integer — Werte auf ganze Zahlen runden
for (let i = 0; i < n; i++) arr[i] = Math.round(arr[i]);
let maxVal = arr[0];
for (let i = 1; i < n; i++) { if (arr[i] > maxVal) maxVal = arr[i]; }
const output = new Array(n);
let digitPos = 0;
for (let exp = 1; Math.floor(maxVal / exp) > 0; exp *= 10) {
digitPos++;
const bucketContents = [];
for (let d = 0; d < 10; d++) bucketContents.push([]);
// Verteile in Buckets
for (let i = 0; i < n; i++) {
const digit = Math.floor(arr[i] / exp) % 10;
bucketContents[digit].push(arr[i]);
snap('move', [i], { type: 'radix', buckets: bucketContents.map(function(b) { return b.slice(); }), digitPos: digitPos, highlight: digit }, { phase: 'Stelle ' + digitPos + ' \u2014 Verteilen' });
}
// Stabil zusammenführen
const buckets = new Array(10).fill(0);
for (let i = 0; i < n; i++) {
const digit = Math.floor(arr[i] / exp) % 10;
buckets[digit]++;
}
for (let i = 1; i < 10; i++) buckets[i] += buckets[i - 1];
for (let i = n - 1; i >= 0; i--) {
const digit = Math.floor(arr[i] / exp) % 10;
output[buckets[digit] - 1] = arr[i];
buckets[digit]--;
}
// Zurückkopieren
for (let i = 0; i < n; i++) {
arr[i] = output[i];
moves++;
snap('move', [i], { type: 'radix', buckets: bucketContents.map(function(b) { return b.slice(); }), digitPos: digitPos, highlight: -1 }, { phase: 'Stelle ' + digitPos + ' \u2014 Sammeln' });
}
}
break;
}
case 'tree': {
const nodes = [];
function getTreeState(highlightPath) {
if (nodes.length === 0) return null;
let xCounter = 0;
const drawNodes = nodes.map(function(n) { return { val: n.val, left: n.left, right: n.right, depth: n.depth, drawX: 0, drawY: 0 }; });
function assignX(idx) {
if (idx === -1 || !drawNodes[idx]) return;
assignX(drawNodes[idx].left);
drawNodes[idx].drawX = xCounter++;
drawNodes[idx].drawY = drawNodes[idx].depth;
assignX(drawNodes[idx].right);
}
assignX(0);
let maxD = 0;
for (let di = 0; di < drawNodes.length; di++) { if (drawNodes[di].depth > maxD) maxD = drawNodes[di].depth; }
return { type: 'bst', nodes: drawNodes, totalWidth: xCounter, maxDepth: maxD, highlight: highlightPath || [] };
}
function bstInsert(val, srcIdx) {
const node = { val: val, left: -1, right: -1, depth: 0 };
if (nodes.length === 0) { nodes.push(node); snap('move', [srcIdx], getTreeState([0])); return; }
let cur = 0, path = [0];
while (true) {
compares++;
snap('compare', [srcIdx], getTreeState(path));
if (val < nodes[cur].val) {
if (nodes[cur].left === -1) { node.depth = nodes[cur].depth + 1; nodes[cur].left = nodes.length; nodes.push(node); snap('move', [srcIdx], getTreeState([].concat(path, [nodes.length - 1]))); return; }
cur = nodes[cur].left; path.push(cur);
} else {
if (nodes[cur].right === -1) { node.depth = nodes[cur].depth + 1; nodes[cur].right = nodes.length; nodes.push(node); snap('move', [srcIdx], getTreeState([].concat(path, [nodes.length - 1]))); return; }
cur = nodes[cur].right; path.push(cur);
}
}
}
for (let i = 0; i < arr.length; i++) bstInsert(arr[i], i);
let writePos = 0;
function inOrder(idx) {
if (idx === -1) return;
inOrder(nodes[idx].left);
arr[writePos] = nodes[idx].val;
moves++;
snap('move', [writePos], getTreeState([idx]));
writePos++;
inOrder(nodes[idx].right);
}
inOrder(0);
break;
}
case 'timsort': {
const n = arr.length;
const RUN = 32;
const runMeta = { runs: [] };
for (let r = 0; r < n; r += RUN) runMeta.runs.push(r);
// Phase 1: Insertion Sort auf Runs der Größe RUN
for (let start = 0; start < n; start += RUN) {
const end = Math.min(start + RUN, n);
for (let i = start + 1; i < end; i++) {
const key = arr[i];
let j = i - 1;
while (j >= start) {
compares++;
snap('compare', [j, j + 1], null, runMeta);
if (arr[j] > key) {
arr[j + 1] = arr[j];
moves++;
snap('move', [j + 1], null, runMeta);
j--;
} else {
break;
}
}
if (arr[j + 1] !== key) {
arr[j + 1] = key;
moves++;
snap('move', [j + 1], null, runMeta);
}
}
}
// Phase 2: Bottom-up Merge mit wachsenden Blöcken
function timMerge(l, m, r) {
const left = arr.slice(l, m + 1);
const right = arr.slice(m + 1, r + 1);
let i = 0, j = 0, k = l;
while (i < left.length && j < right.length) {
compares++;
let oldI = arr[l + i], oldJ = arr[m + 1 + j];
arr[l + i] = left[i]; arr[m + 1 + j] = right[j];
const memState = { type: 'merge', left: left.slice(), right: right.slice(), lPtr: i, rPtr: j, targetPtr: k, l, m, r };
snap('compare', [l + i, m + 1 + j], memState);
arr[l + i] = oldI; arr[m + 1 + j] = oldJ;
if (left[i] <= right[j]) { arr[k] = left[i++]; }
else { arr[k] = right[j++]; }
moves++;
snap('move', [k], memState);
k++;
}
while (i < left.length) { arr[k] = left[i++]; moves++; snap('move', [k], { type: 'merge', left: left.slice(), right: right.slice(), lPtr: i, rPtr: -1, targetPtr: k, l, m, r }); k++; }
while (j < right.length) { arr[k] = right[j++]; moves++; snap('move', [k], { type: 'merge', left: left.slice(), right: right.slice(), lPtr: -1, rPtr: j, targetPtr: k, l, m, r }); k++; }
}
for (let size = RUN; size < n; size *= 2) {
for (let left = 0; left < n; left += 2 * size) {
const mid = Math.min(left + size - 1, n - 1);
const right = Math.min(left + 2 * size - 1, n - 1);
if (mid < right) {
timMerge(left, mid, right);
}
}
}
break;
}
case 'bucket': {
const n = arr.length;
if (n === 0) break;
let minVal = arr[0], maxVal = arr[0];
for (let i = 1; i < n; i++) { if (arr[i] < minVal) minVal = arr[i]; if (arr[i] > maxVal) maxVal = arr[i]; }
const range = maxVal - minVal + 1;
const bucketCount = Math.max(1, Math.ceil(Math.sqrt(n)));
const bucketSize = range / bucketCount;
const bucketArr = [];
for (let i = 0; i < bucketCount; i++) bucketArr.push([]);
function getBucketState(hlBucket) {
return { type: 'buckets', buckets: bucketArr.map(function(b) { return b.slice(); }), highlight: hlBucket };
}
// Verteile in Buckets
for (let i = 0; i < n; i++) {
const idx = Math.min(Math.floor((arr[i] - minVal) / bucketSize), bucketCount - 1);
bucketArr[idx].push(arr[i]);
snap('move', [i], getBucketState(idx));
}
// Sortiere jeden Bucket (Insertion Sort) und schreibe zurück
let pos = 0;
for (let b = 0; b < bucketCount; b++) {
const buck = bucketArr[b];
for (let i = 1; i < buck.length; i++) {
const key = buck[i];
let j = i - 1;
while (j >= 0 && buck[j] > key) {
buck[j + 1] = buck[j];
compares++;
j--;
}
if (j + 1 < i) compares++;
buck[j + 1] = key;
}
// Zurückschreiben
for (let i = 0; i < buck.length; i++) {
arr[pos] = buck[i];
moves++;
snap('move', [pos], getBucketState(b));
pos++;
}
}
break;
}
case 'shuffle': {
const n = arr.length;
if (n === 0) break;
// Fisher-Yates Shuffle (Knuth variant)
for (let i = n - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
if (j !== i) {
[arr[i], arr[j]] = [arr[j], arr[i]];
swaps++;
snap('swap', [i, j]);
}
}
break;
}
case 'pancake': {
const n = arr.length;
function flip(end) {
let left = 0, right = end;
while (left < right) {
[arr[left], arr[right]] = [arr[right], arr[left]];
swaps++;
snap('swap', [left, right]);
left++;
right--;
}
}
for (let size = n - 1; size > 0; size--) {
// Finde Index des Maximums in arr[0..size]
let maxIdx = 0;
for (let i = 1; i <= size; i++) {
compares++;
snap('compare', [maxIdx, i]);
if (arr[i] > arr[maxIdx]) maxIdx = i;
}
if (maxIdx === size) continue;
// Flip Maximum nach vorne (wenn nicht schon dort)
if (maxIdx > 0) {
flip(maxIdx);
}
// Flip nach endgültiger Position
flip(size);
}
break;
}
case 'cycle': {
const n = arr.length;
for (let cycleStart = 0; cycleStart < n - 1; cycleStart++) {
let item = arr[cycleStart];
// Finde die Position, wohin item gehört
let pos = cycleStart;
for (let i = cycleStart + 1; i < n; i++) {
compares++;
snap('compare', [cycleStart, i]);
if (arr[i] < item) pos++;
}
if (pos === cycleStart) continue; // Element ist bereits richtig
// Überspringe Duplikate
while (item === arr[pos]) pos++;
if (pos !== cycleStart) {
let temp = arr[pos];
arr[pos] = item;
item = temp;
moves++;
snap('move', [pos]);
}
// Rotiere den Rest des Zyklus
while (pos !== cycleStart) {
pos = cycleStart;
for (let i = cycleStart + 1; i < n; i++) {
compares++;
snap('compare', [cycleStart, i]);
if (arr[i] < item) pos++;
}
while (item === arr[pos]) pos++;
if (item !== arr[pos]) {
let temp = arr[pos];
arr[pos] = item;
item = temp;
moves++;
snap('move', [pos]);
}
}
}
break;
}
case 'bogo': {
const n = arr.length;
const MAX_ATTEMPTS = 500000;
let attempts = 0;
let success = false;
function isSorted() {
for (let i = 0; i < n - 1; i++) {
compares++; // Zählt für Statistik
// FIX: Snapshot hier entfernt, da sonst das Bild flackert
if (arr[i] > arr[i + 1]) return false;
}
return true;
}
function shuffle() {
for (let i = n - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
if (i !== j) {
[arr[i], arr[j]] = [arr[j], arr[i]];
swaps++;
}
}
}
while (!isSorted() && attempts < MAX_ATTEMPTS) {
shuffle();
attempts++;
// Nur jeden N-ten Versuch snappen, sonst zu viele Steps
if (attempts % Math.max(1, Math.floor(n / 2)) === 0 || attempts < 20) {
snap('swap', Array.from({length: n}, (_, i) => i));
}
}
// FIX: Abbruchbedingung sauber abfangen
if (isSorted()) success = true;
break;
}
}
out.push({ type: 'done', indices: [], array: arr.slice(), compares, swaps, moves, mem: null });
return out;
}
// ================================================================
// Canvas Rendering
// ================================================================
function resizeCanvas(cvs, ctxRef, heightRatio, maxH, minH) {
cvs = cvs || canvas; ctxRef = ctxRef || ctx;
heightRatio = heightRatio || 0.42; maxH = maxH || 500; minH = minH || 180;
const wrap = cvs.parentElement;
const dpr = window.devicePixelRatio || 1;
// Collapse canvas first so parent can shrink to its natural width
cvs.style.width = '0px';
const padding = cvs === canvas ? 24 : 0;
const cssW = wrap.clientWidth - padding;
const cssH = Math.max(minH, Math.min(cssW * heightRatio, maxH));
cvs.style.width = cssW + 'px';
cvs.style.height = cssH + 'px';
cvs.width = Math.round(cssW * dpr);
cvs.height = Math.round(cssH * dpr);
ctxRef.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function drawBars(arr, highlights, allSorted, meta, connectedPair) {
const dpr = window.devicePixelRatio || 1;
const W = canvas.width / dpr;
const H = canvas.height / dpr;
const n = arr.length;
ctx.clearRect(0, 0, W, H);
if (n === 0) return;
const PAD_L = 6, PAD_R = 6;
const PAD_T = 26, PAD_B = 22;
const areaW = W - PAD_L - PAD_R;
const areaH = H - PAD_T - PAD_B;
// Baseline
ctx.strokeStyle = COLORS.axis;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD_L, H - PAD_B + 0.5);
ctx.lineTo(W - PAD_R, H - PAD_B + 0.5);
ctx.stroke();
let maxVal = arr[0];
for (let i = 1; i < n; i++) { if (arr[i] > maxVal) maxVal = arr[i]; }
if (maxVal < 1) maxVal = 1;
const slotW = areaW / n;
const gapW = Math.max(1, slotW * 0.1);
const barW = slotW - gapW;
const showLabels = barW >= 14;
const fontSize = Math.max(8, Math.min(12, barW * 0.55));
// ── Meta: Hintergrund-Overlays (VOR den Balken) ──
if (meta && !allSorted) {
// Sortierter Bereich (grüner Hintergrund)
if (meta.sortedTo !== undefined && meta.sortedTo >= 0) {
ctx.fillStyle = 'rgba(60, 255, 160, 0.06)';
ctx.fillRect(PAD_L, PAD_T - 2, (meta.sortedTo + 1) * slotW, areaH + 4);
}
if (meta.sortedFrom !== undefined && meta.sortedFrom < n) {
ctx.fillStyle = 'rgba(60, 255, 160, 0.06)';
let sx = PAD_L + meta.sortedFrom * slotW;
ctx.fillRect(sx, PAD_T - 2, (n - meta.sortedFrom) * slotW, areaH + 4);
}
// Aktiver Bereich (dünne Linie oben)
if (meta.activeRange) {
let ax0 = PAD_L + meta.activeRange.lo * slotW + gapW / 2;
let ax1 = PAD_L + meta.activeRange.hi * slotW + gapW / 2 + barW;
ctx.save();
ctx.strokeStyle = 'rgba(96, 165, 250, 0.5)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(ax0, PAD_T - 6);
ctx.lineTo(ax1, PAD_T - 6);
ctx.stroke();
// kleine vertikale Endmarker
ctx.beginPath();
ctx.moveTo(ax0, PAD_T - 10); ctx.lineTo(ax0, PAD_T - 2);
ctx.moveTo(ax1, PAD_T - 10); ctx.lineTo(ax1, PAD_T - 2);
ctx.stroke();
ctx.restore();
}
// Partitionsbereiche (subtile Hintergrundfarben)
if (meta.partitions) {
let partColors = ['rgba(96,165,250,0.05)', 'rgba(255,154,60,0.05)', 'rgba(185,74,255,0.05)'];
for (let pi = 0; pi < meta.partitions.length; pi++) {
let part = meta.partitions[pi];
ctx.fillStyle = partColors[pi % partColors.length];
let px0 = PAD_L + part.lo * slotW;
let px1 = PAD_L + (part.hi + 1) * slotW;
ctx.fillRect(px0, PAD_T - 2, px1 - px0, areaH + 4);
}
}
// Run-Grenzen (dünne vertikale Linien)
if (meta.runs) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.18)';
ctx.lineWidth = 1;
for (let ri = 0; ri < meta.runs.length; ri++) {
let rx = PAD_L + meta.runs[ri] * slotW + 0.5;
ctx.beginPath();
ctx.moveTo(rx, PAD_T - 2);
ctx.lineTo(rx, H - PAD_B);
ctx.stroke();
}
}
}
ctx.font = `600 ${fontSize}px Inter, system-ui, sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
// Reusable rounded-rect path helper (defined once, called per bar)
function drawRoundedRectPath(ctx, px, py, pw, ph, pr) {
if (pr > 0 && ph > pr * 2) {
ctx.beginPath();
ctx.moveTo(px + pr, py);
ctx.lineTo(px + pw - pr, py);
ctx.quadraticCurveTo(px + pw, py, px + pw, py + pr);
ctx.lineTo(px + pw, py + ph);
ctx.lineTo(px, py + ph);
ctx.lineTo(px, py + pr);
ctx.quadraticCurveTo(px, py, px + pr, py);
ctx.closePath();
} else {
ctx.beginPath();
ctx.rect(px, py, pw, ph);
}
}
for (let i = 0; i < n; i++) {
const val = arr[i];
const barH = Math.max(2, (val / maxVal) * areaH);
const x = PAD_L + i * slotW + gapW / 2;
const y = H - PAD_B - barH;
// Determine color
let color = COLORS.normal;
if (allSorted) {
color = COLORS.sorted;
} else if (highlights[i]) {
const t = highlights[i];
if (t === 'compare') color = COLORS.compare;
else if (t === 'swap') color = COLORS.swap;
else if (t === 'move') color = COLORS.move;
}
const r = Math.min(3, barW / 4);
// Glow-Effekt
if (highlights[i] && !allSorted) {
ctx.save();
ctx.fillStyle = color;
ctx.shadowColor = color;
ctx.shadowBlur = 14;
ctx.globalAlpha = 0.35;
drawRoundedRectPath(ctx, x, y, barW, barH, r);
ctx.fill();
ctx.restore();
}
// Haupt-Balken
ctx.fillStyle = color;
ctx.globalAlpha = 1;
drawRoundedRectPath(ctx, x, y, barW, barH, r);
ctx.fill();
// Value label
if (showLabels) {
ctx.fillStyle = COLORS.text;
ctx.globalAlpha = 0.85;
const label = Number.isInteger(val) ? String(val) : val.toFixed(1);
ctx.fillText(label, x + barW / 2, y - 2);
ctx.globalAlpha = 1;
}
}
// ── Meta: Vordergrund-Overlays (NACH den Balken) ──
if (meta && !allSorted) {
// Pivot-Markierung (weißer Rahmen)
if (meta.pivots) {
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 2;
for (let pvi = 0; pvi < meta.pivots.length; pvi++) {
let pvIdx = meta.pivots[pvi];
if (pvIdx >= 0 && pvIdx < n) {
let pvBarH = Math.max(2, (arr[pvIdx] / maxVal) * areaH);
let pvX = PAD_L + pvIdx * slotW + gapW / 2;
let pvY = H - PAD_B - pvBarH;
ctx.strokeRect(pvX - 1, pvY - 1, barW + 2, pvBarH + 2);
}
}
}
// Gap-Anzeige (oben rechts)
if (meta.gap !== undefined) {
ctx.save();
ctx.fillStyle = 'rgba(224, 228, 240, 0.55)';
ctx.font = '600 11px Inter, system-ui, sans-serif';
ctx.textAlign = 'right';
ctx.textBaseline = 'top';
ctx.fillText('Gap: ' + meta.gap, W - PAD_R - 2, 4);
ctx.restore();
}
}
// ── Value Tokens (unter den Balken) ──
if (barW >= 16) {
ctx.fillStyle = COLORS.muted || '#6b7394';
ctx.font = '500 ' + Math.max(7, Math.min(10, barW * 0.4)) + 'px Inter, system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
for (let ti = 0; ti < n; ti++) {
const tv = arr[ti];
const tokenLabel = Number.isInteger(tv) ? String(tv) : tv.toFixed(1);
const tx = PAD_L + ti * slotW + gapW / 2 + barW / 2;
ctx.fillText(tokenLabel, tx, H - PAD_B + 3);
}
}
// ── Connection Lines (Bogen zwischen verglichenen/getauschten Balken) ──
if (connectedPair && !allSorted && connectedPair.indices.length >= 2) {
const ci = connectedPair.indices[0], cj = connectedPair.indices[1];
if (ci >= 0 && ci < n && cj >= 0 && cj < n) {
const cx1 = PAD_L + ci * slotW + gapW / 2 + barW / 2;
const cx2 = PAD_L + cj * slotW + gapW / 2 + barW / 2;
const cy = PAD_T - 14;
const dist = Math.abs(cx2 - cx1);
const arcH = Math.min(16, dist * 0.15 + 6);
ctx.save();
ctx.strokeStyle = connectedPair.type === 'compare' ? COLORS.compare : COLORS.swap;
ctx.lineWidth = 1.5;
ctx.globalAlpha = 0.55;
ctx.beginPath();
ctx.moveTo(cx1, cy);
ctx.quadraticCurveTo((cx1 + cx2) / 2, cy - arcH, cx2, cy);
ctx.stroke();
// Small dots at endpoints
ctx.globalAlpha = 0.7;
ctx.fillStyle = ctx.strokeStyle;
ctx.beginPath(); ctx.arc(cx1, cy, 2, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(cx2, cy, 2, 0, Math.PI * 2); ctx.fill();
ctx.restore();
}
}
}
// ================================================================
// Memory View Rendering
// ================================================================
function drawMemory(state) {
if (!state) return;
let dpr = window.devicePixelRatio || 1;
let W = memCanvas.width / dpr, H = memCanvas.height / dpr;
memCtx.clearRect(0, 0, W, H);
if (state.type === 'bst') drawBST(state, W, H);
else if (state.type === 'heap') drawHeap(state, W, H);
else if (state.type === 'merge') drawMergeArrays(state, W, H);
else if (state.type === 'counting') drawCountingArray(state, W, H);
else if (state.type === 'radix') drawRadixBuckets(state, W, H);
else if (state.type === 'buckets') drawBuckets(state, W, H);
}
function drawBST(state, W, H) {
let nodes = state.nodes, totalWidth = state.totalWidth, maxDepth = state.maxDepth, highlight = state.highlight;
if (!nodes || nodes.length === 0) return;
let PAD = 40, aW = W - PAD * 2, aH = H - PAD * 2;
let stepX = totalWidth > 1 ? aW / (totalWidth - 1) : 0;
let stepY = maxDepth > 0 ? aH / maxDepth : 0;
// Knoten-Radius skaliert mit Anzahl
let R = Math.max(6, Math.min(20, stepX * 0.4));
// Kanten zeichnen
memCtx.strokeStyle = '#3a3f55'; memCtx.lineWidth = 2;
for (let i = 0; i < nodes.length; i++) {
let p = nodes[i], px = PAD + p.drawX * stepX, py = PAD + p.drawY * stepY;
let children = [p.left, p.right];
for (let ci = 0; ci < children.length; ci++) {
let cIdx = children[ci];
if (cIdx !== -1 && nodes[cIdx]) {
let c = nodes[cIdx], cx = PAD + c.drawX * stepX, cy = PAD + c.drawY * stepY;
memCtx.beginPath(); memCtx.moveTo(px, py); memCtx.lineTo(cx, cy); memCtx.stroke();
}
}
}
// Knoten zeichnen
for (let i = 0; i < nodes.length; i++) {
let n = nodes[i], x = PAD + n.drawX * stepX, y = PAD + n.drawY * stepY;
let isH = highlight.indexOf(i) !== -1;
let color = isH ? COLORS.compare : COLORS.normal;
if (isH) {
memCtx.save(); memCtx.shadowColor = color; memCtx.shadowBlur = 15;
memCtx.fillStyle = color; memCtx.beginPath(); memCtx.arc(x, y, R, 0, Math.PI * 2); memCtx.fill();
memCtx.restore();
}
memCtx.fillStyle = color; memCtx.beginPath(); memCtx.arc(x, y, R, 0, Math.PI * 2); memCtx.fill();
memCtx.fillStyle = '#fff';
memCtx.font = '700 ' + Math.max(9, R * 0.8) + 'px Inter, sans-serif';
memCtx.textAlign = 'center'; memCtx.textBaseline = 'middle';
memCtx.fillText(String(n.val), x, y + 1);
}
}
function drawMergeArrays(state, W, H) {
let left = state.left, right = state.right, lPtr = state.lPtr, rPtr = state.rPtr;
let totalLen = left.length + right.length;
if (totalLen === 0) return;
// Sicheres Maximum ohne Spread (Stack-Overflow-Schutz bei großen Arrays)
let maxV = 1;
for (let mi = 0; mi < left.length; mi++) { if (left[mi] > maxV) maxV = left[mi]; }
for (let mi = 0; mi < right.length; mi++) { if (right[mi] > maxV) maxV = right[mi]; }
let PAD = 20, GAP = 20;
let isNarrow = W < 400;
if (isNarrow) {
// Mobile: Arrays untereinander
let sW = (W - PAD * 2) / Math.max(left.length, right.length);
let bW = sW * 0.7, gap = sW * 0.3;
let halfH = (H - 40) / 2, aH = halfH * 0.55, baseY1 = halfH - 5, baseY2 = H - 10;
// Links
memCtx.fillStyle = '#8b949e'; memCtx.font = '600 11px Inter, sans-serif'; memCtx.textAlign = 'left';
memCtx.fillText('Links (Index ' + state.l + '\u2013' + state.m + ')', PAD, 13);
for (let i = 0; i < left.length; i++) {
let x = PAD + i * sW + gap / 2, bH = Math.max(2, (left[i] / maxV) * aH), y = baseY1 - bH;
let isH = (i === lPtr), color = isH ? COLORS.compare : '#3a7cff';
if (isH) { memCtx.save(); memCtx.shadowColor = color; memCtx.shadowBlur = 12; memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH); memCtx.restore(); }
memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH);
if (bW > 12) { memCtx.fillStyle = '#fff'; memCtx.font = '600 ' + Math.min(10, bW * 0.6) + 'px monospace'; memCtx.textAlign = 'center'; memCtx.fillText(String(left[i]), x + bW / 2, y - 4); }
}
// Rechts
memCtx.fillStyle = '#8b949e'; memCtx.font = '600 11px Inter, sans-serif'; memCtx.textAlign = 'left';
memCtx.fillText('Rechts (Index ' + (state.m + 1) + '\u2013' + state.r + ')', PAD, halfH + 13);
for (let i = 0; i < right.length; i++) {
let x = PAD + i * sW + gap / 2, bH = Math.max(2, (right[i] / maxV) * aH), y = baseY2 - bH;
let isH = (i === rPtr), color = isH ? COLORS.compare : '#b94aff';
if (isH) { memCtx.save(); memCtx.shadowColor = color; memCtx.shadowBlur = 12; memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH); memCtx.restore(); }
memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH);
if (bW > 12) { memCtx.fillStyle = '#fff'; memCtx.font = '600 ' + Math.min(10, bW * 0.6) + 'px monospace'; memCtx.textAlign = 'center'; memCtx.fillText(String(right[i]), x + bW / 2, y - 4); }
}
} else {
// Desktop: Arrays nebeneinander
let totalW = W - PAD * 2 - GAP;
let sW = totalW / totalLen, bW = sW * 0.7, gap = sW * 0.3;
let aH = H * 0.6, baseY = H - 20;
// Links
memCtx.fillStyle = '#8b949e'; memCtx.font = '600 11px Inter, sans-serif'; memCtx.textAlign = 'left';
memCtx.fillText('Links (Index ' + state.l + '\u2013' + state.m + ')', PAD, 15);
for (let i = 0; i < left.length; i++) {
let x = PAD + i * sW + gap / 2, bH = Math.max(2, (left[i] / maxV) * aH), y = baseY - bH;
let isH = (i === lPtr), color = isH ? COLORS.compare : '#3a7cff';
if (isH) { memCtx.save(); memCtx.shadowColor = color; memCtx.shadowBlur = 12; memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH); memCtx.restore(); }
memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH);
if (bW > 12) { memCtx.fillStyle = '#fff'; memCtx.font = '600 ' + Math.min(10, bW * 0.6) + 'px monospace'; memCtx.textAlign = 'center'; memCtx.fillText(String(left[i]), x + bW / 2, y - 4); }
}
// Rechts
let rightStartX = PAD + left.length * sW + GAP;
memCtx.fillStyle = '#8b949e'; memCtx.font = '600 11px Inter, sans-serif'; memCtx.textAlign = 'left';
memCtx.fillText('Rechts (Index ' + (state.m + 1) + '\u2013' + state.r + ')', rightStartX, 15);
for (let i = 0; i < right.length; i++) {
let x = rightStartX + i * sW + gap / 2, bH = Math.max(2, (right[i] / maxV) * aH), y = baseY - bH;
let isH = (i === rPtr), color = isH ? COLORS.compare : '#b94aff';
if (isH) { memCtx.save(); memCtx.shadowColor = color; memCtx.shadowBlur = 12; memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH); memCtx.restore(); }
memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH);
if (bW > 12) { memCtx.fillStyle = '#fff'; memCtx.font = '600 ' + Math.min(10, bW * 0.6) + 'px monospace'; memCtx.textAlign = 'center'; memCtx.fillText(String(right[i]), x + bW / 2, y - 4); }
}
}
}
function drawHeap(state, W, H) {
let nodes = state.nodes, totalWidth = state.totalWidth, maxDepth = state.maxDepth, highlight = state.highlight;
if (!nodes || nodes.length === 0) return;
let PAD = 30, aW = W - PAD * 2, aH = H - PAD * 2;
let stepY = maxDepth > 0 ? aH / maxDepth : 0;
let R = Math.max(6, Math.min(18, aW / (nodes.length + 1) * 0.35));
// Kanten
memCtx.strokeStyle = '#3a3f55'; memCtx.lineWidth = 1.5;
for (let i = 0; i < nodes.length; i++) {
let nd = nodes[i], px = PAD + (nd.drawX / totalWidth) * aW, py = PAD + nd.drawY * stepY;
let children = [nd.left, nd.right];
for (let ci = 0; ci < children.length; ci++) {
let cIdx = children[ci];
if (cIdx !== -1 && nodes[cIdx]) {
let c = nodes[cIdx], cx = PAD + (c.drawX / totalWidth) * aW, cy = PAD + c.drawY * stepY;
memCtx.beginPath(); memCtx.moveTo(px, py); memCtx.lineTo(cx, cy); memCtx.stroke();
}
}
}
// Knoten
for (let i = 0; i < nodes.length; i++) {
let n = nodes[i], x = PAD + (n.drawX / totalWidth) * aW, y = PAD + n.drawY * stepY;
let isH = highlight.indexOf(i) !== -1;
let color = isH ? COLORS.compare : COLORS.normal;
if (isH) {
memCtx.save(); memCtx.shadowColor = color; memCtx.shadowBlur = 12;
memCtx.fillStyle = color; memCtx.beginPath(); memCtx.arc(x, y, R, 0, Math.PI * 2); memCtx.fill();
memCtx.restore();
}
memCtx.fillStyle = color; memCtx.beginPath(); memCtx.arc(x, y, R, 0, Math.PI * 2); memCtx.fill();
memCtx.fillStyle = '#fff';
memCtx.font = '700 ' + Math.max(8, R * 0.7) + 'px Inter, sans-serif';
memCtx.textAlign = 'center'; memCtx.textBaseline = 'middle';
memCtx.fillText(String(n.val), x, y + 1);
}
}
function drawCountingArray(state, W, H) {
let count = state.count, minVal = state.minVal, hl = state.highlight;
let n = count.length;
if (n === 0) return;
let maxC = 1;
for (let i = 0; i < n; i++) { if (count[i] > maxC) maxC = count[i]; }
let PAD = 20, aW = W - PAD * 2, aH = H * 0.55, baseY = H - 20;
let sW = aW / n, bW = Math.max(1, sW * 0.7), gap = sW * 0.15;
memCtx.fillStyle = '#8b949e'; memCtx.font = '600 11px Inter, sans-serif'; memCtx.textAlign = 'left';
memCtx.fillText('H\u00e4ufigkeiten (Wert ' + minVal + '\u2013' + (minVal + n - 1) + ')', PAD, 14);
for (let i = 0; i < n; i++) {
let x = PAD + i * sW + gap;
let bH = count[i] > 0 ? Math.max(2, (count[i] / maxC) * aH) : 0;
let y = baseY - bH;
let isH = (i === hl);
let color = isH ? COLORS.move : '#3cffa0';
if (bH > 0) {
if (isH) { memCtx.save(); memCtx.shadowColor = color; memCtx.shadowBlur = 12; memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH); memCtx.restore(); }
memCtx.fillStyle = color; memCtx.fillRect(x, y, bW, bH);
}
// Wert-Label unter dem Balken
if (sW > 10) {
memCtx.fillStyle = '#8b949e'; memCtx.font = '500 ' + Math.min(10, sW * 0.5) + 'px monospace'; memCtx.textAlign = 'center';
memCtx.fillText(String(i + minVal), x + bW / 2, baseY + 12);
}
// Häufigkeit über dem Balken
if (count[i] > 0 && sW > 10) {
memCtx.fillStyle = '#fff'; memCtx.font = '600 ' + Math.min(10, sW * 0.5) + 'px monospace'; memCtx.textAlign = 'center';
memCtx.fillText(String(count[i]), x + bW / 2, y - 4);
}
}
}
function drawRadixBuckets(state, W, H) {
let buckets = state.buckets, digitPos = state.digitPos, hl = state.highlight;
let PAD = 20, GAP = 6;
let totalSlots = 10;
let slotW = (W - PAD * 2 - GAP * 9) / totalSlots;
let maxLen = 1;
for (let d = 0; d < 10; d++) { if (buckets[d].length > maxLen) maxLen = buckets[d].length; }
let baseY = H - 20;
let itemH = Math.min(16, (H - 50) / maxLen);
memCtx.fillStyle = '#8b949e'; memCtx.font = '600 11px Inter, sans-serif'; memCtx.textAlign = 'left';
memCtx.fillText('Radix-Buckets (Stelle ' + digitPos + ')', PAD, 14);
for (let d = 0; d < 10; d++) {
let bx = PAD + d * (slotW + GAP);
let isH = (d === hl);
// Bucket-Label
memCtx.fillStyle = isH ? COLORS.move : '#8b949e';
memCtx.font = '700 11px monospace'; memCtx.textAlign = 'center';
memCtx.fillText(String(d), bx + slotW / 2, baseY + 14);
// Elemente als gestapelte Blöcke
for (let ei = 0; ei < buckets[d].length; ei++) {
let y = baseY - (ei + 1) * itemH;
let color = isH ? COLORS.move : COLORS.normal;
memCtx.fillStyle = color; memCtx.globalAlpha = 0.85;
memCtx.fillRect(bx + 1, y + 1, slotW - 2, itemH - 2);
memCtx.globalAlpha = 1;
if (slotW > 18 && itemH > 8) {
memCtx.fillStyle = '#fff'; memCtx.font = '600 ' + Math.min(10, Math.min(slotW * 0.4, itemH * 0.7)) + 'px monospace';
memCtx.textAlign = 'center'; memCtx.textBaseline = 'middle';
memCtx.fillText(String(buckets[d][ei]), bx + slotW / 2, y + itemH / 2);
}
}
}
}
function drawBuckets(state, W, H) {
let buckets = state.buckets, hl = state.highlight;
let k = buckets.length;
if (k === 0) return;
let PAD = 20, GAP = 4;
let slotW = (W - PAD * 2 - GAP * (k - 1)) / k;
let maxLen = 1, maxV = 1;
for (let b = 0; b < k; b++) {
if (buckets[b].length > maxLen) maxLen = buckets[b].length;
for (let j = 0; j < buckets[b].length; j++) { if (buckets[b][j] > maxV) maxV = buckets[b][j]; }
}
let baseY = H - 20;
let aH = H * 0.55;
memCtx.fillStyle = '#8b949e'; memCtx.font = '600 11px Inter, sans-serif'; memCtx.textAlign = 'left';
memCtx.fillText('Buckets (' + k + ' St\u00fcck)', PAD, 14);
for (let b = 0; b < k; b++) {
let bx = PAD + b * (slotW + GAP);
let isH = (b === hl);
// Bucket-Rahmen
memCtx.strokeStyle = isH ? COLORS.move : '#3a3f55'; memCtx.lineWidth = 1;
memCtx.strokeRect(bx, 24, slotW, baseY - 24);
// Elemente als Balken von unten
let itemW = Math.max(1, (slotW - 4) / Math.max(1, buckets[b].length));
for (let j = 0; j < buckets[b].length; j++) {
let bH = Math.max(2, (buckets[b][j] / maxV) * aH);
let x = bx + 2 + j * itemW;
let y = baseY - bH;
let color = isH ? COLORS.move : COLORS.normal;
memCtx.fillStyle = color; memCtx.fillRect(x, y, Math.max(1, itemW - 1), bH);
}
// Bucket-Nummer
if (slotW > 12) {
memCtx.fillStyle = isH ? COLORS.move : '#8b949e';
memCtx.font = '600 ' + Math.min(10, slotW * 0.4) + 'px monospace'; memCtx.textAlign = 'center';
memCtx.fillText(String(b), bx + slotW / 2, baseY + 13);
}
}
}
// ================================================================
// Memory View: Label + Resize (Label nur bei Typwechsel, Resize immer)
// ================================================================
let _lastMemType = null;
function updateMemView(memState) {
if (!memState) {
$memContainer.classList.add('hidden');
_lastMemType = null;
return;
}
$memContainer.classList.remove('hidden');
// Label nur bei Typ-Wechsel aktualisieren (Performance)
if (memState.type !== _lastMemType) {
_lastMemType = memState.type;
if (memState.type === 'bst') {
$memLabel.innerHTML = '<i data-lucide="git-branch" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bin\u00e4rer Suchbaum (BST)';
} else if (memState.type === 'heap') {
$memLabel.innerHTML = '<i data-lucide="git-branch" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bin\u00e4rer Heap';
} else if (memState.type === 'merge') {
$memLabel.innerHTML = '<i data-lucide="hard-drive" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Hilfs-Arrays (Out-of-Place)';
} else if (memState.type === 'counting') {
$memLabel.innerHTML = '<i data-lucide="bar-chart-3" class="w-3.5 h-3.5"></i> Arbeitsspeicher: H\u00e4ufigkeits-Array';
} else if (memState.type === 'radix') {
$memLabel.innerHTML = '<i data-lucide="layers" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Radix-Buckets (10 Ziffern)';
} else if (memState.type === 'buckets') {
$memLabel.innerHTML = '<i data-lucide="inbox" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bucket-Verteilung';
}
lucide.createIcons({ nodes: [$memLabel] });
}
// Resize bei JEDEM Frame (fängt Window-Resize mit)
if (memState.type === 'bst') resizeCanvas(memCanvas, memCtx, 0.4, 400, 160);
else if (memState.type === 'heap') resizeCanvas(memCanvas, memCtx, 0.38, 380, 160);
else if (memState.type === 'merge') resizeCanvas(memCanvas, memCtx, 0.25, 250, 160);
else if (memState.type === 'counting') resizeCanvas(memCanvas, memCtx, 0.25, 250, 160);
else if (memState.type === 'radix') resizeCanvas(memCanvas, memCtx, 0.3, 300, 160);
else if (memState.type === 'buckets') resizeCanvas(memCanvas, memCtx, 0.3, 300, 160);
drawMemory(memState);
}
// ================================================================
// Render Engine
// ================================================================
function renderStep(step, stepNum) {
let hi = {};
step.indices.forEach(function(i) { hi[i] = step.type; });
let allSorted = step.type === 'done';
// Build connected pair for connection lines
let connectedPair = null;
if (!allSorted && step.indices.length >= 2 && (step.type === 'compare' || step.type === 'swap')) {
connectedPair = { indices: [step.indices[0], step.indices[1]], type: step.type };
}
drawBars(step.array, allSorted ? {} : hi, allSorted, step.meta, connectedPair);
setStats(step.compares, step.swaps, step.moves, stepNum);
updateMemView(step.mem);
updateProgress(stepNum, steps.length);
updatePhaseLabel(step.meta);
$stepExplanation.textContent = buildExplanation(step);
}
function renderCurrent() {
drawBars(array, {}, false);
setStats(0, 0, 0, 0);
$memContainer.classList.add('hidden');
_lastMemType = null;
updateProgress(0, 0);
$phaseLabel.innerHTML = '&nbsp;';
$stepExplanation.innerHTML = '&nbsp;';
}
// ================================================================
// Animation Engine
// ================================================================
function scheduleNext() {
if (animTimer !== null) clearInterval(animTimer);
let delay = getDelay();
// Start timing on first tick
if (startTime === 0) startTime = performance.now();
animTimer = window.setInterval(tick, delay);
}
function tick() {
if (stepIndex >= steps.length) { finishAnimation(); return; }
renderStep(steps[stepIndex], stepIndex + 1);
stepIndex++;
// Update elapsed time display
elapsedMs = Math.round(performance.now() - startTime);
$statTime.textContent = elapsedMs + ' ms';
if (stepIndex >= steps.length) finishAnimation();
}
function finishAnimation() {
if (animTimer !== null) { clearInterval(animTimer); animTimer = null; }
isRunning = false;
isPaused = false;
const last = steps[steps.length - 1];
if (last) {
drawBars(last.array, {}, true);
setStats(last.compares, last.swaps, last.moves, steps.length);
}
elapsedMs = Math.round(performance.now() - startTime);
$statTime.textContent = elapsedMs + ' ms';
updateProgress(steps.length, steps.length);
$phaseLabel.innerHTML = '&nbsp;';
$stepExplanation.textContent = 'Sortierung abgeschlossen \u2713';
updateButtonStates();
}
function stopTimer() {
if (animTimer !== null) { clearInterval(animTimer); animTimer = null; }
}
// ================================================================
// Reset
// ================================================================
// Reset
// ================================================================
function doReset() {
stopTimer();
isRunning = false;
isPaused = false;
steps = [];
stepIndex = 0;
startTime = 0;
elapsedMs = 0;
generateArray();
arrayFresh = true;
setStats(0, 0, 0, 0);
$statTime.textContent = '0 ms';
renderCurrent();
updateButtonStates();
}
// ================================================================
// Button Event Handlers
// ================================================================
// Combined Play/Pause button
$btnPlayPause.addEventListener('click', function() {
if (isRunning && !isPaused) {
stopTimer();
isPaused = true;
updateButtonStates();
return;
}
if (!isRunning) {
if (!arrayFresh) { generateArray(); }
arrayFresh = false;
renderCurrent();
steps = buildSteps($algoSelect.value);
stepIndex = 0;
startTime = 0;
elapsedMs = 0;
setStats(0, 0, 0, 0);
$statTime.textContent = '0 ms';
}
isRunning = true;
isPaused = false;
updateButtonStates();
scheduleNext();
});
// Step forward
$btnStep.addEventListener('click', function() {
if (!isRunning) {
if (!arrayFresh) { generateArray(); }
arrayFresh = false;
renderCurrent();
steps = buildSteps($algoSelect.value);
stepIndex = 0;
startTime = 0;
elapsedMs = 0;
setStats(0, 0, 0, 0);
$statTime.textContent = '0 ms';
isRunning = true;
isPaused = true;
}
stopTimer();
isPaused = true;
updateButtonStates();
if (stepIndex < steps.length) {
renderStep(steps[stepIndex], stepIndex + 1);
stepIndex++;
if (stepIndex >= steps.length) finishAnimation();
}
});
// Step back
$btnStepBack.addEventListener('click', function() {
if (!isRunning || stepIndex <= 0) return;
stopTimer();
isPaused = true;
if (stepIndex > 1) {
stepIndex--;
renderStep(steps[stepIndex - 1], stepIndex);
} else {
stepIndex = 0;
renderCurrent();
}
updateButtonStates();
});
$btnReset.addEventListener('click', doReset);
$btnShuffle.addEventListener('click', function() {
stopTimer();
isRunning = false;
isPaused = false;
// Fisher-Yates Shuffle on current array
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
if (j !== i) {
[array[i], array[j]] = [array[j], array[i]];
}
}
steps = [];
stepIndex = 0;
startTime = 0;
elapsedMs = 0;
setStats(0, 0, 0, 0);
$statTime.textContent = '0 ms';
renderCurrent();
updateButtonStates();
$stepExplanation.textContent = 'Array gemischt \u2713';
});
// ================================================================
// Slider & Input Events
// ================================================================
$speedSlider.addEventListener('input', function() {
updateSpeedLabel();
if (isRunning && !isPaused) scheduleNext();
writeUrlState();
});
$sizeSlider.addEventListener('input', function() {
updateSizeLabel();
if (!isRunning) doReset();
writeUrlState();
});
$algoSelect.addEventListener('change', function() {
if (!isRunning) doReset();
writeUrlState();
});
$presetSelect.addEventListener('change', function() {
if (!isRunning) doReset();
writeUrlState();
});
$customArray.addEventListener('change', function() {
if (!isRunning) doReset();
updateCustomArrayCount();
});
$customArray.addEventListener('input', function() {
updateCustomArrayCount();
});
function updateCustomArrayCount() {
const text = $customArray.value.trim();
if (!text) {
$customArrayCount.textContent = '';
return;
}
const values = text.split(/[,;\s\n\t]+/).filter(function(v) { return v !== '' && !isNaN(parseFloat(v)); });
$customArrayCount.textContent = values.length + ' Wert' + (values.length === 1 ? '' : 'e');
}
$btnClearCustom.addEventListener('click', function() {
$customArray.value = '';
$customArrayCount.textContent = '';
$sizeSlider.disabled = false;
if (!isRunning) doReset();
});
$btnPasteCustom.addEventListener('click', async function() {
try {
const text = await navigator.clipboard.readText();
$customArray.value = text.replace(/[\r\n]+/g, ', ').replace(/,/g, ', ');
updateCustomArrayCount();
$customArray.dispatchEvent(new Event('change'));
} catch (e) {
// Clipboard API not available or permission denied
}
});
$btnTheme.addEventListener('click', function() {
const isDark = document.documentElement.classList.contains('dark');
const newTheme = !isDark;
localStorage.setItem('sortvis-theme', newTheme ? 'dark' : 'light');
applyTheme(newTheme);
});
// ================================================================
// Keyboard Shortcuts
// ================================================================
document.addEventListener('keydown', function(e) {
// Ignoriere Events in Input-Feldern
if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT' || e.target.tagName === 'TEXTAREA') return;
switch (e.key) {
case ' ':
case 'k':
e.preventDefault();
$btnPlayPause.click();
break;
case 'ArrowRight':
case 'l':
e.preventDefault();
$btnStep.click();
break;
case 'ArrowLeft':
case 'j':
e.preventDefault();
$btnStepBack.click();
break;
case 'r':
e.preventDefault();
$btnReset.click();
break;
case 't':
e.preventDefault();
$btnTheme.click();
break;
2026-04-06 21:00:49 +02:00
case 's':
e.preventDefault();
$btnShuffle.click();
break;
}
});
// ================================================================
// Touch / Pointer Support for Sliders
// ================================================================
function makeTouchSlider(slider) {
let active = false;
function valFromX(clientX) {
const rect = slider.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const min = parseFloat(slider.min);
const max = parseFloat(slider.max);
return Math.round(min + ratio * (max - min));
}
slider.addEventListener('pointerdown', function(e) {
slider.setPointerCapture(e.pointerId);
active = true;
});
slider.addEventListener('pointermove', function(e) {
if (!active) return;
e.preventDefault();
slider.value = String(valFromX(e.clientX));
slider.dispatchEvent(new Event('input'));
}, { passive: false });
slider.addEventListener('pointerup', function() { active = false; });
slider.addEventListener('pointercancel', function() { active = false; });
}
makeTouchSlider($sizeSlider);
makeTouchSlider($speedSlider);
// ================================================================
// URL State Persistence
// ================================================================
function readUrlState() {
const params = new URLSearchParams(window.location.search);
if (params.has('algo')) $algoSelect.value = params.get('algo');
if (params.has('preset')) $presetSelect.value = params.get('preset');
if (params.has('size')) $sizeSlider.value = Math.max(5, Math.min(200, parseInt(params.get('size'), 10) || 50));
if (params.has('speed')) $speedSlider.value = Math.max(1, Math.min(100, parseInt(params.get('speed'), 10) || 50));
updateSizeLabel();
updateSpeedLabel();
}
function writeUrlState() {
const params = new URLSearchParams();
params.set('algo', $algoSelect.value);
params.set('preset', $presetSelect.value);
params.set('size', $sizeSlider.value);
params.set('speed', $speedSlider.value);
const url = window.location.pathname + '?' + params.toString();
window.history.replaceState(null, '', url);
}
// ================================================================
// Window Resize (ResizeObserver + fallback)
// ================================================================
let _resizeTimer = 0;
function handleResize() {
clearTimeout(_resizeTimer);
_resizeTimer = setTimeout(function() {
resizeCanvas();
if (steps.length > 0 && stepIndex > 0) {
const s = steps[Math.min(stepIndex - 1, steps.length - 1)];
_lastMemType = null;
renderStep(s, stepIndex);
} else {
renderCurrent();
}
}, 150);
}
// Use window resize to avoid feedback loops (ResizeObserver on canvas parent causes blowout)
window.addEventListener('resize', handleResize, { passive: true });
// ================================================================
// Initialization
// ================================================================
lucide.createIcons();
// Apply stored theme (after icons are created so icon visibility toggle works)
(function initTheme() {
const stored = localStorage.getItem('sortvis-theme');
const prefersDark = stored ? stored === 'dark' : true;
applyTheme(prefersDark, true); // skipRender=true during init
})();
// Restore state from URL
readUrlState();
refreshColors();
updateSpeedLabel();
updateSizeLabel();
resizeCanvas();
generateArray();
arrayFresh = true;
renderCurrent();
updateButtonStates();
updateProgress(0, 0);
// Entry-Animationen mit CSS @keyframes (GSAP entfernt)
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReducedMotion) {
document.querySelector('header').classList.add('anim-header');
document.querySelectorAll('section, details').forEach(function(el, i) {
el.style.animationDelay = (0.15 + i * 0.07) + 's';
el.classList.add('anim-section');
});
document.querySelectorAll('.stat-card').forEach(function(el, i) {
el.style.animationDelay = (0.35 + i * 0.06) + 's';
el.classList.add('anim-stat');
});
}
</script>
<footer class="text-center text-xs text-muted py-6 border-t border-border mt-8">
&copy; 2026 Dieter Schl&uuml;ter &lt;dieter(dot)schlueter(at)linux(dot)de&gt;
</footer>
</body>
</html>