- Publicado el
Mi obsesión con LeetCode 41: Una inmersión profunda en el acertijo del primer entero positivo que falta
- Autores
- Nombre
- Imamuzzaki Abu Salam
- https://x.com/ImBIOS_Dev
El problema de "First Missing Positive" de LeetCode (número 41) se ha convertido en mi última obsesión en la codificación. Es un rompecabezas engañosamente simple que ha consumido mis pensamientos, impulsándome a explorar sus intrincados detalles y a crear la solución en TypeScript más eficiente posible. Esta publicación no trata solo de sobresalir en las entrevistas de codificación (aunque ese es un buen beneficio adicional). Se trata del puro placer de resolver problemas, la emoción de la optimización y la elegante belleza de un código eficiente. Y como soy un poco aficionado a las matemáticas, incluso profundizaremos en los fundamentos matemáticos de la solución.
El Problema: Un Lobo con Piel de Cordero
El problema presenta un array desordenado de enteros. Tu tarea: encontrar el entero positivo más pequeño que falta. ¿Parece sencillo? Piénsalo de nuevo. Las restricciones de tiempo y complejidad espacial transforman esta tarea aparentemente simple en un desafiante rompecabezas algorítmico.
Luchas Iniciales: La Trampa de la Fuerza Bruta
Mis intentos iniciales incluyeron la ordenación (violando la restricción de tiempo ) y conjuntos hash (excediendo el límite de espacio ). Claramente, se necesitaba un enfoque más sofisticado.
El Momento Eureka: Transformación In-situ
La idea clave fue darme cuenta de que podía usar el array de entrada como una tabla hash. Al reorganizar los elementos, podía colocar el número en el índice (si , donde es la longitud del array). Esta transformación in-situ desbloquea el camino hacia una solución eficiente.
Evolución del Código: Una Saga de TypeScript
Aquí está la crónica de la evolución de mi código, con explicaciones detalladas y conocimientos matemáticos:
Versión 1: El Enfoque Ingenuo (con Intercambios Redundantes)
function firstMissingPositive(nums: number[]): number {
const n = nums.length
// Coloca cada número en su posición correcta si está en el rango [1, n]
for (let i = 0; i < n; i++) {
while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] !== nums[i]) {
const correctIndex = nums[i] - 1
;[nums[i], nums[correctIndex]] = [nums[correctIndex], nums[i]] // Intercambio
}
}
// Encuentra el primer positivo que falta
for (let i = 0; i < n; i++) {
if (nums[i] !== i + 1) {
return i + 1
}
}
// Si todos los números están en sus posiciones correctas, el número que falta es n + 1
return n + 1
}
Esta versión, aunque funcional, sufre de intercambios redundantes. El número de intercambios en el peor de los casos se aproxima a , aunque el caso promedio está más cerca de .
Explicación:
Reorganiza el array: Cada número
nums[i]
se coloca en su índice "correcto" (nums[i] - 1
) si está dentro del rango[1, n]
y no está ya en la posición correcta.Identifica el positivo que falta: Después de la reorganización, el primer índice
i
dondenums[i] !== i + 1
indica quei + 1
es el entero positivo que falta.Devuelve n + 1 si es necesario: Si todos los números de
1
an
están en las posiciones correctas, el número que falta esn + 1
.
Puntos Clave:
- Reorganización in-situ: Modificamos el array en sí para evitar el uso de espacio extra.
- Eficiencia: Tanto la reorganización como el escaneo final tardan tiempo, asegurando un rendimiento óptimo.
Versión 2: Aumento del Rendimiento (Eliminando Intercambios Redundantes)
function firstMissingPositive(nums: number[]): number {
const n = nums.length
let idx = 0
// Reorganiza los números a sus posiciones correctas si es posible
while (idx < n) {
const correctIdx = nums[idx] - 1
if (correctIdx >= 0 && correctIdx < n && nums[idx] !== nums[correctIdx]) {
// Intercambio sin usar una variable temporal
;[nums[idx], nums[correctIdx]] = [nums[correctIdx], nums[idx]]
} else {
idx++
}
}
// Identifica el primer positivo que falta
for (let i = 0; i < n; i++) {
if (nums[i] !== i + 1) {
return i + 1
}
}
return n + 1
}
Explicación:
Reorganización Eficiente:
- El bucle
while
intercambia elementos directamente a sus posiciones correctas (nums[idx] - 1
) solo cuando el número está en el rango válido[1, n]
y no está ya en su posición correcta. - La condición
if
asegura que los números inválidos o ya correctos se salten sin intercambios innecesarios.
- El bucle
Comprobación Directa del Índice:
- Durante la fase de reorganización, se evitan intercambios inválidos y operaciones redundantes comprobando el rango y el valor de
nums[idx]
.
- Durante la fase de reorganización, se evitan intercambios inválidos y operaciones redundantes comprobando el rango y el valor de
Intercambio Eficiente en Memoria:
- El uso de desestructuración (
[nums[idx], nums[correctIdx]] = [nums[correctIdx], nums[idx]]
) elimina la necesidad de una variable temporal, minimizando el uso de memoria.
- El uso de desestructuración (
Bucles Mínimos:
- El algoritmo utiliza solo dos pasadas lineales:
- Una para reorganizar el array in-situ.
- Otra para encontrar el primer positivo que falta.
- El algoritmo utiliza solo dos pasadas lineales:
Análisis de Complejidad:
- Complejidad Temporal:
- El bucle
while
y el buclefor
se ejecutan en , haciendo que la complejidad temporal total sea .
- El bucle
- Complejidad Espacial:
- El algoritmo opera in-situ sin usar ninguna estructura de datos auxiliar, por lo que la complejidad espacial es .
Por Qué Este Código Es Óptimo:
- Rendimiento: El bucle
while
optimizado asegura operaciones redundantes mínimas, maximizando la velocidad. - Eficiencia de Memoria: El uso de intercambio in-situ y la evitación de variables adicionales asegura la menor huella de memoria posible.
Versión 3: Optimizado para Memoria (Uso Mínimo de Espacio)
function firstMissingPositive(nums: number[]): number {
const n = nums.length
let idx = 0
while (idx < n) {
const correctIdx = nums[idx] - 1
if (correctIdx >= 0 && correctIdx < n && nums[idx] !== nums[correctIdx]) {
nums[idx] = nums[correctIdx]
nums[correctIdx] = correctIdx + 1
} else {
idx++
}
}
for (let idx = 0; idx < n; idx++) {
if (nums[idx] !== idx + 1) {
return idx + 1
}
}
return nums.length + 1
}
Optimizaciones Clave de Memoria:
Actualizaciones In-situ con Variables Temporales Mínimas:
- En lugar de usar una variable
temp
para el intercambio, este código modifica directamente el array connums[idx] = nums[correctIdx]; nums[correctIdx] = correctIdx + 1
. - Esto elimina la necesidad de una variable temporal, reduciendo la sobrecarga de memoria en una cantidad constante.
- En lugar de usar una variable
Menos Valores Temporales en el Bucle:
- El código calcula
correctIdx
directamente en el bucle, evitando la necesidad de almacenar variables intermedias adicionales fuera de la lógica principal. - Menos asignaciones de variables significan un uso de memoria ligeramente menor por iteración.
- El código calcula
Una Sola Pasada para la Reorganización:
- A diferencia del primer código que utiliza condiciones anidadas en el bucle
while
, esta implementación completa los intercambios o avanza el índice de una manera más eficiente, reduciendo la profundidad de la pila de ejecución y el uso de memoria. - La estructura del bucle (
while
con un solo incremento o reorganización) es más directa, requiriendo menos valores auxiliares.
- A diferencia del primer código que utiliza condiciones anidadas en el bucle
Sin Comprobaciones Redundantes:
- Las comprobaciones para
correctIdx >= 0 && correctIdx < n && nums[idx] !== nums[correctIdx]
son concisas y evitan operaciones innecesarias que podrían usar temporalmente memoria de pila o caché de CPU. - Esto evita la necesidad de asignar recursos adicionales para operaciones que no contribuirán al resultado.
- Las comprobaciones para
Por Qué Esto Marca la Diferencia en Memoria:
- Almacenamiento Temporal Reducido: Al no depender de una variable
temp
y minimizar los cálculos intermedios, la huella de memoria es menor. - Ejecución Optimizada: Menos pasos y una lógica más simple se traducen en un menor uso de memoria por operación.
- Utilización Mejorada de la Caché: El patrón de acceso lineal y predecible del algoritmo mejora el rendimiento de la caché de la CPU, reduciendo indirectamente la sobrecarga de memoria.
El enfoque de esta solución en actualizaciones in-situ directas, uso mínimo de variables y patrones de acceso a memoria eficientes la convierte en la mejor en términos de eficiencia de memoria. Si bien los ahorros son constantes (), son lo suficientemente significativos como para registrarse en plataformas competitivas como LeetCode, especialmente para conjuntos de datos grandes.
Versión 4: La Obra Maestra Optimizada (Intercambio In-situ, Sin Variable Temporal)
function firstMissingPositive(nums: number[]): number {
for (let i = 0; i < nums.length; i++) {
while (nums[i] !== i + 1) {
if (nums[i] <= 0 || nums[i] > nums.length || nums[i] === nums[nums[i] - 1]) {
break
}
const temp = nums[i]
nums[i] = nums[temp - 1]
nums[temp - 1] = temp
}
}
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== i + 1) {
return i + 1
}
}
return nums.length + 1
}
Esta versión final logra tanto un rendimiento óptimo como un uso mínimo de memoria mediante un elegante intercambio in-situ sin una variable temporal. La complejidad temporal ahora es firmemente , y la complejidad espacial es , cumpliendo todos los requisitos. Matemáticamente, estamos realizando un tipo de permutación cíclica dentro del array para colocar los elementos en sus posiciones "correctas".
Al agregar una comprobación (nums[i] === nums[nums[i] - 1]
), evitamos intercambios innecesarios. Esto mejora significativamente el rendimiento, acercando la complejidad temporal al deseado.
Diferencias Clave en la Implementación:
Evitando Intercambios Redundantes:
- El código proporcionado comprueba explícitamente si
nums[i] === nums[nums[i] - 1]
antes de realizar el intercambio. Esto evita intercambios innecesarios cuando el número ya está en la posición correcta o existen duplicados. - En la primera implementación, esta comprobación se omite, lo que potencialmente lleva a intercambios redundantes incluso cuando son innecesarios. Cada intercambio adicional agrega sobrecarga, especialmente para arrays más grandes.
- El código proporcionado comprueba explícitamente si
Comparación Directa del Índice:
- El código proporcionado utiliza
while (nums[i] !== i + 1)
para asegurar que un número se intercambia repetidamente a su posición correcta hasta que se coloca correctamente o se cumple una condición inválida. Esto elimina iteraciones de bucle innecesarias. - El primer código no compara explícitamente el número con su índice previsto dentro de la condición del bucle. Puede realizar más operaciones en casos donde los números necesitan un movimiento mínimo.
- El código proporcionado utiliza
Comprobaciones Condicionales Optimizadas:
- Al combinar condiciones en un solo bloque
if
(por ejemplo,nums[i] <= 0 || nums[i] > nums.length || nums[i] === nums[nums[i] - 1]
), el código evita la sobrecarga adicional de comprobaciones o cálculos innecesarios.
- Al combinar condiciones en un solo bloque
Por Qué Esto Importa en LeetCode:
Consistencia del Tiempo de Ejecución:
- Las mediciones del tiempo de ejecución de LeetCode pueden ser sensibles a operaciones innecesarias, como intercambios redundantes o comparaciones adicionales.
- El código proporcionado minimiza estas operaciones, lo que lleva a un tiempo de ejecución más rápido en promedio.
Eficiencia de la Caché:
- Menos intercambios y una lógica de bucle más simple resultan en una mejor utilización de la caché de la CPU. Los procesadores modernos se benefician de patrones de acceso predecibles y optimizados, que el código proporcionado exhibe.
Resultados
Aquí hay un resumen presentado en formato de tabla:
Intento | Tiempo de Ejecución | Uso de Memoria | Notas |
---|---|---|---|
4 (Optimizado) | 1 ms | 58.8 MB | Mejor equilibrio de rendimiento y memoria. |
3 (Mejor Memoria) | 2 ms | 58.5 MB | Ligeramente más lento pero menor uso de memoria. |
2 (Mejor Rendimiento) | 1 ms | 58.9 MB | Tiempo de ejecución más rápido. |
1 (Intento Inicial) | 3 ms | 59 MB | Más lento y mayor uso de memoria. |
Esta tabla destaca las compensaciones y la optimalidad de la solución final.
El resumen indica que el intento para el mejor rendimiento y memoria es de hecho el más optimizado, logrando:
- 1 ms de tiempo de ejecución: Coincidiendo con el resultado más rápido en términos de rendimiento.
- 58.9 MB de uso de memoria: Ligeramente superior al clon de "mejor memoria" pero inferior a otros intentos.
Análisis:
Tiempo de Ejecución:
- El tiempo de ejecución de 1 ms muestra que el enfoque optimizado elimina las comprobaciones redundantes y los intercambios innecesarios.
- La complejidad temporal consistente de asegura la escalabilidad para conjuntos de datos más grandes.
Memoria:
- 58.9 MB es competitivo, mostrando que la huella de memoria es baja a pesar de la ejecución rápida. Este ligero aumento con respecto a "mejor memoria" (58.5 MB) podría deberse a diferencias en la desestructuración o optimizaciones específicas del motor.
Compensaciones:
- La solución de "mejor memoria" sacrifica ligeramente el tiempo de ejecución para un menor uso de memoria.
- La solución de "mejor rendimiento" se centra más en la velocidad pero utiliza marginalmente más memoria.
Conclusión:
La solución optimizada logra un buen equilibrio:
- 1 ms de tiempo de ejecución es tan rápido como el código con mejor rendimiento.
- 58.9 MB de uso de memoria está cerca del mejor resultado de memoria, con una sobrecarga insignificante.
Es una opción completa, especialmente para escenarios donde tanto el rendimiento como la memoria son críticos.
Fundamentos Matemáticos: Permutaciones Cíclicas y Principio del Palomar
La idea central gira en torno al concepto de permutaciones cíclicas. Nuestro objetivo es crear un ciclo donde cada número se coloque en el índice . El bucle while
recorre eficazmente estos ciclos, asegurando que cada elemento encuentre su lugar designado.
El Principio del Palomar juega un papel sutil aquí. Dado que tenemos posiciones posibles (índices a ) y estamos buscando un entero positivo que falta dentro del rango , si todos los números del 1 al están presentes, el número que falta debe ser .
La Obsesión Continúa...
Mi fascinación con LeetCode 41 permanece. Constantemente busco más optimizaciones y conocimientos más profundos. ¡Únete a mí en esta búsqueda! Comparte tus ideas, tus propias soluciones y exploremos juntos la elegante intersección de las matemáticas y los algoritmos.