Skip to content

Commit 544b274

Browse files
committed
✨ feat: Add visualization for Longest Substring Without Repeating Characters algorithm
1 parent b588efc commit 544b274

File tree

3 files changed

+651
-0
lines changed

3 files changed

+651
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// Configuración
2+
const inputString = 'abcabcbb';
3+
const cellSize = 60; // Debe coincidir con CSS --cell-size
4+
const container = document.getElementById('string-container');
5+
const windowEl = document.getElementById('window');
6+
const pointerL = document.getElementById('pointer-l');
7+
const pointerR = document.getElementById('pointer-r');
8+
const mapBody = document.getElementById('map-body');
9+
const logBox = document.getElementById('log-box');
10+
11+
// Elementos de Stats
12+
const elLeft = document.getElementById('val-left');
13+
const elRight = document.getElementById('val-right');
14+
const elMax = document.getElementById('val-max');
15+
const btnNext = document.getElementById('btn-next');
16+
17+
// Estado del algoritmo
18+
let left = 0;
19+
let right = -1;
20+
let maxLen = 0;
21+
let charMap = {}; // Diccionario para almacenar char -> index
22+
let stepState = 'IDLE'; // IDLE, EXPANDING, COLLISION
23+
24+
// Inicialización
25+
function init() {
26+
// Generar celdas HTML
27+
const cellsHTML = inputString
28+
.split('')
29+
.map(
30+
(char, index) => `
31+
<div class="cell" id="cell-${index}">
32+
<span class="index-label">${index}</span>
33+
${char}
34+
</div>
35+
`
36+
)
37+
.join('');
38+
39+
// Insertar celdas manteniendo la ventana y punteros
40+
const tempDiv = document.createElement('div');
41+
tempDiv.innerHTML = cellsHTML;
42+
while (tempDiv.firstChild) {
43+
container.insertBefore(tempDiv.firstChild, container.querySelector('.pointer-container'));
44+
}
45+
46+
updateVisuals();
47+
}
48+
49+
function updateVisuals() {
50+
// Actualizar textos
51+
elLeft.textContent = left;
52+
elRight.textContent = right;
53+
elMax.textContent = maxLen;
54+
55+
// Mover punteros
56+
// Multiplicamos por cellSize + bordes(4px) aprox, para simplicidad usamos gap de 0 en css
57+
// Ajustamos cálculo basado en que width es 60px y border es 2px (total box model)
58+
// Mejor cálculo: (Index * (cellSize + 4px de borde aprox si hubiera gap))
59+
// Como no hay gap, es Index * (cellSize + 4).
60+
// Las celdas tienen border 2px -> Total width = 64px.
61+
const cellTotalWidth = 64;
62+
63+
pointerL.style.transform = `translateX(${left * cellTotalWidth}px)`;
64+
65+
if (right === -1) {
66+
pointerR.style.display = 'none';
67+
windowEl.style.width = '0px';
68+
windowEl.style.opacity = '0';
69+
} else {
70+
pointerR.style.display = 'flex';
71+
pointerR.style.transform = `translateX(${right * cellTotalWidth}px)`;
72+
73+
// Actualizar Ventana
74+
windowEl.style.opacity = '1';
75+
windowEl.style.left = `${left * cellTotalWidth}px`;
76+
const width = (right - left + 1) * cellTotalWidth;
77+
windowEl.style.width = `${width}px`;
78+
}
79+
80+
renderMap();
81+
}
82+
83+
function renderMap() {
84+
mapBody.innerHTML = '';
85+
for (const [char, idx] of Object.entries(charMap)) {
86+
const tr = document.createElement('tr');
87+
tr.innerHTML = `<td>'${char}'</td><td>${idx}</td>`;
88+
if (char === inputString[right]) {
89+
tr.classList.add('highlight');
90+
}
91+
mapBody.appendChild(tr);
92+
}
93+
}
94+
95+
function log(msg, type = 'info') {
96+
logBox.innerHTML = msg;
97+
logBox.style.borderColor =
98+
type === 'error'
99+
? 'var(--accent-red)'
100+
: type === 'success'
101+
? 'var(--accent-green)'
102+
: 'var(--accent-blue)';
103+
}
104+
105+
function highlightCell(idx, type) {
106+
const cell = document.getElementById(`cell-${idx}`);
107+
if (cell) {
108+
// Remover clases previas
109+
cell.classList.remove('active', 'error', 'valid');
110+
if (type) cell.classList.add(type);
111+
}
112+
}
113+
114+
function clearHighlights() {
115+
for (let i = 0; i < inputString.length; i++) {
116+
document.getElementById(`cell-${i}`).classList.remove('active', 'error', 'valid');
117+
}
118+
}
119+
120+
async function nextStep() {
121+
btnNext.disabled = true;
122+
123+
// Máquina de estados simple para la lógica paso a paso
124+
125+
// Paso 1: Mover Right
126+
if (stepState === 'IDLE') {
127+
if (right + 1 >= inputString.length) {
128+
log('¡Algoritmo Terminado! Longitud Máxima alcanzada: ' + maxLen, 'success');
129+
btnNext.disabled = true;
130+
btnNext.textContent = 'Fin';
131+
return;
132+
}
133+
134+
clearHighlights();
135+
right++;
136+
updateVisuals();
137+
highlightCell(right, 'active');
138+
139+
const char = inputString[right];
140+
log(`Moviendo <b>Right</b> a índice ${right} ('${char}'). Comprobando repeticiones...`);
141+
142+
stepState = 'CHECKING';
143+
btnNext.disabled = false;
144+
return;
145+
}
146+
147+
// Paso 2: Verificar Colisión
148+
if (stepState === 'CHECKING') {
149+
const char = inputString[right];
150+
const prevIndex = charMap[char];
151+
152+
// Condición de colisión: Existe en mapa Y su índice es >= left
153+
if (prevIndex !== undefined && prevIndex >= left) {
154+
// Colisión detectada
155+
log(
156+
`¡Colisión! '${char}' ya existe en el índice ${prevIndex} (dentro de la ventana).`,
157+
'error'
158+
);
159+
highlightCell(right, 'error');
160+
highlightCell(prevIndex, 'error'); // Resaltar el duplicado original
161+
162+
stepState = 'COLLISION_RESOLVE';
163+
} else {
164+
// Sin colisión
165+
log(`'${char}' es único en la ventana actual. Expandiendo ventana.`, 'success');
166+
highlightCell(right, 'valid');
167+
168+
// Actualizar Lógica
169+
charMap[char] = right;
170+
maxLen = Math.max(maxLen, right - left + 1);
171+
172+
updateVisuals();
173+
stepState = 'IDLE'; // Volver al inicio para el siguiente char
174+
}
175+
btnNext.disabled = false;
176+
return;
177+
}
178+
179+
// Paso 3: Resolver Colisión (Saltar Left)
180+
if (stepState === 'COLLISION_RESOLVE') {
181+
const char = inputString[right];
182+
const prevIndex = charMap[char];
183+
184+
log(
185+
`Saltando <b>Left</b> del índice ${left} al ${
186+
prevIndex + 1
187+
} (previo + 1) para excluir la '${char}' repetida.`
188+
);
189+
190+
// Animación lógica
191+
left = prevIndex + 1;
192+
193+
// Actualizar mapa con la nueva posición del char actual
194+
charMap[char] = right;
195+
196+
// Recalcular max (aunque suele crecer, hay que mantener consistencia)
197+
// En este paso usualmente la ventana se encoge, así que max no cambia,
198+
// pero actualizamos el estado visual.
199+
200+
updateVisuals();
201+
clearHighlights();
202+
highlightCell(right, 'valid'); // Ahora es válido
203+
204+
stepState = 'IDLE';
205+
btnNext.disabled = false;
206+
return;
207+
}
208+
}
209+
210+
function resetViz() {
211+
left = 0;
212+
right = -1;
213+
maxLen = 0;
214+
charMap = {};
215+
stepState = 'IDLE';
216+
217+
// Limpiar UI
218+
container.innerHTML =
219+
'<div class="sliding-window" id="window"></div><div class="pointer-container"><div class="pointer pointer-l" id="pointer-l"><span>L</span><small>Left</small><span>⬆</span></div><div class="pointer pointer-r" id="pointer-r"><span>R</span><small>Right</small><span>⬆</span></div></div>';
220+
221+
// Re-vincular elementos que se borraron al limpiar container (solo window y pointers no se borran si los manejamos bien, pero innerHTML borra celdas)
222+
// Para simplicidad, recargamos la página o re-ejecutamos init limpiando celdas.
223+
// Mejor estrategia: borrar solo celdas.
224+
const cells = document.querySelectorAll('.cell');
225+
cells.forEach((c) => c.remove());
226+
227+
// Reiniciar referencias globales si fueron destruidas (no lo fueron porque estan fuera del init en structure HTML fija)
228+
// Solo necesitamos re-generar celdas.
229+
init();
230+
231+
logBox.textContent = 'Presiona "Siguiente Paso" para comenzar el algoritmo.';
232+
logBox.style.borderColor = 'var(--accent-blue)';
233+
btnNext.disabled = false;
234+
btnNext.textContent = 'Siguiente Paso ▶';
235+
}
236+
237+
// Run
238+
init();
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<!DOCTYPE html>
2+
<html lang="es">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta
6+
name="viewport"
7+
content="width=device-width, initial-scale=1.0" />
8+
<title>Visualización: Longest Substring Without Repeating Characters</title>
9+
<link
10+
rel="stylesheet"
11+
href="styles.css" />
12+
</head>
13+
<body>
14+
<h1>Ventana Deslizante</h1>
15+
<div class="subtitle">Longest Substring Without Repeating Characters</div>
16+
17+
<div class="main-container">
18+
<!-- Sección Visual -->
19+
<div class="viz-section">
20+
<div
21+
class="string-container"
22+
id="string-container">
23+
<!-- La ventana deslizante se insertará aquí via JS -->
24+
<div
25+
class="sliding-window"
26+
id="window"></div>
27+
<!-- Las celdas se generarán aquí -->
28+
<div class="pointer-container">
29+
<div
30+
class="pointer pointer-l"
31+
id="pointer-l">
32+
<span>L</span><small>Left</small>
33+
<span></span>
34+
</div>
35+
<div
36+
class="pointer pointer-r"
37+
id="pointer-r">
38+
<span>R</span><small>Right</small>
39+
<span></span>
40+
</div>
41+
</div>
42+
</div>
43+
44+
<div class="controls">
45+
<button
46+
id="btn-reset"
47+
onclick="resetViz()">
48+
Resetear
49+
</button>
50+
<button
51+
id="btn-next"
52+
onclick="nextStep()">
53+
Siguiente Paso ▶
54+
</button>
55+
</div>
56+
57+
<div
58+
class="log-box"
59+
id="log-box">
60+
Presiona "Siguiente Paso" para comenzar el algoritmo.
61+
</div>
62+
</div>
63+
64+
<!-- Dashboard -->
65+
<div class="dashboard-section">
66+
<div class="stats">
67+
<div class="stat-box">
68+
<div
69+
class="stat-value"
70+
id="val-left">
71+
0
72+
</div>
73+
<div class="stat-label">Left</div>
74+
</div>
75+
<div class="stat-box">
76+
<div
77+
class="stat-value"
78+
id="val-right">
79+
-1
80+
</div>
81+
<div class="stat-label">Right</div>
82+
</div>
83+
<div class="stat-box">
84+
<div
85+
class="stat-value"
86+
id="val-max">
87+
0
88+
</div>
89+
<div class="stat-label">Max Len</div>
90+
</div>
91+
</div>
92+
93+
<div class="hash-map-container">
94+
<div class="hash-map-title">HashMap (Char -> Last Index)</div>
95+
<table class="hash-table">
96+
<thead>
97+
<tr>
98+
<th>Char</th>
99+
<th>Index</th>
100+
</tr>
101+
</thead>
102+
<tbody id="map-body">
103+
<!-- Filas dinámicas -->
104+
</tbody>
105+
</table>
106+
</div>
107+
</div>
108+
</div>
109+
110+
<script src="app.js"></script>
111+
</body>
112+
</html>

0 commit comments

Comments
 (0)