M. C. Perla Ma. Saldivar Castillo
La representación por posiciones es una forma secuencial y contigua de almacenamiento en memoria. Cada dato ocupa una posición fija y conocida dentro de una secuencia, y se accede a él mediante índices numéricos. Este tipo de representación se utiliza en estructuras estáticas como arreglos unidimensionales, bidimensionales o multidimensionales.
Según Joyanes Aguilar (2003), este enfoque se basa en la idea de que si se conoce la dirección base y el índice, se puede calcular la posición exacta en memoria donde se encuentra el dato.
Ejemplo
Supongamos que tenemos el siguiente arreglo en C#:
Cada valor ocupa un espacio contiguo en memoria. Si edades[0] comienza en la dirección de memoria 0x100, y cada int ocupa 4 bytes, las posiciones serían:
La dirección de un elemento edades[i] se calcula como:
Dirección(i) = DirecciónBase + i * tamaño_elemento
Este método permite acceso directo, es decir, no se necesita recorrer el arreglo para llegar a un elemento.
Acceso aleatorio inmediato (arreglo(1)).
Sencillez de implementación.
Muy eficiente para búsquedas por índice.
No permite modificar el tamaño una vez creado.
Inserciones y eliminaciones en medio del arreglo son costosas (O(n)).
Puede generar desperdicio de memoria si no se usa por completo.
Representación por ligas
La representación por ligas (también conocida como representación ligada) es una técnica de organización de estructuras de datos que utiliza referencias o apuntadores para establecer relaciones entre elementos, en lugar de depender exclusivamente de la ubicación contigua en memoria como ocurre con los arreglos.
La representación por ligas permite construir estructuras de datos más flexibles al enlazar elementos que pueden estar ubicados en diferentes direcciones de memoria, gracias al uso de punteros o referencias (2004).
Cada elemento de la estructura almacena, además del dato, una o más referencias (llamadas ligas o enlaces) hacia otros elementos. Esta técnica permite representar una amplia gama de estructuras de datos más complejas, como:
Árboles.
Grafos.
Páginas encadenadas.
Conjuntos dispersos.
Tablas hash con encadenamiento.
Cada elemento o nodo contiene no solo los datos, sino también una liga al siguiente (o a varios otros) nodos.
Un nodo típico para una estructura ligada incluye:
Campo de datos (información almacenada).
Campo de liga (una referencia o apuntador al siguiente nodo).
Ejemplo
Este esquema es el patrón base sobre el cual se construyen estructuras más elaboradas, como árboles (con varias ligas por nodo), grafos (ligas múltiples, no necesariamente ordenadas), o listas enlazadas (con una sola o dos ligas).
Las estructuras ligadas permiten:
Asignación dinámica de memoria: No es necesario reservar grandes bloques contiguos como en los arreglos.
Inserción y eliminación eficientes: No requieren desplazar elementos.
Flexibilidad estructural: Se pueden construir estructuras no lineales como árboles y grafos.
La representación por ligas es fundamental para la eficiencia en estructuras donde la manipulación de nodos es frecuente y dinámica, como en parsers, sistemas de archivos y motores de juegos.
Comparación con la representación por posiciones
Algunos escenarios donde se prefiere representación por ligas:
Sistemas que requieren estructuras jerárquicas (exploradores de archivos).
Interpretación de expresiones matemáticas con árboles.
Programación de inteligencia artificial con grafos de conocimiento.
Modelado de relaciones entre objetos (por ejemplo, relaciones padre-hijo o amigo-amigo).
Muchas estructuras de datos avanzadas (como árboles balanceados o grafos dirigidos) no serían posibles sin una representación ligada.
Aunque la lista enlazada es el caso más comúnmente usado como ejemplo de representación por ligas, no es la única aplicación. Se considera como el punto de partida para comprender cómo los nodos se pueden relacionar entre sí, pero estructuras más complejas, como árboles binarios o grafos, se construyen sobre esta misma lógica, pero ampliada a múltiples ligas por nodo.
Por tanto, es esencial entender la idea general de "enlazar elementos" usando referencias o punteros, sin limitarse al ejemplo clásico de una lista.
Concepto apuntador
Un apuntador es una variable que almacena una dirección de memoria. No guarda un valor como un número o texto, sino que "apunta" a la ubicación en memoria donde está almacenado ese valor u objeto.
Es como tener un mapa con la dirección de una casa:
La dirección es el apuntador.
La casa es la variable u objeto real.
Un apuntador es una variable que apunta a otra, permitiendo manipular el contenido de esa variable indirectamente, a través de su dirección de memoria.
Acceder a una variable indirectamente (por su dirección)
Modificar el valor al que apunta
Compartir un valor entre funciones sin copiarlo
Construir estructuras como listas, árboles, grafos, etc.
Representación visual
Entonces:
p está en 0x200, pero apunta a 0x100
*p → se va a 0x100 y ve el valor 10
Ejemplo en C++:
Salida esperada:
Sí, pero están restringidos a código unsafe, por seguridad. Sin embargo, C# trabaja con referencias, que son similares a apuntadores, pero sin necesidad de manipular direcciones directamente.
Ejemplo:
Esto pasa porque p1 y p2 apuntan al mismo objeto en memoria.
Diferencias clave entre apuntadores y referencias
Memoria Dinámica
La memoria dinámica es la porción de memoria que se asigna durante la ejecución del programa, en contraste con la memoria estática, que se reserva en tiempo de compilación. Este tipo de memoria permite crear estructuras de datos flexibles y escalables, como listas, árboles o grafos, que no necesitan conocer su tamaño con anticipación.
La memoria dinámica es fundamental para implementar estructuras que deben crecer o reducirse de forma eficiente, permitiendo que el programa responda a necesidades cambiantes en tiempo real.
Asignación estática: El tamaño y espacio de las variables se conoce en tiempo de compilación (ej. arreglos).
Asignación dinámica: La memoria se solicita con instrucciones específicas durante la ejecución del programa.
Liberación: Es el proceso de devolver la memoria al sistema una vez que ya no se necesita.
Para evitar pérdida de rendimiento y fallos, el programador debe ser consciente del uso y liberación de memoria dinámica, aunque en lenguajes modernos esta se gestiona automáticamente (como en C#).
Porque permite:
Crear estructuras de tamaño variable (listas, colas, árboles…).
Ahorrar memoria al reservar solo lo que se necesita.
Modificar estructuras fácilmente durante la ejecución (agregar, quitar, reorganizar elementos).
Comparación entre memoria estática y dinámica
Ejemplo conceptual en C++
Memoria dinámica en C#
En C# y .NET, la memoria dinámica se gestiona automáticamente mediante el Garbage Collector (recolector de basura), que libera la memoria cuando un objeto ya no se usa.
Cada vez que se crea un nodo, se solicita memoria nueva automáticamente. El recolector de basura libera la memoria cuando ya no hay referencias apuntando a ese nodo.
La programación moderna debe aprovechar los beneficios de la memoria dinámica para permitir estructuras adaptables, sin perder de vista el impacto que tienen en el rendimiento del sistema y en la complejidad del código.
La memoria dinámica permite el crecimiento flexible de estructuras en tiempo de ejecución.
Es imprescindible para representar estructuras de datos complejas como listas, árboles y grafos.
Aunque C# simplifica su uso con el Garbage Collector, comprender su funcionamiento ayuda al programador a optimizar el uso de memoria y evitar errores sutiles.