2894 lines
151 KiB
HTML
2894 lines
151 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de" class="dark">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<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>
|
||
|
||
<!-- GSAP -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.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 ── */
|
||
body { font-size: clamp(14px, 1.6vw, 16px); }
|
||
.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; }
|
||
|
||
/* ── 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;
|
||
}
|
||
}
|
||
|
||
/* ── 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);">
|
||
Sortier-Algorithmen <span style="font-size: 0.45em; font-weight: 400; opacity: 0.5; vertical-align: middle;">v0.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 overflow-hidden">
|
||
|
||
<!-- 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="Parallel — Netzwerk-Verfahren">
|
||
<option value="bitonic">Bitonic Sort</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>
|
||
</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
|
||
</label>
|
||
<div class="flex gap-1.5">
|
||
<input type="text" id="customArray" placeholder="z.B. 5, 3, 8, 1"
|
||
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="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>
|
||
|
||
<!-- Threads -->
|
||
<div class="space-y-1.5">
|
||
<label for="threadSlider" class="label-text uppercase tracking-wider text-muted font-medium flex items-center gap-1.5">
|
||
<i data-lucide="cpu" class="w-3.5 h-3.5"></i> Threads
|
||
<span id="threadVal" class="text-accent font-mono ml-auto">1</span>
|
||
</label>
|
||
<input type="range" id="threadSlider" min="1" max="1" value="1" class="w-full h-6" disabled>
|
||
</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-4 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="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" class="text-xs font-semibold text-accent truncate" style="min-height: 20px;"> </div>
|
||
<div id="stepExplanation" class="text-xs text-muted truncate font-mono" style="min-height: 18px;"> </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" class="text-xs text-muted font-mono whitespace-nowrap">0 / 0</span>
|
||
</div>
|
||
|
||
<!-- Statistics -->
|
||
<section class="grid grid-cols-2 lg:grid-cols-4 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="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="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="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="text-[10px] mt-1 leading-snug" style="color: var(--c-muted); opacity: 0.7;">Der aktuell ausgeführte Visualisierungsschritt.</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, Bucket Sort und Bitonic Sort wird der Arbeitsspeicher unterhalb des Arrays visualisiert. Sie sehen genau, wo Daten zwischengespeichert werden — ob als Baumstruktur (BST, Heap), Hilfs-Arrays (Merge), Häufigkeitstabelle (Counting), Bucket-Verteilung (Radix, Bucket) oder Sortiernetz (Bitonic).</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ü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 ä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ä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ötigt ein zusätzliches Hilfsarray proportional zur Eingabegröß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 — 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²)</span> Zeitkomplexität — beschreibt, wie die Laufzeit mit der Eingabegröße wächst. <strong class="text-gray-400">O(n²)</strong> = quadratisch (langsam bei groß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öglich). Stehen zwei Werte (z.B. „O(n²) worst · O(n) best“), 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ü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 < 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 (< Pivot und > 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 < P1 (links), Elemente zwischen P1 und P2 (Mitte), Elemente > 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 10–20 % 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 -->
|
||
<div>
|
||
<h3 class="text-xs uppercase tracking-wider text-muted font-medium mb-3">Parallel — Netzwerk-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">Bitonic 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">Parallelisierbar</span>
|
||
<span class="text-[10px] px-2 py-0.5 rounded-full bg-surface border border-border text-gray-400">O(n log²n)</span>
|
||
</div>
|
||
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Funktionsweise:</strong> Basiert auf dem Konzept der „bitonischen Sequenz" — einer Folge, die erst steigt und dann fällt (oder umgekehrt). Der Algorithmus arbeitet in Phasen: Zuerst werden Paare zu bitonischen Sequenzen der Länge 2 sortiert (abwechselnd aufsteigend/absteigend). Dann werden diese zu bitonischen Sequenzen der Länge 4 gemergt, dann 8, usw. Der „Bitonic Merge" vergleicht dabei Elemente mit festem Abstand und tauscht sie so, dass eine sortierte Sequenz entsteht. Das Vergleichsmuster ist datenunabhängig — es steht vor der Ausführung fest.</p>
|
||
<p class="text-xs text-muted leading-relaxed mb-2"><strong class="text-gray-300">Darstellung:</strong> Im Haupt-Canvas verbinden orange Bögen die verglichenen Positionen — sie springen in einem streng regelmäßigen Muster durch das Array, das sich von den Daten völlig unbeeindruckt zeigt; rote Bögen zeigen Tausche. Unterhalb erscheint das vollständige <strong class="text-gray-300">Sortiernetz</strong>: n horizontale Drähte repräsentieren die Array-Positionen, senkrechte Komparatoren verbinden jeweils die verglichenen Positionen. Vergangene Phasen leuchten gedimmt blau, die aktuelle orange mit Leuchteffekt, künftige Phasen sind abgedimmt — so ist das vollständige, datenunabhängige Vergleichsprogramm auf einen Blick sichtbar.</p>
|
||
<p class="text-xs text-muted leading-relaxed"><strong class="text-gray-300">Wann einsetzen:</strong> Der Algorithmus der Wahl für GPU-Sortierung und Hardware-Implementierungen (FPGAs, ASICs). Da das Vergleichsnetzwerk vollständig vorab feststeht und keine Datenabhängigkeiten hat, können alle Vergleiche einer Phase echt parallel ausgeführt werden. Auf einer GPU mit p Prozessoren erreicht Bitonic Sort O(n/p · log²n). Wird in CUDA-Bibliotheken, Grafik-Pipelines und überall dort eingesetzt, wo massiv parallele Hardware zur Verfügung steht. Die Einschränkung: Array-Größe muss eine Zweierpotenz sein (wird intern aufgefüllt).</p>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 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 1–6, Alter 0–150, 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 0–9) 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 Ausprobieren 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>
|
||
</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 $sizeSlider = document.getElementById('sizeSlider');
|
||
const $speedSlider = document.getElementById('speedSlider');
|
||
const $threadSlider = document.getElementById('threadSlider');
|
||
const $sizeVal = document.getElementById('sizeVal');
|
||
const $speedVal = document.getElementById('speedVal');
|
||
const $threadVal = document.getElementById('threadVal');
|
||
const $statComp = document.getElementById('statCompares');
|
||
const $statSwap = document.getElementById('statSwaps');
|
||
const $statMove = document.getElementById('statMoves');
|
||
const $statStep = document.getElementById('statStep');
|
||
const $btnPlayPause = document.getElementById('btnPlayPause');
|
||
const $btnStepBack = document.getElementById('btnStepBack');
|
||
const $btnStep = document.getElementById('btnStep');
|
||
const $btnReset = document.getElementById('btnReset');
|
||
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
|
||
|
||
// ================================================================
|
||
// 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;
|
||
}
|
||
|
||
// Parallele Algorithmen: Bitonic Sort
|
||
const PARALLEL_ALGOS = new Set(['bitonic']);
|
||
|
||
// Algorithmen mit Memory-Visualisierung
|
||
const MEM_ALGOS = new Set(['tree', 'merge', 'timsort', 'heap', 'counting', 'radix', 'bucket']);
|
||
const MAX_THREADS = navigator.hardwareConcurrency || 4;
|
||
|
||
function updateThreadSlider() {
|
||
const isParallel = PARALLEL_ALGOS.has($algoSelect.value);
|
||
$threadSlider.disabled = !isParallel;
|
||
if (isParallel) {
|
||
$threadSlider.max = String(MAX_THREADS);
|
||
} else {
|
||
$threadSlider.max = '1';
|
||
$threadSlider.value = '1';
|
||
}
|
||
$threadVal.textContent = $threadSlider.value;
|
||
}
|
||
|
||
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 = ' ';
|
||
}
|
||
}
|
||
|
||
function buildExplanation(step) {
|
||
if (!step) return '\u00A0';
|
||
const idx = step.indices;
|
||
const arr = step.array;
|
||
function fmtVal(i) {
|
||
var 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) {
|
||
var nodes = [];
|
||
for (var hi = 0; hi < heapSize; hi++) {
|
||
var 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
|
||
var maxD = nodes.length > 0 ? nodes[nodes.length - 1].depth : 0;
|
||
var drawNodes = [];
|
||
for (var hi = 0; hi < nodes.length; hi++) {
|
||
var nd = nodes[hi], d = nd.depth;
|
||
// Position in der Ebene
|
||
var posInLevel = hi - (Math.pow(2, d) - 1);
|
||
var levelCount = Math.min(Math.pow(2, d), heapSize - (Math.pow(2, d) - 1));
|
||
var totalWidth = Math.pow(2, maxD);
|
||
var 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]);
|
||
const minVal = Math.min(...arr);
|
||
const maxVal = Math.max(...arr);
|
||
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]);
|
||
const maxVal = Math.max(...arr);
|
||
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);
|
||
var maxD = 0;
|
||
for (var 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;
|
||
// 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]);
|
||
if (arr[j] > key) {
|
||
arr[j + 1] = arr[j];
|
||
moves++;
|
||
snap('move', [j + 1]);
|
||
j--;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
if (arr[j + 1] !== key) {
|
||
arr[j + 1] = key;
|
||
moves++;
|
||
snap('move', [j + 1]);
|
||
}
|
||
}
|
||
}
|
||
// 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 'bitonic': {
|
||
const n = arr.length;
|
||
// Arbeite auf separatem Array mit Zweierpotenz-Länge
|
||
let size = 1;
|
||
while (size < n) size <<= 1;
|
||
// Padding: eindeutige Werte oberhalb des Maximums (alle > origMax → Sortierung korrekt,
|
||
// aber unterschiedlich → Balken visuell unterscheidbar wenn sie in sichtbare Positionen geraten)
|
||
var origMax = 0;
|
||
for (let i = 0; i < n; i++) { if (arr[i] > origMax) origMax = arr[i]; }
|
||
const bArr = arr.slice();
|
||
var padLo = origMax * 1.05 + 1;
|
||
var padHi = origMax * 1.25 + 1;
|
||
var padCount = size - n;
|
||
for (let i = n; i < size; i++) {
|
||
bArr.push(padCount > 1 ? padLo + (padHi - padLo) * (i - n) / (padCount - 1) : padLo);
|
||
}
|
||
|
||
// Vorberechnung des Sortiernetzes (datenunabhängig)
|
||
const allStages = [];
|
||
function collectMergeStages(lo, cnt) {
|
||
if (cnt <= 1) return;
|
||
var half = cnt >> 1;
|
||
var stage = [];
|
||
for (var i = lo; i < lo + half; i++) {
|
||
if (i < n && (i + half) < n) stage.push([i, i + half]);
|
||
}
|
||
if (stage.length > 0) allStages.push(stage);
|
||
collectMergeStages(lo, half);
|
||
collectMergeStages(lo + half, half);
|
||
}
|
||
function collectSortStages(lo, cnt) {
|
||
if (cnt <= 1) return;
|
||
var half = cnt >> 1;
|
||
collectSortStages(lo, half);
|
||
collectSortStages(lo + half, half);
|
||
collectMergeStages(lo, cnt);
|
||
}
|
||
collectSortStages(0, size);
|
||
|
||
// Stage-Index: synchron mit bitonicMerge hochgezählt
|
||
var snapStageId = 0;
|
||
|
||
// Snap-Wrapper: zeigt nur die ersten n Elemente + Netz-Zustand
|
||
function bSnap(type, indices) {
|
||
var visIdx = indices.filter(function(idx) { return idx < n; });
|
||
for (var k = 0; k < n; k++) arr[k] = bArr[k];
|
||
out.push({
|
||
type: type, indices: visIdx, array: arr.slice(),
|
||
compares: compares, swaps: swaps, moves: moves,
|
||
mem: { type: 'bitonic_network', stages: allStages, currentStage: snapStageId, n: n }
|
||
});
|
||
}
|
||
|
||
function bitonicCompareSwap(i, j, dir) {
|
||
if (i < n && j < n) {
|
||
compares++;
|
||
bSnap('compare', [i, j]);
|
||
}
|
||
if ((bArr[i] > bArr[j]) === dir) {
|
||
var tmp = bArr[i]; bArr[i] = bArr[j]; bArr[j] = tmp;
|
||
if (i < n && j < n) {
|
||
swaps++;
|
||
bSnap('swap', [i, j]);
|
||
}
|
||
}
|
||
}
|
||
function bitonicMerge(lo, cnt, dir) {
|
||
if (cnt <= 1) return;
|
||
var half = cnt >> 1;
|
||
var hasVisible = false;
|
||
for (var i = lo; i < lo + half; i++) {
|
||
if (i < n && (i + half) < n) hasVisible = true;
|
||
bitonicCompareSwap(i, i + half, dir);
|
||
}
|
||
if (hasVisible) snapStageId++; // nächste Phase
|
||
bitonicMerge(lo, half, dir);
|
||
bitonicMerge(lo + half, half, dir);
|
||
}
|
||
function bitonicSortRec(lo, cnt, dir) {
|
||
if (cnt <= 1) return;
|
||
var half = cnt >> 1;
|
||
bitonicSortRec(lo, half, true);
|
||
bitonicSortRec(lo + half, half, false);
|
||
bitonicMerge(lo, cnt, dir);
|
||
}
|
||
bitonicSortRec(0, size, true);
|
||
// Ergebnis zurückkopieren (nur die ersten n Elemente, Padding verwerfen)
|
||
for (var i = 0; i < n; i++) arr[i] = bArr[i];
|
||
break;
|
||
}
|
||
|
||
case 'bucket': {
|
||
const n = arr.length;
|
||
if (n === 0) break;
|
||
const minVal = Math.min(...arr);
|
||
const maxVal = Math.max(...arr);
|
||
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 '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();
|
||
|
||
const maxVal = Math.max(...arr, 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)';
|
||
var 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) {
|
||
var ax0 = PAD_L + meta.activeRange.lo * slotW + gapW / 2;
|
||
var 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) {
|
||
var partColors = ['rgba(96,165,250,0.05)', 'rgba(255,154,60,0.05)', 'rgba(185,74,255,0.05)'];
|
||
for (var pi = 0; pi < meta.partitions.length; pi++) {
|
||
var part = meta.partitions[pi];
|
||
ctx.fillStyle = partColors[pi % partColors.length];
|
||
var px0 = PAD_L + part.lo * slotW;
|
||
var 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 (var ri = 0; ri < meta.runs.length; ri++) {
|
||
var 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';
|
||
|
||
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;
|
||
}
|
||
|
||
// Rounded-top bar path generator
|
||
const r = Math.min(3, barW / 4);
|
||
function drawPath() {
|
||
if (r > 0 && barH > r * 2) {
|
||
ctx.beginPath();
|
||
ctx.moveTo(x + r, y);
|
||
ctx.lineTo(x + barW - r, y);
|
||
ctx.quadraticCurveTo(x + barW, y, x + barW, y + r);
|
||
ctx.lineTo(x + barW, y + barH);
|
||
ctx.lineTo(x, y + barH);
|
||
ctx.lineTo(x, y + r);
|
||
ctx.quadraticCurveTo(x, y, x + r, y);
|
||
ctx.closePath();
|
||
} else {
|
||
ctx.beginPath();
|
||
ctx.rect(x, y, barW, barH);
|
||
}
|
||
}
|
||
|
||
// FIX: Glow-Effekt VOR dem normalen Balken zeichnen
|
||
if (highlights[i] && !allSorted) {
|
||
ctx.save();
|
||
ctx.fillStyle = color;
|
||
ctx.shadowColor = color;
|
||
ctx.shadowBlur = 14;
|
||
ctx.globalAlpha = 0.35;
|
||
drawPath();
|
||
ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
|
||
// Haupt-Balken zeichnen
|
||
ctx.fillStyle = color;
|
||
ctx.globalAlpha = 1;
|
||
drawPath();
|
||
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 (var pvi = 0; pvi < meta.pivots.length; pvi++) {
|
||
var pvIdx = meta.pivots[pvi];
|
||
if (pvIdx >= 0 && pvIdx < n) {
|
||
var pvBarH = Math.max(2, (arr[pvIdx] / maxVal) * areaH);
|
||
var pvX = PAD_L + pvIdx * slotW + gapW / 2;
|
||
var 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;
|
||
var dpr = window.devicePixelRatio || 1;
|
||
var 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);
|
||
else if (state.type === 'bitonic_network') drawBitonicNetwork(state, W, H);
|
||
}
|
||
|
||
function drawBST(state, W, H) {
|
||
var nodes = state.nodes, totalWidth = state.totalWidth, maxDepth = state.maxDepth, highlight = state.highlight;
|
||
if (!nodes || nodes.length === 0) return;
|
||
var PAD = 40, aW = W - PAD * 2, aH = H - PAD * 2;
|
||
var stepX = totalWidth > 1 ? aW / (totalWidth - 1) : 0;
|
||
var stepY = maxDepth > 0 ? aH / maxDepth : 0;
|
||
// Knoten-Radius skaliert mit Anzahl
|
||
var R = Math.max(6, Math.min(20, stepX * 0.4));
|
||
|
||
// Kanten zeichnen
|
||
memCtx.strokeStyle = '#3a3f55'; memCtx.lineWidth = 2;
|
||
for (var i = 0; i < nodes.length; i++) {
|
||
var p = nodes[i], px = PAD + p.drawX * stepX, py = PAD + p.drawY * stepY;
|
||
var children = [p.left, p.right];
|
||
for (var ci = 0; ci < children.length; ci++) {
|
||
var cIdx = children[ci];
|
||
if (cIdx !== -1 && nodes[cIdx]) {
|
||
var 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 (var i = 0; i < nodes.length; i++) {
|
||
var n = nodes[i], x = PAD + n.drawX * stepX, y = PAD + n.drawY * stepY;
|
||
var isH = highlight.indexOf(i) !== -1;
|
||
var 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) {
|
||
var left = state.left, right = state.right, lPtr = state.lPtr, rPtr = state.rPtr;
|
||
var totalLen = left.length + right.length;
|
||
if (totalLen === 0) return;
|
||
|
||
// Sicheres Maximum ohne Spread (Stack-Overflow-Schutz bei großen Arrays)
|
||
var maxV = 1;
|
||
for (var mi = 0; mi < left.length; mi++) { if (left[mi] > maxV) maxV = left[mi]; }
|
||
for (var mi = 0; mi < right.length; mi++) { if (right[mi] > maxV) maxV = right[mi]; }
|
||
|
||
var PAD = 20, GAP = 20;
|
||
var isNarrow = W < 400;
|
||
|
||
if (isNarrow) {
|
||
// Mobile: Arrays untereinander
|
||
var sW = (W - PAD * 2) / Math.max(left.length, right.length);
|
||
var bW = sW * 0.7, gap = sW * 0.3;
|
||
var 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 (var i = 0; i < left.length; i++) {
|
||
var x = PAD + i * sW + gap / 2, bH = Math.max(2, (left[i] / maxV) * aH), y = baseY1 - bH;
|
||
var 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 (var i = 0; i < right.length; i++) {
|
||
var x = PAD + i * sW + gap / 2, bH = Math.max(2, (right[i] / maxV) * aH), y = baseY2 - bH;
|
||
var 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
|
||
var totalW = W - PAD * 2 - GAP;
|
||
var sW = totalW / totalLen, bW = sW * 0.7, gap = sW * 0.3;
|
||
var 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 (var i = 0; i < left.length; i++) {
|
||
var x = PAD + i * sW + gap / 2, bH = Math.max(2, (left[i] / maxV) * aH), y = baseY - bH;
|
||
var 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
|
||
var 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 (var i = 0; i < right.length; i++) {
|
||
var x = rightStartX + i * sW + gap / 2, bH = Math.max(2, (right[i] / maxV) * aH), y = baseY - bH;
|
||
var 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) {
|
||
var nodes = state.nodes, totalWidth = state.totalWidth, maxDepth = state.maxDepth, highlight = state.highlight;
|
||
if (!nodes || nodes.length === 0) return;
|
||
var PAD = 30, aW = W - PAD * 2, aH = H - PAD * 2;
|
||
var stepY = maxDepth > 0 ? aH / maxDepth : 0;
|
||
var R = Math.max(6, Math.min(18, aW / (nodes.length + 1) * 0.35));
|
||
|
||
// Kanten
|
||
memCtx.strokeStyle = '#3a3f55'; memCtx.lineWidth = 1.5;
|
||
for (var i = 0; i < nodes.length; i++) {
|
||
var nd = nodes[i], px = PAD + (nd.drawX / totalWidth) * aW, py = PAD + nd.drawY * stepY;
|
||
var children = [nd.left, nd.right];
|
||
for (var ci = 0; ci < children.length; ci++) {
|
||
var cIdx = children[ci];
|
||
if (cIdx !== -1 && nodes[cIdx]) {
|
||
var 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 (var i = 0; i < nodes.length; i++) {
|
||
var n = nodes[i], x = PAD + (n.drawX / totalWidth) * aW, y = PAD + n.drawY * stepY;
|
||
var isH = highlight.indexOf(i) !== -1;
|
||
var 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) {
|
||
var count = state.count, minVal = state.minVal, hl = state.highlight;
|
||
var n = count.length;
|
||
if (n === 0) return;
|
||
var maxC = 1;
|
||
for (var i = 0; i < n; i++) { if (count[i] > maxC) maxC = count[i]; }
|
||
var PAD = 20, aW = W - PAD * 2, aH = H * 0.55, baseY = H - 20;
|
||
var 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 (var i = 0; i < n; i++) {
|
||
var x = PAD + i * sW + gap;
|
||
var bH = count[i] > 0 ? Math.max(2, (count[i] / maxC) * aH) : 0;
|
||
var y = baseY - bH;
|
||
var isH = (i === hl);
|
||
var 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) {
|
||
var buckets = state.buckets, digitPos = state.digitPos, hl = state.highlight;
|
||
var PAD = 20, GAP = 6;
|
||
var totalSlots = 10;
|
||
var slotW = (W - PAD * 2 - GAP * 9) / totalSlots;
|
||
var maxLen = 1;
|
||
for (var d = 0; d < 10; d++) { if (buckets[d].length > maxLen) maxLen = buckets[d].length; }
|
||
var baseY = H - 20;
|
||
var 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 (var d = 0; d < 10; d++) {
|
||
var bx = PAD + d * (slotW + GAP);
|
||
var 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 (var ei = 0; ei < buckets[d].length; ei++) {
|
||
var y = baseY - (ei + 1) * itemH;
|
||
var 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) {
|
||
var buckets = state.buckets, hl = state.highlight;
|
||
var k = buckets.length;
|
||
if (k === 0) return;
|
||
var PAD = 20, GAP = 4;
|
||
var slotW = (W - PAD * 2 - GAP * (k - 1)) / k;
|
||
var maxLen = 1, maxV = 1;
|
||
for (var b = 0; b < k; b++) {
|
||
if (buckets[b].length > maxLen) maxLen = buckets[b].length;
|
||
for (var j = 0; j < buckets[b].length; j++) { if (buckets[b][j] > maxV) maxV = buckets[b][j]; }
|
||
}
|
||
var baseY = H - 20;
|
||
var 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 (var b = 0; b < k; b++) {
|
||
var bx = PAD + b * (slotW + GAP);
|
||
var 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
|
||
var itemW = Math.max(1, (slotW - 4) / Math.max(1, buckets[b].length));
|
||
for (var j = 0; j < buckets[b].length; j++) {
|
||
var bH = Math.max(2, (buckets[b][j] / maxV) * aH);
|
||
var x = bx + 2 + j * itemW;
|
||
var y = baseY - bH;
|
||
var 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);
|
||
}
|
||
}
|
||
}
|
||
|
||
function drawBitonicNetwork(state, W, H) {
|
||
var stages = state.stages, cur = state.currentStage, n = state.n;
|
||
var numS = stages.length;
|
||
if (numS === 0 || n < 2) return;
|
||
|
||
var PAD_L = 24, PAD_R = 8, PAD_T = 10, PAD_B = 10;
|
||
var wireSpacing = (H - PAD_T - PAD_B) / Math.max(n - 1, 1);
|
||
var stageW = (W - PAD_L - PAD_R) / numS;
|
||
|
||
// Horizontale Drähte
|
||
for (var w = 0; w < n; w++) {
|
||
var wy = PAD_T + w * wireSpacing;
|
||
memCtx.strokeStyle = 'rgba(100,120,160,0.3)';
|
||
memCtx.lineWidth = 1;
|
||
memCtx.beginPath();
|
||
memCtx.moveTo(PAD_L, wy);
|
||
memCtx.lineTo(W - PAD_R, wy);
|
||
memCtx.stroke();
|
||
}
|
||
|
||
// Index-Labels links
|
||
memCtx.fillStyle = 'rgba(140,160,190,0.5)';
|
||
memCtx.font = '500 9px Inter, sans-serif';
|
||
memCtx.textAlign = 'right';
|
||
memCtx.textBaseline = 'middle';
|
||
for (var w = 0; w < n; w++) {
|
||
memCtx.fillText(String(w), PAD_L - 4, PAD_T + w * wireSpacing);
|
||
}
|
||
|
||
// Komparatoren
|
||
for (var s = 0; s < numS; s++) {
|
||
var cx = PAD_L + (s + 0.5) * stageW;
|
||
var isCur = (s === cur);
|
||
var isPast = (s < cur);
|
||
var color = isCur ? COLORS.compare
|
||
: isPast ? 'rgba(60,160,255,0.35)'
|
||
: 'rgba(150,170,200,0.15)';
|
||
var lw = isCur ? 2 : 1;
|
||
var R = isCur ? 3 : 2;
|
||
|
||
for (var ci = 0; ci < stages[s].length; ci++) {
|
||
var a = stages[s][ci][0], b = stages[s][ci][1];
|
||
var y1 = PAD_T + a * wireSpacing, y2 = PAD_T + b * wireSpacing;
|
||
if (isCur) {
|
||
memCtx.save();
|
||
memCtx.shadowColor = COLORS.compare;
|
||
memCtx.shadowBlur = 8;
|
||
memCtx.strokeStyle = color; memCtx.lineWidth = lw;
|
||
memCtx.beginPath(); memCtx.moveTo(cx, y1); memCtx.lineTo(cx, y2); memCtx.stroke();
|
||
memCtx.restore();
|
||
} else {
|
||
memCtx.strokeStyle = color; memCtx.lineWidth = lw;
|
||
memCtx.beginPath(); memCtx.moveTo(cx, y1); memCtx.lineTo(cx, y2); memCtx.stroke();
|
||
}
|
||
memCtx.fillStyle = color;
|
||
memCtx.beginPath(); memCtx.arc(cx, y1, R, 0, Math.PI * 2); memCtx.fill();
|
||
memCtx.beginPath(); memCtx.arc(cx, y2, R, 0, Math.PI * 2); memCtx.fill();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// Memory View: Label + Resize (nur bei Typwechsel, nicht bei jedem Frame)
|
||
// ================================================================
|
||
var _lastMemType = null;
|
||
|
||
function updateMemView(memState) {
|
||
if (!memState) {
|
||
$memContainer.classList.add('hidden');
|
||
_lastMemType = null;
|
||
return;
|
||
}
|
||
$memContainer.classList.remove('hidden');
|
||
// Label und Resize nur bei Typ-Wechsel (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)';
|
||
resizeCanvas(memCanvas, memCtx, 0.4, 400, 160);
|
||
} else if (memState.type === 'heap') {
|
||
$memLabel.innerHTML = '<i data-lucide="git-branch" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bin\u00e4rer Heap';
|
||
resizeCanvas(memCanvas, memCtx, 0.38, 380, 160);
|
||
} 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)';
|
||
resizeCanvas(memCanvas, memCtx, 0.25, 250, 160);
|
||
} 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';
|
||
resizeCanvas(memCanvas, memCtx, 0.25, 250, 160);
|
||
} else if (memState.type === 'radix') {
|
||
$memLabel.innerHTML = '<i data-lucide="layers" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Radix-Buckets (10 Ziffern)';
|
||
resizeCanvas(memCanvas, memCtx, 0.3, 300, 160);
|
||
} else if (memState.type === 'buckets') {
|
||
$memLabel.innerHTML = '<i data-lucide="inbox" class="w-3.5 h-3.5"></i> Arbeitsspeicher: Bucket-Verteilung';
|
||
resizeCanvas(memCanvas, memCtx, 0.3, 300, 160);
|
||
} else if (memState.type === 'bitonic_network') {
|
||
$memLabel.innerHTML = '<i data-lucide="network" class="w-3.5 h-3.5"></i> Sortiernetz: ' + memState.stages.length + ' Phasen \u00b7 ' + memState.n + ' Dr\u00e4hte';
|
||
resizeCanvas(memCanvas, memCtx, 0.35, 300, 140);
|
||
}
|
||
lucide.createIcons({ nodes: [$memLabel] });
|
||
}
|
||
drawMemory(memState);
|
||
}
|
||
|
||
// ================================================================
|
||
// Render Engine
|
||
// ================================================================
|
||
|
||
function renderStep(step, stepNum) {
|
||
var hi = {};
|
||
step.indices.forEach(function(i) { hi[i] = step.type; });
|
||
var allSorted = step.type === 'done';
|
||
// Build connected pair for connection lines
|
||
var 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 = ' ';
|
||
$stepExplanation.innerHTML = ' ';
|
||
}
|
||
|
||
// ================================================================
|
||
// Animation Engine
|
||
// ================================================================
|
||
|
||
function scheduleNext() {
|
||
if (animTimer !== null) clearInterval(animTimer);
|
||
let delay = getDelay();
|
||
|
||
// FIX: Thread-Slider hat nun einen funktionalen Effekt auf die Geschwindigkeit
|
||
if (PARALLEL_ALGOS.has($algoSelect.value)) {
|
||
const threads = parseInt($threadSlider.value, 10);
|
||
if (threads > 1) {
|
||
delay = Math.max(1, Math.round(delay / threads));
|
||
}
|
||
}
|
||
|
||
animTimer = window.setInterval(tick, delay);
|
||
}
|
||
|
||
function tick() {
|
||
if (stepIndex >= steps.length) { finishAnimation(); return; }
|
||
renderStep(steps[stepIndex], stepIndex + 1);
|
||
stepIndex++;
|
||
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);
|
||
}
|
||
updateProgress(steps.length, steps.length);
|
||
$phaseLabel.innerHTML = ' ';
|
||
$stepExplanation.textContent = 'Sortierung abgeschlossen \u2713';
|
||
updateButtonStates();
|
||
}
|
||
|
||
function stopTimer() {
|
||
if (animTimer !== null) { clearInterval(animTimer); animTimer = null; }
|
||
}
|
||
|
||
// ================================================================
|
||
// Reset
|
||
// ================================================================
|
||
|
||
function doReset() {
|
||
stopTimer();
|
||
isRunning = false;
|
||
isPaused = false;
|
||
steps = [];
|
||
stepIndex = 0;
|
||
generateArray();
|
||
arrayFresh = true;
|
||
setStats(0, 0, 0, 0);
|
||
renderCurrent();
|
||
updateButtonStates();
|
||
}
|
||
|
||
// ================================================================
|
||
// Button Event Handlers
|
||
// ================================================================
|
||
|
||
// Combined Play/Pause button
|
||
$btnPlayPause.addEventListener('click', function() {
|
||
if (isRunning && !isPaused) {
|
||
// Currently playing → pause
|
||
stopTimer();
|
||
isPaused = true;
|
||
updateButtonStates();
|
||
return;
|
||
}
|
||
if (!isRunning) {
|
||
// Not started → start; skip generateArray() if Reset already prepared a fresh array
|
||
if (!arrayFresh) { generateArray(); }
|
||
arrayFresh = false;
|
||
renderCurrent();
|
||
steps = buildSteps($algoSelect.value);
|
||
stepIndex = 0;
|
||
setStats(0, 0, 0, 0);
|
||
}
|
||
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;
|
||
setStats(0, 0, 0, 0);
|
||
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);
|
||
|
||
// ================================================================
|
||
// Slider & Input Events
|
||
// ================================================================
|
||
|
||
$speedSlider.addEventListener('input', function() {
|
||
updateSpeedLabel();
|
||
if (isRunning && !isPaused) scheduleNext();
|
||
});
|
||
|
||
$sizeSlider.addEventListener('input', function() {
|
||
updateSizeLabel();
|
||
if (!isRunning) doReset();
|
||
});
|
||
|
||
$algoSelect.addEventListener('change', function() {
|
||
updateThreadSlider();
|
||
if (!isRunning) doReset();
|
||
});
|
||
|
||
$threadSlider.addEventListener('input', function() {
|
||
$threadVal.textContent = $threadSlider.value;
|
||
if (isRunning && !isPaused) scheduleNext();
|
||
});
|
||
|
||
$presetSelect.addEventListener('change', function() {
|
||
if (!isRunning) doReset();
|
||
});
|
||
|
||
$customArray.addEventListener('change', function() {
|
||
if (!isRunning) doReset();
|
||
});
|
||
|
||
$btnClearCustom.addEventListener('click', function() {
|
||
$customArray.value = '';
|
||
$sizeSlider.disabled = false;
|
||
if (!isRunning) doReset();
|
||
});
|
||
|
||
$btnTheme.addEventListener('click', function() {
|
||
const isDark = document.documentElement.classList.contains('dark');
|
||
const newTheme = !isDark;
|
||
localStorage.setItem('sortvis-theme', newTheme ? 'dark' : 'light');
|
||
applyTheme(newTheme);
|
||
});
|
||
|
||
// ================================================================
|
||
// 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);
|
||
makeTouchSlider($threadSlider);
|
||
|
||
// ================================================================
|
||
// 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);
|
||
|
||
// ================================================================
|
||
// 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
|
||
})();
|
||
refreshColors();
|
||
updateSpeedLabel();
|
||
updateSizeLabel();
|
||
updateThreadSlider();
|
||
resizeCanvas();
|
||
generateArray();
|
||
arrayFresh = true;
|
||
renderCurrent();
|
||
updateButtonStates();
|
||
updateProgress(0, 0);
|
||
|
||
// FIX: Berücksichtigung von prefers-reduced-motion für die GSAP Eintritts-Animationen
|
||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
if (!prefersReducedMotion) {
|
||
gsap.from('header', { opacity: 0, y: -20, duration: 0.5, ease: 'power2.out' });
|
||
gsap.from('section, details', { opacity: 0, y: 16, duration: 0.45, stagger: 0.07, ease: 'power2.out', delay: 0.15 });
|
||
gsap.from('.stat-card', { opacity: 0, scale: 0.92, duration: 0.35, stagger: 0.06, ease: 'back.out(1.4)', delay: 0.35 });
|
||
}
|
||
|
||
</script>
|
||
|
||
<footer class="text-center text-xs text-muted py-6 border-t border-border mt-8">
|
||
© 2026 Dieter Schlüter <dieter(dot)schlueter(at)linux(dot)de>
|
||
</footer>
|
||
|
||
</body>
|
||
</html>
|