Blog en español

Fusionar k listas ordenadas: entrevista y O(N log k)

10 de mayo de 202620 min de lectura
Fusionar k listas ordenadas: entrevista y O(N log k)

Domina fusionar k listas ordenadas con heap o divide y vencerás, explica O(N log k) y evita errores típicos en entrevista. Aprende la respuesta clave.

Los candidatos que se quedan en blanco con este problema suelen conocer la forma del código. La pregunta de entrevista sobre fusionar k listas ordenadas no es un misterio: es una prueba de reconocimiento de patrones que la mayoría no supera no porque no pueda escribir el heap, sino porque no puede explicar por qué lo está usando. El entrevistador pregunta “¿por qué un heap?” y la respuesta que sale es “porque es eficiente”, lo cual es técnicamente cierto y prácticamente inútil. Lo que el entrevistador quería era una explicación de 20 segundos sobre por qué este es un problema de k-way merge, por qué eso cambia la forma de la solución y por qué el enfoque elegido llega a O(N log k) sin vaguedades.

Esa es la brecha que cierra este artículo. No solo el código, sino la decisión, la justificación y la explicación oral que puede dar sin divagar.

Vea el k way merge antes de tocar el código

Por qué esto no es solo “fusionar dos listas” con pasos extra

La intuición es pensar en esto como aplicar repetidamente la fusión de dos listas. Fusiona la lista 1 y la lista 2, toma el resultado, fusiónalo con la lista 3, y así sucesivamente. Esto funciona. También produce una complejidad temporal de O(Nk) en el peor caso, y le indica al entrevistador que está resolviendo el problema equivocado.

La diferencia estructural es esta: fusionar dos listas es una decisión bilateral en cada paso: nodo izquierdo o nodo derecho. Fusionar k listas ordenadas es una decisión multilateral: ¿cuál de las k fronteras actuales tiene el nodo más pequeño? Eso es un trabajo fundamentalmente distinto, y requiere una estructura de datos diferente. Cuando reconoce fusionar k listas ordenadas como un k-way merge, el espacio de soluciones se reduce a dos opciones claras: un min-heap que siga explícitamente la frontera, o una estrategia de divide y vencerás que programe fusiones por parejas en capas equilibradas. Ambas son correctas. Ninguna es “fusionar dos listas con pasos extra”.

Los candidatos que suenan fluidos en las entrevistas son los que nombran esta distinción antes de tocar el código. No es una trampa: es el patrón. Una vez que ve el k-way merge, el resto es un detalle de implementación.

Qué está evaluando realmente la pregunta de entrevista

El entrevistador está evaluando tres cosas a la vez: reconocimiento de patrones, control de la complejidad y su capacidad para explicar por qué el siguiente nodo más pequeño solo puede venir de uno de los k cabezales actuales de las listas, nunca de más adentro de ninguna lista, porque cada lista ya está ordenada.

Ese último punto es el invariante sobre el que se apoya toda la solución. En cada paso, el nodo restante globalmente más pequeño debe ser el cabezal de una de las k listas restantes. No necesita mirar el segundo nodo de ninguna lista. No necesita recorrerlas. Solo necesita encontrar de forma eficiente el mínimo entre k candidatos, avanzar el puntero de esa lista y repetir. Todo lo demás —heap, recursión, árbol de fusiones— es solo una forma de hacer rápida esa operación.

Cómo se ve esto en la práctica

Tome tres listas: `[1→4→7]`, `[2→5→8]`, `[3→6→9]`. El primer nodo de salida tiene que ser 1, que es el cabezal de la lista 1. Después de tomar 1, los candidatos son `[4]`, `[2]`, `[3]`. El siguiente nodo de salida es 2, de la lista 2. Ahora los candidatos son `[4]`, `[5]`, `[3]`. Luego va 3, y así sucesivamente.

