From 450ab0b8370468fa9ac2517d421d706510a9b021 Mon Sep 17 00:00:00 2001 From: Lobo Date: Wed, 4 Feb 2026 11:37:11 +0100 Subject: [PATCH] Implement snapshot-based sorting visualizer Refactor sorting to produce and consume SortSnapshot sequences for visualization. SortingService now creates immutable snapshots and implements bubble, quick and heap sorts (with helper methods) instead of performing UI delays; a swap/heapify/partition flow records state changes. SortingComponent was updated to animate snapshots (with start/stop timeout handling), added array size input and controls, stores an unsorted copy for resets, and uses ChangeDetectorRef for updates. Minor UI tweaks: faster bar transitions, info color, updated default array size and animation speed, and added i18n keys for ARRAY_SIZE (en/de). --- .../sorting/service/sorting.service.ts | 221 ++++++++++++------ .../algorithms/sorting/sorting.component.html | 22 +- .../algorithms/sorting/sorting.component.scss | 4 +- .../algorithms/sorting/sorting.component.ts | 116 ++++++--- .../algorithms/sorting/sorting.models.ts | 4 + src/assets/i18n/de.json | 3 +- src/assets/i18n/en.json | 3 +- 7 files changed, 261 insertions(+), 112 deletions(-) diff --git a/src/app/pages/algorithms/sorting/service/sorting.service.ts b/src/app/pages/algorithms/sorting/service/sorting.service.ts index bbad7b1..d0972d2 100644 --- a/src/app/pages/algorithms/sorting/service/sorting.service.ts +++ b/src/app/pages/algorithms/sorting/service/sorting.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core'; -import { SortData } from '../sorting.models'; +import {SortData, SortSnapshot} from '../sorting.models'; @Injectable({ providedIn: 'root' @@ -8,90 +8,171 @@ export class SortingService { constructor() { } - async bubbleSort(array: SortData[], animationSpeed: number): Promise { - console.log(animationSpeed); - const n = array.length; + private createSnapshot(array: SortData[]): SortSnapshot { + return { + array: array.map(item => ({ ...item })) + }; + } + + bubbleSort(array: SortData[]): SortSnapshot[] { + const snapshots: SortSnapshot[] = []; + const arr = array.map(item => ({ ...item })); + const n = arr.length; + + snapshots.push(this.createSnapshot(arr)); + for (let i = 0; i < n - 1; i++) { - console.log("1"); for (let j = 0; j < n - i - 1; j++) { - console.log("2"); - array[j].state = 'comparing'; - array[j + 1].state = 'comparing'; - await this.delay(animationSpeed); + arr[j].state = 'comparing'; + arr[j + 1].state = 'comparing'; + snapshots.push(this.createSnapshot(arr)); // Snapshot Vergleich - if (array[j].value > array[j + 1].value) { - // Swap elements - const temp = array[j].value; - array[j].value = array[j + 1].value; - array[j + 1].value = temp; + if (arr[j].value > arr[j + 1].value) { + const temp = arr[j].value; + arr[j].value = arr[j + 1].value; + arr[j + 1].value = temp; + + snapshots.push(this.createSnapshot(arr)); } - array[j].state = 'unsorted'; - array[j + 1].state = 'unsorted'; + arr[j].state = 'unsorted'; + arr[j + 1].state = 'unsorted'; } - array[n - 1 - i].state = 'sorted'; // Mark the largest element as sorted + arr[n - 1 - i].state = 'sorted'; + snapshots.push(this.createSnapshot(arr)); } - array[0].state = 'sorted'; // Mark the last element as sorted (if n > 0) + + arr[0].state = 'sorted'; + snapshots.push(this.createSnapshot(arr)); + + return snapshots; } - async selectionSort(array: SortData[], animationSpeed: number): Promise { - const n = array.length; - for (let i = 0; i < n - 1; i++) { - let minIdx = i; - array[i].state = 'comparing'; - for (let j = i + 1; j < n; j++) { - array[j].state = 'comparing'; - await this.delay(animationSpeed); + // --- QUICK SORT --- + quickSort(array: SortData[]): SortSnapshot[] { + const snapshots: SortSnapshot[] = []; + const arr = array.map(item => ({ ...item })); + snapshots.push(this.createSnapshot(arr)); - if (array[j].value < array[minIdx].value) { - minIdx = j; - } - if (j !== minIdx) { // Only reset if it wasn't the minimum - array[j].state = 'unsorted'; - } - } - if (minIdx !== i) { - // Swap elements - const temp = array[i].value; - array[i].value = array[minIdx].value; - array[minIdx].value = temp; - } - array[i].state = 'sorted'; // Mark the current element as sorted - if (minIdx !== i) { - array[minIdx].state = 'unsorted'; - } - } - array[n - 1].state = 'sorted'; // Mark the last element as sorted + this.quickSortHelper(arr, 0, arr.length - 1, snapshots); + + arr.forEach(i => i.state = 'sorted'); + snapshots.push(this.createSnapshot(arr)); + + return snapshots; } - async insertionSort(array: SortData[], animationSpeed: number): Promise { - const n = array.length; - for (let i = 1; i < n; i++) { - const key = array[i].value; - array[i].state = 'comparing'; - let j = i - 1; + private quickSortHelper(arr: SortData[], low: number, high: number, snapshots: SortSnapshot[]) { + if (low < high) { + const pi = this.partition(arr, low, high, snapshots); - while (j >= 0 && array[j].value > key) { - array[j].state = 'comparing'; - await this.delay(animationSpeed); - - array[j + 1].value = array[j].value; - array[j + 1].state = 'unsorted'; // Reset after shifting - j = j - 1; - } - await this.delay(animationSpeed); // Delay after loop for final position - array[j + 1].value = key; - array[i].state = 'unsorted'; // Reset original 'key' position - array.forEach((item, idx) => { // Mark sorted up to i - if (idx <= i) { - item.state = 'sorted'; - } - }); + this.quickSortHelper(arr, low, pi - 1, snapshots); + this.quickSortHelper(arr, pi + 1, high, snapshots); + } else if (low >= 0 && high >= 0 && low === high) { + arr[low].state = 'sorted'; + snapshots.push(this.createSnapshot(arr)); } - array.forEach(item => item.state = 'sorted'); // Mark all as sorted at the end } - private delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + private partition(arr: SortData[], low: number, high: number, snapshots: SortSnapshot[]): number { + const pivot = arr[high]; + arr[high].state = 'comparing'; // Pivot visualisieren + snapshots.push(this.createSnapshot(arr)); + + let i = (low - 1); + + for (let j = low; j <= high - 1; j++) { + arr[j].state = 'comparing'; + snapshots.push(this.createSnapshot(arr)); + + if (arr[j].value < pivot.value) { + i++; + this.swap(arr, i, j); + snapshots.push(this.createSnapshot(arr)); + } + arr[j].state = 'unsorted'; + } + this.swap(arr, i + 1, high); + + arr[high].state = 'unsorted'; + arr[i + 1].state = 'sorted'; + snapshots.push(this.createSnapshot(arr)); + + return i + 1; + } + + // --- HEAP SORT --- + heapSort(array: SortData[]): SortSnapshot[] { + const snapshots: SortSnapshot[] = []; + const arr = array.map(item => ({ ...item })); + const n = arr.length; + + snapshots.push(this.createSnapshot(arr)); + + for (let i = Math.floor(n / 2) - 1; i >= 0; i--) { + this.heapify(arr, n, i, snapshots); + } + + for (let i = n - 1; i > 0; i--) { + arr[0].state = 'comparing'; + arr[i].state = 'comparing'; + snapshots.push(this.createSnapshot(arr)); + + this.swap(arr, 0, i); + + arr[0].state = 'unsorted'; + arr[i].state = 'sorted'; + snapshots.push(this.createSnapshot(arr)); + + this.heapify(arr, i, 0, snapshots); + } + arr[0].state = 'sorted'; + snapshots.push(this.createSnapshot(arr)); + + return snapshots; + } + + private heapify(arr: SortData[], n: number, i: number, snapshots: SortSnapshot[]) { + let largest = i; + const l = 2 * i + 1; + const r = 2 * i + 2; + + if (l < n) { + arr[l].state = 'comparing'; + arr[largest].state = 'comparing'; + snapshots.push(this.createSnapshot(arr)); + + if (arr[l].value > arr[largest].value) { + largest = l; + } + arr[l].state = 'unsorted'; + arr[largest].state = 'unsorted'; + } + + // Vergleich Rechts + if (r < n) { + arr[r].state = 'comparing'; + arr[largest].state = 'comparing'; + snapshots.push(this.createSnapshot(arr)); + + if (arr[r].value > arr[largest].value) { + largest = r; + } + arr[r].state = 'unsorted'; + arr[largest].state = 'unsorted'; + } + + if (largest !== i) { + this.swap(arr, i, largest); + snapshots.push(this.createSnapshot(arr)); + + this.heapify(arr, n, largest, snapshots); + } + } + + private swap(arr: SortData[], i: number, j: number) { + const temp = arr[i].value; + arr[i].value = arr[j].value; + arr[j].value = temp; } } diff --git a/src/app/pages/algorithms/sorting/sorting.component.html b/src/app/pages/algorithms/sorting/sorting.component.html index 8133128..e9dc6d0 100644 --- a/src/app/pages/algorithms/sorting/sorting.component.html +++ b/src/app/pages/algorithms/sorting/sorting.component.html @@ -7,20 +7,34 @@
{{ 'ALGORITHM.SORTING.ALGORITHM' | translate }} - + @for (algo of availableAlgorithms; track algo.value) { {{ algo.name }} } -
+
+ - -
diff --git a/src/app/pages/algorithms/sorting/sorting.component.scss b/src/app/pages/algorithms/sorting/sorting.component.scss index 128fcaf..b911eac 100644 --- a/src/app/pages/algorithms/sorting/sorting.component.scss +++ b/src/app/pages/algorithms/sorting/sorting.component.scss @@ -35,7 +35,7 @@ .bar { flex-grow: 1; background-color: #424242; /* Default unsorted color */ - transition: height 0.1s ease-in-out, background-color 0.1s ease-in-out; + transition: height 0.05s ease-in-out, background-color 0.05s ease-in-out; width: 10px; /* Default width, flex-grow will adjust */ min-width: 1px; /* Ensure bars are always visible */ @@ -56,7 +56,7 @@ .info-panel { margin-top: 10px; font-size: 0.9em; - color: #555; + color: #FFFFFF; } } } diff --git a/src/app/pages/algorithms/sorting/sorting.component.ts b/src/app/pages/algorithms/sorting/sorting.component.ts index 43bd720..a247b31 100644 --- a/src/app/pages/algorithms/sorting/sorting.component.ts +++ b/src/app/pages/algorithms/sorting/sorting.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import {ChangeDetectorRef, Component, OnInit} from '@angular/core'; import { CommonModule } from '@angular/common'; import {MatCardModule} from "@angular/material/card"; import {MatFormFieldModule} from "@angular/material/form-field"; @@ -7,86 +7,134 @@ import {MatButtonModule} from "@angular/material/button"; import {MatIconModule} from "@angular/material/icon"; import {TranslateModule} from "@ngx-translate/core"; import { SortingService } from './service/sorting.service'; -import { SortData } from './sorting.models'; +import {SortData, SortSnapshot} from './sorting.models'; import { FormsModule } from '@angular/forms'; - +import {MatInput} from '@angular/material/input'; @Component({ selector: 'app-sorting', standalone: true, - imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslateModule, FormsModule], + imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslateModule, FormsModule, MatInput], templateUrl: './sorting.component.html', styleUrls: ['./sorting.component.scss'] }) export class SortingComponent implements OnInit { + readonly MAX_ARRAY_SIZE: number = 200; + readonly MIN_ARRAY_SIZE: number = 20; + + private timeoutIds: any[] = []; sortArray: SortData[] = []; - arraySize: number = 50; + unsortedArrayCopy: SortData[] = []; + arraySize: number = 100; maxArrayValue: number = 100; - animationSpeed: number = 10; // Milliseconds per step + animationSpeed: number = 50; // Milliseconds per step // Placeholder for available sorting algorithms availableAlgorithms: { name: string; value: string }[] = [ { name: 'Bubble Sort', value: 'bubbleSort' }, - { name: 'Selection Sort', value: 'selectionSort' }, - { name: 'Insertion Sort', value: 'insertionSort' }, + { name: 'Quick Sort', value: 'quickSort' }, + { name: 'Heap Sort', value: 'heapSort' }, ]; selectedAlgorithm: string = this.availableAlgorithms[0].value; - isSorting: boolean = false; executionTime: number = 0; - constructor(private sortingService: SortingService) { } + constructor(private readonly sortingService: SortingService, private readonly cdr: ChangeDetectorRef) { } ngOnInit(): void { this.generateNewArray(); } + newArraySizeSet() + { + if (this.arraySize == this.sortArray.length) + { + return; + } + this.generateNewArray(); + } + generateNewArray(): void { - this.isSorting = false; + this.resetSorting(); this.executionTime = 0; + this.unsortedArrayCopy = []; this.sortArray = []; + for (let i = 0; i < this.arraySize; i++) { + const randomValue = Math.floor(Math.random() * this.maxArrayValue) + 1; this.sortArray.push({ - value: Math.floor(Math.random() * this.maxArrayValue) + 1, + value: randomValue, + state: 'unsorted' + }); + + this.unsortedArrayCopy.push({ + value: randomValue, state: 'unsorted' }); } } - async startSorting(): Promise { - if (this.isSorting) { - return; + private resetSortState() { + for (let i = 0; i < this.sortArray.length; i++) { + let element = this.sortArray[i]; + let unsortedElement = this.unsortedArrayCopy[i]; + element.value = unsortedElement.value; + element.state = 'unsorted'; } - console.log('Starting sorting...'); - this.isSorting = true; - this.executionTime = 0; + } - // Reset states for visualization - this.sortArray.forEach(item => item.state = 'unsorted'); + async startSorting(): Promise { + this.resetSorting(); const startTime = performance.now(); - console.log("Select algorithm ", this.selectedAlgorithm); + let snapshots: SortSnapshot[] = []; switch (this.selectedAlgorithm) { case 'bubbleSort': - await this.sortingService.bubbleSort(this.sortArray, this.animationSpeed); + snapshots = this.sortingService.bubbleSort(this.sortArray); break; - case 'selectionSort': - await this.sortingService.selectionSort(this.sortArray, this.animationSpeed); + case 'quickSort': + snapshots = this.sortingService.quickSort(this.sortArray); break; - case 'insertionSort': - await this.sortingService.insertionSort(this.sortArray, this.animationSpeed); - break; - default: - console.warn('Unknown sorting algorithm selected:', this.selectedAlgorithm); + case 'heapSort': + snapshots = this.sortingService.heapSort(this.sortArray); break; } - console.log("Done with sorting..."); + const endTime = performance.now(); - this.executionTime = Math.round(endTime - startTime); - this.isSorting = false; - this.sortArray.forEach(item => item.state = 'sorted'); // Mark all as sorted after completion + this.executionTime = parseFloat((endTime - startTime).toFixed(4)); + + console.log(snapshots.length); + this.animateSorting(snapshots); + } + + private animateSorting(snapshots: SortSnapshot[]): void { + snapshots.forEach((snapshot, index) => { + const id = setTimeout(() => { + for (let i = 0; i < this.sortArray.length; i++) { + if (snapshot.array[i]) { + this.sortArray[i].value = snapshot.array[i].value; + this.sortArray[i].state = snapshot.array[i].state; + } + } + + this.cdr.detectChanges(); + + if (index === snapshots.length - 1) { + this.sortArray.forEach(item => item.state = 'sorted'); + this.cdr.detectChanges(); + } + }, index * this.animationSpeed); + + this.timeoutIds.push(id); + }); + } + + private stopAnimations(): void { + this.timeoutIds.forEach(id => clearTimeout(id)); + this.timeoutIds = []; } resetSorting(): void { - this.generateNewArray(); + this.stopAnimations(); + this.resetSortState(); } } diff --git a/src/app/pages/algorithms/sorting/sorting.models.ts b/src/app/pages/algorithms/sorting/sorting.models.ts index 2741353..e4d8bce 100644 --- a/src/app/pages/algorithms/sorting/sorting.models.ts +++ b/src/app/pages/algorithms/sorting/sorting.models.ts @@ -2,3 +2,7 @@ export interface SortData { value: number; state: 'sorted' | 'comparing' | 'unsorted'; } + +export interface SortSnapshot { + array: SortData[]; +} diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 1d0e778..428a3e2 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -334,7 +334,8 @@ "START": "Sortierung starten", "RESET": "Zurücksetzen", "GENERATE_NEW_ARRAY": "Neues Array generieren", - "EXECUTION_TIME": "Ausführungszeit" + "EXECUTION_TIME": "Ausführungszeit", + "ARRAY_SIZE": "Anzahl der Balken" } } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 15c429a..daaa808 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -334,7 +334,8 @@ "START": "Start Sorting", "RESET": "Reset", "GENERATE_NEW_ARRAY": "Generate New Array", - "EXECUTION_TIME": "Execution Time" + "EXECUTION_TIME": "Execution Time", + "ARRAY_SIZE": "Number of Bars" } } }