Comprender cómo se ubican los datos en la memoria RAM nos permite anticipar el costo real de cada operación. No basta con saber cuántos elementos contiene una estructura; también debemos conocer si están juntos, qué metadatos adicionales transportan y cuánto tarda la CPU en traerlos a los registros.
La representación también determina cuánto provecho obtenemos de la caché de CPU y cómo se comportan los algoritmos bajo presión. Un diseño acertado evita realocaciones costosas y reduce errores de manejo de punteros, dos de los problemas más frecuentes cuando pasamos de ejemplos académicos a sistemas reales.
El objetivo de este tema es comparar diferentes disposiciones físicas, entender cómo los punteros describen relaciones entre nodos y reconocer el impacto directo sobre la velocidad. Cada subsección contiene ejemplos prácticos y criterios para elegir la opción adecuada en tus proyectos.
Cuando hablamos de memoria contigua nos referimos a bloques consecutivos de direcciones. Es el caso de los arrays estáticos o de estructuras como std::vector, que reservan segmentos completos para garantizar acceso directo. Las estructuras no contiguas, como las listas enlazadas, pueden ubicar cada nodo en cualquier parte del heap y conectarlos a través de referencias.
Los punteros son direcciones que apuntan al siguiente dato o al inicio de un buffer. Para las estructuras no contiguas funcionan como el pegamento que mantiene todo unido, motivo por el cual debemos validar cada referencia antes de usarla. Las referencias, comunes en lenguajes como C++, son alias seguros que evitan reasignaciones accidentales pero siguen representándose internamente como un puntero.
int buffer[4] = {4, 8, 15, 16};
int *ptr = buffer; // Dirección contigua
struct Nodo {
int valor;
struct Nodo *siguiente;
};
struct Nodo cabeza = {23, NULL};
struct Nodo otro = {42, NULL};
cabeza.siguiente = &otro; // Encadenamiento explícito
La manipulación de punteros requiere reglas claras: inicializarlos con valores conocidos, evitar dobles liberaciones y documentar la propiedad del recurso. En lenguajes gestionados (Java, C#) estos conceptos aparecen como referencias que el recolector de basura supervisa, pero los principios de acceso siguen siendo los mismos.
El tiempo de lectura depende de la distancia entre la CPU y los datos. Cuando un algoritmo recorre una estructura contigua explota la localidad de referencia: la caché carga líneas completas y las siguientes lecturas son casi instantáneas. En cambio, saltar entre nodos dispersos puede obligar a traer cada dirección desde la RAM, lo que multiplica la latencia.
Una estructura estática reserva todo su espacio al inicio del programa. Es ideal cuando conocemos el tamaño máximo y queremos evitar sorpresas en ejecución. Las estructuras dinámicas solicitan memoria a demanda (con new, malloc o asignadores personalizados) y se amplían o contraen según los datos reales.
Incluso cuando una estructura ofrece buenas complejidades teóricas, la implementación concreta puede pagar peajes silenciosos. La fragmentación de memoria reduce la cantidad de bloques grandes disponibles y obliga al sistema operativo a mantener mapas extensos de espacios libres.
Las realocaciones también son costosas: copiar miles de elementos para duplicar la capacidad de un vector implica tiempo lineal y puede invalidar punteros existentes. Por eso es recomendable exponer funciones como reserve() o parámetros de factor de carga que permitan a los usuarios planificar el crecimiento.
Otros costos disfrazados incluyen inicializaciones perezosas, padding para alinear estructuras y los guardias que agregamos para detectar corrupción de memoria. Todos ellos ocupan bytes y pueden volver impredecible la distancia entre dos nodos, afectando al recolector de basura o a las rutinas de serialización.