Observe lo que está ocurriendo: en cada paso elige entre exactamente k candidatos y avanza exactamente un puntero. El número total de esas decisiones es N (el total de nodos en todas las listas). Cada decisión cuesta algo que depende de k, no de N. De ahí surge la forma O(N log k) directamente desde la estructura del problema, no desde la implementación, sino desde lo que el problema realmente exige.

Los candidatos fluidos nombran esto antes de escribir una sola línea. Los candidatos que solo memorizaron fusionar dos listas empiezan a codificar de inmediato y luego no pueden explicar la complejidad cuando se les pregunta.

Haga que O(N log k) sea el objetivo, no una suposición

Por qué al entrevistador le importa la k del logaritmo

O(N log k) es la afirmación de complejidad que separa una solución correcta de una buena. La razón por la que aparece k en el logaritmo es que tanto el heap como el árbol de fusión de divide y vencerás realizan operaciones cuyo coste escala con el número de listas, no con el número total de nodos. Cuando k es pequeño —digamos, 10 listas— log k es aproximadamente 3.3. Cuando N es un millón, la diferencia entre 3.3 millones de operaciones y mil millones es enorme. Por eso le importa al entrevistador. Como se describe en Introduction to Algorithms (CLRS), el k-way merge con una cola de prioridad alcanza exactamente ese límite, y es una referencia estándar para problemas de selección basados en heap.

Cómo se ve esto en la práctica

Aquí tiene el argumento de conteo que puede decir en voz alta. Hay N nodos en total. Cada nodo se inserta en el heap exactamente una vez y se extrae exactamente una vez. Cada inserción o extracción en el heap cuesta O(log k) porque el heap nunca contiene más de k elementos: uno por lista. Así que el trabajo total es N inserciones × O(log k) cada una = O(N log k). El mismo argumento se aplica a divide y vencerás: hay log k niveles en el árbol de fusiones (porque el número de listas se reduce a la mitad en cada nivel), y cada nivel procesa cada nodo exactamente una vez, lo que da O(N) de trabajo por nivel × O(log k) niveles = O(N log k).

Ambas estrategias llegan al mismo sitio. La diferencia está en cómo se programa el trabajo, no en cuánto trabajo hay.

La respuesta que da cuando le preguntan por qué no O(Nk)

El enfoque ingenuo —recorrer los k cabezales en cada paso para encontrar el mínimo— cuesta O(k) por decisión de nodo. Con N nodos en total, eso es O(Nk). Para k=2 está bien. Para k=1000, está haciendo mil comparaciones para extraer un nodo, y eso un millón de veces. La estructura se rompe en cuanto k crece, que es exactamente el escenario de entrevista en el que k se trata como una variable significativa.

El heap sustituye ese recorrido O(k) por una operación de heap O(log k). La maquinaria extra —el heap en sí— merece la pena porque cambia un recorrido lineal por uno logarítmico. Ese es todo el argumento. No está añadiendo complejidad por elegancia; la está añadiendo porque el recorrido ingenuo es realmente más lento cuando k no es trivial.

Use un min heap cuando quiera la respuesta en vivo más clara

Por qué la versión con heap es tan amigable para la entrevista

Un min-heap mantiene exactamente un nodo por cada lista activa en cada momento: el cabezal actual. El flujo de control es fácil de describir en una sola frase: inserte todos los cabezales iniciales y luego extraiga repetidamente el mínimo, agréguelo al resultado y empuje el siguiente nodo de esa lista si existe. No hay llamadas recursivas, ni aritmética de índices, ni contabilidad de árbol de fusiones. Bajo presión de tiempo, esa simplicidad vale mucho.

La versión con heap también es fácil de confiar. Como el invariante del heap garantiza que el mínimo siempre está arriba, nunca tiene que razonar sobre si pudo haberse perdido un nodo más pequeño en algún sitio. La estructura de datos hace ese razonamiento por usted.

Cómo se ve esto en la práctica

