diff --git a/src/components/ConceptVisualizer.tsx b/src/components/ConceptVisualizer.tsx index 9b11adf..39a3c42 100644 --- a/src/components/ConceptVisualizer.tsx +++ b/src/components/ConceptVisualizer.tsx @@ -10,6 +10,7 @@ import type { SlidingWindowState, MemoTableState, CoinChangeState, + BucketsState, } from '@lib/types' interface ConceptVisualizerProps { @@ -41,6 +42,8 @@ export default function ConceptVisualizer({ step }: ConceptVisualizerProps) { return case 'coinChange': return + case 'buckets': + return default: return null } @@ -1164,3 +1167,249 @@ function CoinChangeViz({ state }: { state: CoinChangeState }) { ) } + +// ════════════════════════════════════════════════════════════════ +// BUCKETS — Visualization for Bucket Sort +// ════════════════════════════════════════════════════════════════ + +function BucketsViz({ state }: { state: BucketsState }) { + const { + array, + buckets, + range, + min, + max, + bucketSize, + currentElementIndex, + activeBucketIndex, + innerHighlights, + phase, + operation, + } = state + + return ( +
+
+ Bucket Sort Visualization +
+ + {operation && ( +
+ {operation} +
+ )} + + {/* Min/Max Status & Calculation during Initializing */} + {phase === 'initializing' && (min !== undefined || max !== undefined) && ( +
+
+
+ Current Min + {min ?? '—'} +
+
+ Current Max + {max ?? '—'} +
+
+ + {/* Bucket Calculation Formula */} + {buckets.length > 0 && ( +
+
Bucket Count Calculation
+
+
+
floor((max - min) / size) + 1
+
+ +
+
+ {max} - {min} +
+
{bucketSize}
+
+ + + 1 + = + {buckets.length} +
+
+
+
+ Each bucket covers a range of {bucketSize} values +
+
+ )} +
+ )} + + {/* Main Array Section */} +
+
+ + {phase === 'collecting' ? 'Result Array' : 'Input Array'} + + {phase === 'distributing' && currentElementIndex !== undefined && ( + + Processing: {array[currentElementIndex]} + + )} + {phase === 'initializing' && ( + + {buckets.length > 0 ? 'Creating Buckets...' : 'Scanning Range...'} + + )} +
+
+ {array.map((val, i) => { + const isProcessing = i === currentElementIndex + const isCollected = phase === 'collecting' && i < currentElementIndex! + return ( +
+
+ {val} +
+ {isProcessing && ( +
+ + + +
+ )} +
+ ) + })} +
+
+ + {/* Buckets Section */} +
+ {buckets.map((bucket, bIdx) => { + const isActive = bIdx === activeBucketIndex + const rangeStart = range ? range.min + bIdx * (bucketSize ?? 1) : 0 + const rangeEnd = range ? rangeStart + (bucketSize ?? 1) - 1 : 0 + + return ( +
+ {/* Range label */} +
+ {rangeStart}–{rangeEnd} +
+ + {/* Bucket container */} +
+
+ {bucket.map((val, vIdx) => { + const highlight = isActive ? innerHighlights?.[vIdx] : undefined + const getHighlightStyles = () => { + switch (highlight) { + case 'comparing': return { bg: 'rgba(59,130,246,0.3)', border: '#3b82f6', text: '#fff' } + case 'active': return { bg: 'rgba(234,179,8,0.3)', border: '#eab308', text: '#fff' } + case 'current': return { bg: 'rgba(168,85,247,0.3)', border: '#a855f7', text: '#fff' } + case 'found': return { bg: 'rgba(74,222,128,0.2)', border: '#4ade80', text: '#4ade80' } + default: return { bg: 'rgba(38,38,38,1)', border: 'rgba(255,255,255,0.1)', text: '#60a5fa' } + } + } + const styles = getHighlightStyles() + + return ( +
+ {val} +
+ ) + })} + + {/* Empty state dots */} + {bucket.length === 0 && ( +
+
+
+
+
+ )} +
+ + {/* Bucket Bottom */} +
+ + {/* Active indicator */} + {isActive && ( +
+ + Active + +
+ )} +
+ + {/* Bucket Index */} +
+ Bucket {bIdx} +
+
+ ) + })} +
+ + +
+ ) +} diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index ac152b9..5700556 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -724,6 +724,28 @@ Properties: - Adaptive Shell Sort is faster than Insertion Sort for larger arrays because it moves elements closer to their final position earlier. Performance depends heavily on the gap sequence chosen.`, + 'bucket-sort': `Bucket Sort + +Bucket Sort is a distribution-based sorting algorithm that works by partitioning an array into a number of buckets. Each bucket is then sorted individually using another sorting algorithm or recursively applying the bucket sort. + +How it works: +1. Find the range (min/max) to determine bucket indices +2. Create empty buckets based on a fixed size (e.g., 10 or 20) +3. Distribute elements into buckets: index = floor((value - min) / size) +4. Sort each non-empty bucket using Insertion Sort +5. Collect elements from sorted buckets back into the main array + +Time Complexity: + Best: O(n + k) — uniform distribution + Average: O(n + k) + Worst: O(n²) — all elements fall into one bucket + +Space Complexity: O(n + k) — extra space for buckets + +Properties: + - Stable sort (if underlying sort is stable) + - Not in-place + - Data-distribution dependent`, 'jump-search': `Jump Search @@ -1634,6 +1656,28 @@ Propiedades: - Adaptativo Shell Sort es más rápido que Insertion Sort para arreglos grandes porque mueve elementos más cerca de su posición final antes. El rendimiento depende mucho de la secuencia de brechas elegida.`, + 'bucket-sort': `Bucket Sort (Ordenamiento por Cubetas) + +Bucket Sort es un algoritmo de ordenamiento basado en la distribución que funciona particionando un arreglo en varias cubetas. Cada cubeta se ordena individualmente usando otro algoritmo de ordenamiento o aplicando recursivamente el mismo algoritmo. + +Cómo funciona: +1. Encontrar el rango (mín/máx) para determinar los índices de las cubetas +2. Crear cubetas vacías basadas en un tamaño fijo (por ejemplo, 10 o 20) +3. Distribuir elementos en las cubetas: índice = suelo((valor - mín) / tamaño) +4. Ordenar cada cubeta no vacía usando Insertion Sort +5. Recolectar elementos de las cubetas ordenadas de vuelta al arreglo principal + +Complejidad Temporal: + Mejor: O(n + k) — distribución uniforme + Promedio: O(n + k) + Peor: O(n²) — todos los elementos caen en una sola cubeta + +Complejidad Espacial: O(n + k) — espacio extra para cubetas + +Propiedades: + - Ordenamiento estable (si el ordenamiento subyacente lo es) + - No es in-place + - Dependiente de la distribución de los datos`, 'jump-search': `Jump Search (Búsqueda por Saltos) diff --git a/src/lib/algorithms/index.ts b/src/lib/algorithms/index.ts index e5e9b15..8f15f85 100644 --- a/src/lib/algorithms/index.ts +++ b/src/lib/algorithms/index.ts @@ -29,6 +29,7 @@ import { countingSort, radixSort, shellSort, + bucketSort, } from '@lib/algorithms/sorting' import { @@ -86,6 +87,7 @@ export const algorithms: Algorithm[] = [ countingSort, radixSort, shellSort, + bucketSort, // Searching binarySearch, linearSearch, diff --git a/src/lib/algorithms/sorting.ts b/src/lib/algorithms/sorting.ts index 4d1ffcd..22a7e04 100644 --- a/src/lib/algorithms/sorting.ts +++ b/src/lib/algorithms/sorting.ts @@ -862,7 +862,7 @@ Useful when worst-case performance matters and stability is not required.`, variables: { i, max: arr[0] }, }) - ;[arr[0], arr[i]] = [arr[i], arr[0]] + ;[arr[0], arr[i]] = [arr[i], arr[0]] sortedIndices.push(i) steps.push({ @@ -1339,6 +1339,366 @@ A good general-purpose algorithm; much faster than Insertion Sort for medium-siz }, } +// ============================================================ +// BUCKET SORT +// ============================================================ +const bucketSort: Algorithm = { + id: 'bucket-sort', + name: 'Bucket Sort', + category: 'Sorting', + difficulty: 'intermediate', + visualization: 'concept', + code: `function bucketSort(array, bucketSize = 5) { + if (array.length === 0) return array; + + // 1. Find min and max values + let min = array[0]; + let max = array[0]; + for (let i = 1; i < array.length; i++) { + if (array[i] < min) min = array[i]; + else if (array[i] > max) max = array[i]; + } + + // 2. Initialize buckets + const bucketCount = Math.floor((max - min) / bucketSize) + 1; + const buckets = new Array(bucketCount); + for (let i = 0; i < buckets.length; i++) { + buckets[i] = []; + } + + // 3. Distribute elements into buckets + for (let i = 0; i < array.length; i++) { + const bucketIndex = Math.floor((array[i] - min) / bucketSize); + buckets[bucketIndex].push(array[i]); + } + + // 4. Sort buckets and concatenate + array.length = 0; + for (let i = 0; i < buckets.length; i++) { + insertionSort(buckets[i]); + for (let j = 0; j < buckets[i].length; j++) { + array.push(buckets[i][j]); + } + } + + return array; +}`, + description: `Bucket Sort + +Bucket Sort is a distribution-based sorting algorithm that works by partitioning an array into a number of buckets. Each bucket is then sorted individually, either using a different sorting algorithm or by recursively applying bucket sort. + +How it works: +1. Find the maximum and minimum values in the array +2. Determine the number of buckets and initialize them +3. Distribute each element into its corresponding bucket based on its value +4. Sort each non-empty bucket (typically using Insertion Sort) +5. Concatenate all sorted buckets back into the original array + +Time Complexity: + Best: O(n + k) — when elements are uniformly distributed + Average: O(n + k) + Worst: O(n²) — when elements are concentrated in one bucket + +Space Complexity: O(n + k) — where k is the number of buckets + +Properties: + - Stable sort (if the underlying sort is stable) + - Not in-place + - Efficiency depends on the distribution of input data + +Ideal for sorting floating-point numbers or data uniformly distributed over a range.`, + + generateSteps(locale = 'en') { + const arr = [22, 45, 12, 8, 10, 6, 72, 81, 33, 18, 50, 14] + const steps: Step[] = [] + const n = arr.length + const bucketSize = 20 + + // Phase 1: Range finding (Min/Max loop) + let min = arr[0] + let max = arr[0] + + // Initial min/max step + steps.push({ + array: [...arr], + highlights: { 0: 'comparing' }, + concept: { + type: 'buckets', + array: [...arr], + buckets: [], + phase: 'initializing', + currentElementIndex: 0, + min, + max, + operation: d(locale, 'Step 1: Finding range (min & max)', 'Paso 1: Buscando el rango (mín y máx)'), + }, + description: d( + locale, + `Starting with the first element: setting initial min and max to ${arr[0]}.`, + `Empezando con el primer elemento: estableciendo mín y máx inicial en ${arr[0]}.` + ), + codeLine: 5, + variables: { min, max }, + }) + + for (let i = 1; i < n; i++) { + const isNewMin = arr[i] < min + const isNewMax = arr[i] > max + + if (isNewMin) min = arr[i] + if (isNewMax) max = arr[i] + + steps.push({ + array: [...arr], + highlights: { [i]: 'comparing' }, + concept: { + type: 'buckets', + array: [...arr], + buckets: [], + phase: 'initializing', + currentElementIndex: i, + min, + max, + operation: d(locale, `Checking element ${i}: ${arr[i]}`, `Verificando elemento ${i}: ${arr[i]}`), + }, + description: d( + locale, + `Checking ${arr[i]}. ${isNewMin ? 'New min found!' : isNewMax ? 'New max found!' : 'Range remains same.'} Current min: ${min}, max: ${max}.`, + `Verificando ${arr[i]}. ${isNewMin ? '¡Nuevo mín!' : isNewMax ? '¡Nuevo máx!' : 'El rango se mantiene.'} Mín actual: ${min}, máx: ${max}.` + ), + codeLine: 5, + variables: { i, min, max, 'array[i]': arr[i] }, + }) + } + + // Phase 2: Create buckets + const bucketCount = Math.floor((max - min) / bucketSize) + 1 + const finalBuckets: number[][] = Array.from({ length: bucketCount }, () => []) + + // Show empty buckets appearing + steps.push({ + array: [...arr], + concept: { + type: 'buckets', + array: [...arr], + buckets: Array.from({ length: bucketCount }, () => []), + phase: 'initializing', + min, + max, + bucketSize, + operation: d(locale, `Step 2: Creating ${bucketCount} buckets`, `Paso 2: Creando ${bucketCount} cubetas`), + }, + description: d( + locale, + `With range [${min}..${max}] and size ${bucketSize}, we create ${bucketCount} empty buckets.`, + `Con el rango [${min}..${max}] y tamaño ${bucketSize}, creamos ${bucketCount} cubetas vacías.` + ), + codeLine: 13, + variables: { min, max, bucketCount, bucketSize }, + }) + + // Phase 3: Distribution + const buckets: number[][] = Array.from({ length: bucketCount }, () => []) + for (let i = 0; i < n; i++) { + const bucketIndex = Math.floor((arr[i] - min) / bucketSize) + buckets[bucketIndex].push(arr[i]) + + steps.push({ + array: [...arr], + highlights: { [i]: 'active' }, + concept: { + type: 'buckets', + array: [...arr], + buckets: buckets.map(b => [...b]), + range: { min, max }, + min, + max, + bucketSize, + currentElementIndex: i, + activeBucketIndex: bucketIndex, + phase: 'distributing', + operation: d(locale, `Distributing ${arr[i]} → Bucket ${bucketIndex}`, `Distribuyendo ${arr[i]} → Cubeta ${bucketIndex}`), + }, + description: d( + locale, + `Element ${arr[i]} belongs to bucket ${bucketIndex} (range ${min + bucketIndex * bucketSize}-${min + (bucketIndex + 1) * bucketSize - 1}).`, + `El elemento ${arr[i]} pertenece a la cubeta ${bucketIndex} (rango ${min + bucketIndex * bucketSize}-${min + (bucketIndex + 1) * bucketSize - 1}).` + ), + codeLine: 20, + variables: { i, 'array[i]': arr[i], bucketIndex }, + }) + } + + // Phase 4: Selection and Sort + const collected: number[] = [] + + for (let i = 0; i < bucketCount; i++) { + if (buckets[i].length > 0) { + // Step-by-step Insertion Sort inside the bucket + const bucketArr = [...buckets[i]] + for (let k = 1; k < bucketArr.length; k++) { + let key = bucketArr[k] + let l = k - 1 + + steps.push({ + array: [...arr], + concept: { + type: 'buckets', + array: [...arr], + buckets: buckets.map((b, idx) => idx === i ? [...bucketArr] : [...b]), + range: { min, max }, + min, + max, + bucketSize, + activeBucketIndex: i, + innerHighlights: { [k]: 'current', [l]: 'comparing' }, + phase: 'sorting', + operation: d(locale, `Bucket ${i}: Comparing ${bucketArr[k]} and ${bucketArr[l]}`, `Cubeta ${i}: Comparando ${bucketArr[k]} y ${bucketArr[l]}`), + }, + description: d( + locale, + `Bucket ${i}: Checking if ${bucketArr[k]} should move before ${bucketArr[l]}.`, + `Cubeta ${i}: Verificando si ${bucketArr[k]} debe ir antes de ${bucketArr[l]}.` + ), + codeLine: 27, + variables: { bucketIndex: i, comparing: `${bucketArr[k]} < ${bucketArr[l]}` }, + }) + + while (l >= 0 && bucketArr[l] > key) { + bucketArr[l + 1] = bucketArr[l] + l = l - 1 + + steps.push({ + array: [...arr], + concept: { + type: 'buckets', + array: [...arr], + buckets: buckets.map((b, idx) => idx === i ? [...bucketArr] : [...b]), + range: { min, max }, + min, + max, + bucketSize, + activeBucketIndex: i, + innerHighlights: { [l + 1]: 'active', [l + 2]: 'active' }, + phase: 'sorting', + operation: d(locale, `Bucket ${i}: Shifting elements`, `Cubeta ${i}: Desplazando elementos`), + }, + description: d( + locale, + `Bucket ${i}: Shifting ${bucketArr[l + 1]} to the right.`, + `Cubeta ${i}: Desplazando ${bucketArr[l + 1]} a la derecha.` + ), + codeLine: 27, + variables: { bucketIndex: i, key }, + }) + } + bucketArr[l + 1] = key + + steps.push({ + array: [...arr], + concept: { + type: 'buckets', + array: [...arr], + buckets: buckets.map((b, idx) => idx === i ? [...bucketArr] : [...b]), + range: { min, max }, + min, + max, + bucketSize, + activeBucketIndex: i, + innerHighlights: { [l + 1]: 'found' }, + phase: 'sorting', + operation: d(locale, `Bucket ${i}: Placed ${key}`, `Cubeta ${i}: Ubicado ${key}`), + }, + description: d( + locale, + `Bucket ${i}: Placed ${key} in its sorted position.`, + `Cubeta ${i}: Ubicado ${key} en su posición ordenada.` + ), + codeLine: 27, + variables: { bucketIndex: i, position: l + 1 }, + }) + } + + // Final update for this bucket + buckets[i] = [...bucketArr] + + steps.push({ + array: [...arr], + concept: { + type: 'buckets', + array: [...arr], + buckets: buckets.map(b => [...b]), + range: { min, max }, + min, + max, + bucketSize, + activeBucketIndex: i, + phase: 'sorting', + operation: d(locale, `Bucket ${i} sorted`, `Cubeta ${i} ordenada`), + }, + description: d( + locale, + `Bucket ${i} is now sorted: [${buckets[i].join(', ')}]`, + `La cubeta ${i} ahora está ordenada: [${buckets[i].join(', ')}]` + ), + codeLine: 27, + variables: { bucketIndex: i }, + }) + + // Collect + for (let j = 0; j < buckets[i].length; j++) { + collected.push(buckets[i][j]) + steps.push({ + array: [...collected, ...arr.slice(collected.length)], + highlights: { [collected.length - 1]: 'found' }, + concept: { + type: 'buckets', + array: [...collected, ...arr.slice(collected.length)], + buckets: buckets.map(b => [...b]), + range: { min, max }, + min, + max, + bucketSize, + activeBucketIndex: i, + currentElementIndex: collected.length - 1, + innerHighlights: { [j]: 'found' }, + phase: 'collecting', + operation: d(locale, `Step 4: Collecting ${buckets[i][j]}`, `Paso 4: Recolectando ${buckets[i][j]}`), + }, + description: d( + locale, + `Collecting ${buckets[i][j]} from bucket ${i} into the final array.`, + `Recolectando ${buckets[i][j]} de la cubeta ${i} al arreglo final.` + ), + codeLine: 29, + variables: { bucketIndex: i, element: buckets[i][j] }, + }) + } + } + } + + steps.push({ + array: [...collected], + highlights: {}, + sorted: Array.from({ length: n }, (_, i) => i), + concept: { + type: 'buckets', + array: [...collected], + buckets: buckets.map(b => [...b]), + min, + max, + phase: 'collecting', + operation: d(locale, 'Bucket Sort complete', 'Bucket Sort completado'), + }, + description: d(locale, 'All buckets are collected. The array is now fully sorted!', 'Todas las cubetas han sido recolectadas. ¡El arreglo ahora está completamente ordenado!'), + codeLine: 1, + variables: { totalElements: collected.length }, + }) + + return steps + }, +} + export { bubbleSort, selectionSort, @@ -1349,4 +1709,5 @@ export { countingSort, radixSort, shellSort, + bucketSort, } diff --git a/src/lib/types.ts b/src/lib/types.ts index b647820..f9054d1 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -179,6 +179,21 @@ export interface CoinChangeState { operation?: string } +export interface BucketsState { + type: 'buckets' + array: number[] + buckets: number[][] + range?: { min: number; max: number } + min?: number + max?: number + bucketSize?: number + currentElementIndex?: number + activeBucketIndex?: number + innerHighlights?: Record + phase: 'initializing' | 'distributing' | 'sorting' | 'collecting' + operation?: string +} + export type ConceptState = | BigOState | CallStackState @@ -190,6 +205,7 @@ export type ConceptState = | SlidingWindowState | MemoTableState | CoinChangeState + | BucketsState export interface Step { array?: number[]