Empiece con tres listas enlazadas: `1→4→7`, `2→5→8`, `3→6→9`. Inicialice el heap con los cabezales: inserte `(1, node_1_4_7)`, `(2, node_2_5_8)`, `(3, node_3_6_9)`.

Paso 1: extraiga `(1, node)`. Añada el nodo 1 al resultado. Inserte el siguiente nodo: `(4, node_4_7)`. El heap ahora contiene `(2, ...)`, `(3, ...)`, `(4, ...)`.

Paso 2: extraiga `(2, node)`. Añada el nodo 2. Inserte `(5, node_5_8)`. Heap: `(3, ...)`, `(4, ...)`, `(5, ...)`.

Paso 3: extraiga `(3, node)`. Añada el nodo 3. Inserte `(6, node_6_9)`. Y así sucesivamente.

El tamaño del heap nunca supera k. Cada nodo se inserta una vez y se extrae una vez. La cadena fusionada se construye correctamente porque siempre añade el mínimo global. Este es el recorrido que debe ser capaz de narrar sin mirar el código.

El pequeño y feo problema de heapq en Python

`heapq` de Python compara las tuplas de izquierda a derecha. Si dos nodos tienen el mismo valor, intenta comparar directamente los objetos `ListNode` —y como `ListNode` no implementa `__lt__`, eso lanza un `TypeError`. La solución es una tupla de tres elementos: `(node.val, index, node)`, donde `index` es el índice de la lista (de 0 a k-1). Como los índices son únicos, la comparación nunca llega al objeto `ListNode`.

Según la documentación de heapq de Python, los elementos del heap se comparan usando las reglas estándar de comparación de Python, lo que significa que cualquier elemento de desempate debe ser comparable. El índice proporciona esa garantía de forma limpia.

La otra trampa: listas nulas. Si alguna lista de entrada es `None` o está vacía, insertarla puede hacer que el programa falle o corrompa silenciosamente el heap. Filtre esos casos antes de inicializarlo. Son dos líneas y le evitan un error en tiempo de ejecución que parece un fallo lógico.

Cada nodo se inserta una vez, se extrae una vez. El índice `i` rompe los empates sin tocar el objeto nodo.

Trate divide y vencerás como la misma idea, solo mejor organizada

Por qué el árbol de fusiones no es un truco distinto

La fusión con divide y vencerás es el mismo trabajo de k-way merge, solo que programado de otra forma. En lugar de que un heap siga la frontera dinámicamente, agrupa listas por parejas y fusiónelas, luego agrupe los resultados y fusiónelos otra vez, repitiendo hasta que quede una sola lista. La estructura que está construyendo es un árbol binario de fusiones equilibrado, y la profundidad de ese árbol es log k, exactamente el mismo log k que aparece en la complejidad del heap.

La idea clave es que cada fusión por parejas es O(n₁ + n₂), donde n₁ y n₂ son los tamaños de las dos listas que se fusionan. En cada nivel del árbol, el trabajo total de todas las fusiones es O(N), porque cada nodo participa exactamente en una fusión en cada nivel. Con log k niveles, el trabajo total es O(N log k). El análisis de merge sort en CLRS presenta la misma estructura de recurrencia: esto es merge sort aplicado a k listas en lugar de k elementos.

Cómo se ve esto en la práctica

Con ocho listas, la ronda 1 produce cuatro listas fusionadas. La ronda 2 produce dos. La ronda 3 produce una. Tres rondas = log₂(8) = 3 niveles. Cada nivel procesa todos los N nodos una vez. El árbol queda equilibrado por construcción si siempre empareja de fuera hacia dentro (lista 0 con lista 1, lista 2 con lista 3, y así sucesivamente), lo que mantiene los tamaños de las listas aproximadamente iguales entre rondas.

Con cuatro listas de tamaños 3, 3, 3, 3: la ronda 1 las fusiona en dos listas de tamaño 6, la ronda 2 las fusiona en una lista de tamaño 12. Comparaciones totales: 6 + 6 + 12 = 24 = 12 × 2 = N × log k. Las cuentas cuadran. Cada paso de fusión preserva el orden porque fusionar dos listas ordenadas es correcto por inducción, y cada nivel solo toca cada nodo una vez.

Cuándo suena más inteligente esta explicación en una entrevista

Divide y vencerás resulta más limpio cuando el entrevistador menciona explícitamente la recursión, cuando quiere hablar de paralelización —cada pareja puede fusionarse de forma independiente— o cuando está explorando intuición de merge sort. También es la mejor respuesta si el entrevistador pregunta “¿y si tuviera que hacer esto en un sistema distribuido?”: el árbol de fusiones se mapea directamente a una operación reduce entre trabajadores.

La respuesta con heap es más rápida de implementar. La respuesta con divide y vencerás suele ser más fácil de defender conceptualmente, porque el árbol de fusiones es algo que puede dibujar en una pizarra en 10 segundos y señalar mientras explica la complejidad.

Elija heap o divide y vencerás como alguien que ya ha hecho esto antes

La matriz de decisión que los entrevistadores realmente respetan

La elección no consiste en cuál enfoque es objetivamente mejor: son equivalentes en complejidad. Se trata de cuál puede implementar correctamente y explicar con claridad en el tiempo que tiene.

Use un min-heap cuando: esté en Python y quiera usar `heapq` directamente, la entrevista sea una sesión de codificación en vivo donde la simplicidad reduzca errores, o el entrevistador no haya mencionado recursión. La versión con heap es más corta, más lineal en su estructura y más fácil de seguir en un recorrido mental.

Use divide y vencerás cuando: el entrevistador mencione recursión, merge sort o soluciones con estructura de árbol; cuando pregunte por paralelismo o procesamiento distribuido; o cuando esté en un lenguaje donde la recursión es idiomática y las bibliotecas de heap son menos ergonómicas. La explicación del árbol de fusiones también suele funcionar mejor en contextos cercanos al system design, donde las operaciones reduce son vocabulario familiar.

Cómo se ve esto en la práctica

En una entrevista telefónica en Python: empiece con el heap. Diga “Voy a usar un min-heap para seguir el cabezal actual de cada lista, lo que me da O(N log k) sin necesitar recursión”. Escríbalo. Listo.

En una entrevista de pizarra donde el entrevistador dibuja un árbol: cambie a divide y vencerás. “También podemos pensarlo como un árbol de fusiones: emparejamos las listas, fusionamos cada par y repetimos. Misma O(N log k), y encaja muy bien con el árbol que acaba de dibujar.”

Si le preguntan cuál prefiere: “El heap es más rápido de implementar en Python. Divide y vencerás es más fácil de explicar visualmente y se extiende de forma natural a fusiones en paralelo. Ambas son correctas; elegiré la que mejor encaje con el contexto”. Esa respuesta impresiona más que una seguridad falsa.

El error que suena seguro pero no lo es

Decir “el heap siempre es mejor” o “divide y vencerás es más limpio” sin matices indica que ha memorizado una preferencia, no que haya desarrollado criterio. Los entrevistadores que conocen bien este problema lo van a cuestionar de inmediato: “¿y si k es muy grande? ¿y si las listas están en máquinas distintas?” Y una preferencia memorizada no tiene respuesta.

La versión honesta es: ambos enfoques resuelven el mismo problema con la misma complejidad, y la elección correcta depende del contexto de la entrevista, del lenguaje y de lo que el entrevistador esté evaluando realmente. Esa respuesta es más difícil de decir, y es la que gana respeto.

Códigolo sin tropezar con los obstáculos habituales

Los riesgos de listas nulas y valores duplicados

Antes de escribir una línea de código para fusionar k listas enlazadas ordenadas, responda tres preguntas: ¿alguna lista puede ser `None`? ¿alguna lista puede estar vacía (un puntero no nulo a nada)? ¿puede aparecer el mismo valor en varias listas?

Las listas nulas rompen la inicialización del heap si las inserta sin comprobar. Las listas vacías son una variante sutil: el puntero existe, pero `node` es falso. Ambas deben filtrarse antes de inicializar el heap. Los valores duplicados no son un error: se fusionan correctamente, pero desencadenan el problema de comparación de Python descrito arriba si no usa el desempate con índice.

Cómo se ve esto en la práctica

El modo de fallo sin el desempate con índice: dos nodos con valor 5 de listas distintas. Python extrae el primero, inserta su siguiente nodo (valor 7) y luego intenta comparar el `(5, ListNode)` restante con `(7, ListNode)`. Los valores son distintos, así que nunca llega a comparar los nodos: bien. Pero si más tarde llega otro 5, compara `(5, ListNode)` con `(5, ListNode)` y falla. El índice en la tupla evita esto por completo: `(5, 0, node)` frente a `(5, 2, node)` se resuelve con el índice, nunca con el nodo.

La versión limpia reutiliza los nodos directamente —`tail.next = node`— en lugar de crear nodos nuevos. Eso es correcto siempre que avance `node.next` antes de reasignar `tail`. El invariante de una sola pasada: `tail` siempre apunta al último nodo de la cadena fusionada, y nada detrás de él tiene un puntero suelto.

La mentalidad de una sola pasada que mantiene sanos los punteros

Cada nodo se inserta en el heap una vez y se extrae una vez. Cuando extrae un nodo y asigna `tail.next = node`, ese nodo pasa a formar parte de la cadena fusionada. Cuando inserta `node.next`, está encolando el siguiente candidato de esa lista. El único puntero que muta es `tail`: lo avanza al nodo que acaba de añadir. Nada más cambia.

El invariante: en cada paso del bucle, `dummy.next` es la cabeza de una cadena correctamente fusionada que termina en `tail`, y el heap contiene exactamente los cabezales actuales de todas las listas no agotadas. Si ese invariante se mantiene al inicio de cada iteración, se mantiene al final. Cada nodo se inserta una vez, se extrae una vez, se añade una vez.

Explíquelo con tanta claridad que el entrevistador deje de interrumpirle

La respuesta de 20 segundos

Aquí tiene la versión hablada: “Este es un problema de k-way merge. En cada paso, el siguiente nodo de salida es el mínimo de los cabezales actuales de las k listas. Voy a usar un min-heap para seguir esos cabezales de forma eficiente, lo que me da O(log k) por extracción y O(N log k) en total. Me encargaré de las listas nulas antes de inicializar el heap y usaré un índice de desempate en Python para evitar errores de comparación con valores iguales”.

Eso es todo. Treinta segundos, cubre el patrón, el enfoque, la complejidad y los casos límite. El entrevistador ahora sabe que entiende el problema antes de que haya escrito una sola línea.

Cómo se ve esto en la práctica

Una buena apertura para explicar fusionar k listas ordenadas en una entrevista: “Antes de codificar, quiero asegurarme de que entiendo las restricciones. ¿Alguna de las listas de entrada puede ser nula o estar vacía? ¿Hay valores duplicados entre listas? ¿Y se permite reutilizar los nodos existentes en el sitio, o quieren que cree nodos nuevos?”

Esas tres preguntas tardan 10 segundos y demuestran que ha pensado en el problema más allá del caso ideal. Luego: “Perfecto. Haré un enfoque con min-heap. Insertaré la cabeza de cada lista no nula en el heap como una tupla de (valor, índice, nodo) para resolver empates, luego extraeré el mínimo, lo añadiré al resultado y empujaré su sucesor si existe. Esto corre en O(N log k) de tiempo y O(k) de espacio para el heap”.

Después codifica. No pide permiso para empezar. Va narrando brevemente mientras escribe, pero no se detiene a explicar cada línea.

Cuando le presionan para justificar por qué su elección es la correcta

La respuesta probada bajo presión: “El heap es la opción correcta aquí porque mantiene la implementación lineal y el invariante fácil de verificar: el heap siempre contiene como máximo k elementos, así que cada operación cuesta O(log k), y hacemos exactamente N de ellas. Si quisiera hablar del enfoque de divide y vencerás, también puedo: llega al mismo límite O(N log k) mediante un árbol de fusiones, y a menudo es más fácil de paralelizar. Pero para una implementación en una sola máquina en Python, el heap es más simple de hacer bien bajo presión de tiempo”.

Esa respuesta defiende la elección sin descartar la alternativa, y termina volviendo a O(N log k), que es donde debería aterrizar la atención del entrevistador.

Los candidatos sólidos dicen la complejidad antes de que se la pidan. Los candidatos más débiles esperan a que se la pidan y luego dan un número sin justificación. La diferencia no es el conocimiento: es el hábito de explicar mientras codifican, no después.

Cómo Verve AI puede ayudarle a prepararse para su entrevista sobre fusionar k listas ordenadas

El problema estructural al prepararse para esta pregunta es que leer sobre heap frente a divide y vencerás no construye la habilidad que realmente evalúa la entrevista: decir en voz alta la explicación de 20 segundos, bajo presión, a alguien que va a hacerle preguntas de seguimiento. Puede leer todos los recorridos guiados de internet y aun así quedarse en blanco cuando el entrevistador pregunte “¿por qué no O(Nk)?” si nunca ha tenido que responder eso en tiempo real.

Verve AI Interview Copilot está diseñado precisamente para esa brecha. Escucha en tiempo real su respuesta oral, ve lo que realmente está diciendo en lugar de lo que planeó decir y responde a la parte concreta que se desvió, no a una rúbrica genérica. Si explica correctamente el heap pero tropieza con la justificación de la complejidad, Verve AI Interview Copilot detecta esa brecha de inmediato, no después de que ya haya pasado a otra cosa. Si su explicación de 20 segundos se alarga a dos minutos, también lo detecta.

La práctica que realmente ayuda no es volver a leer el algoritmo: es hacer el recorrido mental en voz alta, narrar la inserción y extracción del heap sobre tres listas enlazadas, y luego defender O(N log k) cuando alguien le lleva la contraria. Verve AI Interview Copilot realiza entrevistas simuladas que replican esa presión sin necesitar que una persona esté disponible a las 11 de la noche anterior a su entrevista. Úselo para ensayar la explicación oral hasta que el argumento de complejidad salga limpio, sin notas.

Conclusión

Nunca fue una pregunta sobre si puede escribir un heap. La pregunta de entrevista sobre fusionar k listas ordenadas es una prueba de si puede reconocer un k-way merge, elegir entre dos estrategias válidas y defender esa elección en el tiempo que tarda el entrevistador en decidir si sigue preguntando o pasa a otra cosa.

La decisión no es complicada una vez que la ha interiorizado: heap cuando quiere una implementación limpia y lineal y codificar rápido; divide y vencerás cuando el entrevistador quiere recursión, razonamiento con árbol de fusiones o pensamiento en paralelo. Ambas llegan a O(N log k). Ambas son defendibles. La única incorrecta es la que no puede explicar.

Antes de su próxima entrevista, haga dos cosas. Ensaye la explicación de 20 segundos —patrón, enfoque, complejidad, casos límite— hasta que salga sin pensar. Luego haga un recorrido mental de punteros sobre tres listas enlazadas, narrando en voz alta cada inserción y extracción del heap. Si puede hacer ambas cosas sin notas, está listo para la pregunta. Si tropieza, eso es lo que debe corregir: no el código, sino la explicación.

VA

Verve AI

Contenido