Subido por Gerardo Loza

Apuntes- Listas enlazadas

Anuncio
PUNTEROS Y LISTAS ENLAZADAS
DEFINICIÓN DE PUNTERO: Un puntero en el lenguaje C es una variable que almacena la
dirección de memoria de otro objeto (como una variable, función o estructura de datos). En
esencia, un puntero "apunta" a la ubicación en memoria de otro valor en el programa. Los
punteros son una característica poderosa y fundamental en C, ya que permiten la manipulación
directa de la memoria y facilitan la implementación de estructuras de datos y algoritmos
eficientes.
En términos más técnicos, un puntero es una variable que almacena la dirección de memoria
de un objeto en lugar de almacenar su valor real. Para declarar un puntero en C, se utiliza el
tipo de dato seguido por un asterisco (*). Los punteros son especialmente útiles para pasar
argumentos por referencia a funciones, asignar y manipular dinámicamente memoria en el
montón (heap) y trabajar con estructuras de datos complejas como matrices, listas enlazadas y
árboles.
Símbolo * (Asterisco): El símbolo * se utiliza para declarar y trabajar con variables que son
punteros. Cuando se coloca antes del nombre de una variable, se convierte en un puntero que
almacenará una dirección de memoria en lugar de un valor directo.
Símbolo & (Ampersand): El símbolo & se utiliza para obtener la dirección de memoria de una
variable existente. Se coloca antes del nombre de la variable para obtener su dirección, que
luego puede ser asignada a un puntero.
Con punteros, es posible asignar memoria dinámicamente y crear arreglos de tamaño variable
según las necesidades del programa. Para ello será necesario que como programadores
"manejemos la memoria" y en C eso se logra usando 2 funcionalidades: Pedir Memoria
(malloc) y Liberar Memoria (free).
¿Qué es un NULL?
En C, NULL es un valor especial que se utiliza para representar un puntero que no apunta a
ninguna dirección de memoria válida. En otras palabras, NULL se usa para indicar que un
puntero no está apuntando a ningún objeto o variable en particular.
Cuando se declara una variable de tipo puntero y no se le asigna una dirección de memoria
válida, el puntero automáticamente toma el valor NULL. Esto es de gran utilidad para validar la
"VALIDEZ" del puntero en nuestras soluciones algorítmicas.
PREGUNTAS
¿Cómo inicializamos un puntero?
Inicialización a NULL: La forma más común de inicializar un puntero es asignándole el valor
NULL, que indica que el puntero no apunta a ninguna dirección de memoria en ese momento.
Esto es útil para evitar acceder a direcciones de memoria no deseadas antes de asignarles un
valor válido.
Inicialización a una dirección de memoria existente: Puedes inicializar un puntero para que
apunte directamente a una variable existente o a una dirección de memoria conocida. Por
ejemplo:
Asignación dinámica de memoria: Puedes usar la función malloc para asignar memoria
dinámicamente en tiempo de ejecución y luego asignar la dirección de memoria recién
asignada al puntero. Recuerda que cuando uses malloc, deberás liberar la memoria utilizando
free cuando ya no la necesites.
Recuerda que, en todos los casos, el tipo de dato al que apunta el puntero debe coincidir con el
tipo de dato de la variable o la memoria que está apuntando.
¿Cuánta memoria ocupa un puntero?
La cantidad de memoria que ocupa un puntero en C depende de la arquitectura y del
compilador que estés utilizando. En la mayoría de las arquitecturas modernas, el tamaño de un
puntero es generalmente igual al tamaño de una dirección de memoria, ya que un puntero
almacena la dirección de memoria a la que apunta. Aquí hay algunas pautas generales:
En arquitecturas de 32 bits: Los punteros generalmente ocupan 4 bytes (32 bits) de memoria,
ya que las direcciones de memoria suelen ser representadas por números de 32 bits.
En arquitecturas de 64 bits: Los punteros generalmente ocupan 8 bytes (64 bits) de memoria,
ya que las direcciones de memoria son representadas por números de 64 bits en estas
arquitecturas.
En el caso de mi pc o Zinjal ocupa 4 bytes.
En C, el tamaño de un puntero no depende de lo que está apuntando. El tamaño de un puntero
está determinado por la arquitectura y el compilador que estás utilizando, y generalmente es
constante independientemente del tipo de dato al que apunte.
Por ejemplo, en la mayoría de las arquitecturas modernas de 32 bits, un puntero ocupará 4
bytes, ya sea que esté apuntando a un entero, un carácter, una estructura o cualquier otro tipo
de dato. De manera similar, en arquitecturas de 64 bits, un puntero ocupará 8 bytes,
independientemente del tipo de dato al que apunte.
La razón de esto es que un puntero simplemente almacena una dirección de memoria, que es
un valor que indica la ubicación en la memoria del objeto al que apunta. El tipo de dato al que
apunta el puntero se utiliza para interpretar correctamente los bytes en esa dirección, pero no
afecta el tamaño del puntero en sí.
¿Punteros a estructuras de datos?
Los punteros a estructuras de datos son una característica poderosa en C (y otros lenguajes
relacionados) que te permite trabajar con estructuras de manera más eficiente y flexible. Un
puntero a una estructura simplemente almacena la dirección de memoria donde se encuentra
la estructura, lo que te permite acceder y manipular los campos de la estructura de manera
indirecta.
Declaración de un puntero a estructura:
1. Puedes declarar un puntero a una estructura de la siguiente manera:
2. Asignación de dirección de memoria: Para que el puntero apunte a una estructura existente,
debes asignarle la dirección de memoria de esa estructura:
3. Acceso a los campos de la estructura: Puedes acceder a los campos de la estructura a través
del puntero utilizando el operador de acceso a miembro ->:
4. Asignación dinámica de memoria: Puedes asignar dinámicamente memoria para una
estructura y luego asignar la dirección a un puntero:
5. Paso de estructuras por referencia: Usar un puntero a una estructura puede ser más
eficiente que pasar estructuras por valor en funciones, especialmente para estructuras
grandes, ya que solo se pasa la dirección de memoria en lugar de copiar todo el contenido.
6. Uso de punteros a estructuras en arreglos: Los punteros a estructuras también se pueden
usar en arreglos, lo que te permite trabajar con múltiples estructuras de manera más dinámica.
En resumen, los punteros a estructuras te brindan la capacidad de trabajar con datos
estructurados de manera más flexible y eficiente, ya que puedes acceder y manipular los
campos de las estructuras mediante punteros. Sin embargo, también requieren una
comprensión cuidadosa de la administración de memoria para evitar problemas como fugas de
memoria o accesos no válidos.
¿Todos los lenguajes de programación usan punteros?
No, no todos los lenguajes de programación utilizan punteros como una característica
fundamental. Los punteros son más comunes en lenguajes de programación de bajo nivel y
lenguajes que ofrecen un mayor control sobre la memoria y los recursos del sistema. Algunos
lenguajes de programación populares que utilizan punteros incluyen C, C++, y Rust.
Por otro lado, muchos lenguajes de programación de alto nivel, como Python, Java, C#, Ruby y
otros, intentan abstractizar la gestión de memoria y ofrecen estructuras de datos más seguras y
menos propensas a errores, eliminando en gran medida la necesidad de trabajar directamente
con punteros. Estos lenguajes suelen proporcionar mecanismos más automáticos para la
administración de memoria, como la recolección de basura.
La decisión de incluir o no punteros en un lenguaje de programación depende de varios
factores, como el nivel de abstracción que el lenguaje pretende ofrecer, el enfoque en la
seguridad y la facilidad de uso, así como el dominio de aplicación para el que está diseñado el
lenguaje.
DEFINICIÓN DE LISTA ENLAZADA: Una lista enlazada es una estructura de datos DINÁMICA
utilizada en programación para almacenar y organizar una colección de elementos de manera
secuencial. A diferencia de un arreglo estático, donde los elementos se almacenan en
ubicaciones de memoria contiguas, en una lista enlazada, cada elemento (llamado nodo)
contiene un valor y una referencia (o puntero) al siguiente nodo en la secuencia.
La principal característica de una lista enlazada es que los nodos no necesitan estar
almacenados en ubicaciones de memoria contiguas, lo que permite la inserción y eliminación
eficiente de elementos en cualquier posición de la lista sin requerir cambios significativos en la
estructura de datos.
Existen varios tipos de listas enlazadas, entre los que se incluyen:
1. Lista enlazada simple: Cada nodo contiene un valor y un puntero al siguiente nodo en la
secuencia.
2. Lista enlazada doble: Cada nodo contiene un valor, un puntero al siguiente nodo y un
puntero al nodo anterior en la secuencia, lo que permite la navegación en ambas direcciones.
3. Lista enlazada circular: Similar a la lista enlazada simple o doble, pero el último nodo apunta
al primer nodo (en el caso de una lista circular simple) o el primer y último nodo están
interconectados (en el caso de una lista circular doble).
Las listas enlazadas son útiles en situaciones donde es necesario realizar inserciones y
eliminaciones frecuentes en posiciones arbitrarias de la estructura de datos, ya que estas
operaciones pueden realizarse de manera eficiente ajustando los punteros. Sin embargo,
también tienen algunas desventajas, como un mayor consumo de memoria debido a los
punteros adicionales y una menor eficiencia en el acceso aleatorio en comparación con los
arreglos estáticos.
En resumen, una lista enlazada es una estructura de datos dinámica y flexible que permite la
organización de elementos de manera secuencial mediante nodos que están interconectados a
través de punteros.
Implementaciones posibles:
➤Con arreglos ---> estructura estática.
Ventaja: facilidad de acceso a los elementos.
Desventaja: tamaño acotado.
➤A través de punteros --> estructura dinámica.
Ventaja: Se reserva memoria solo cuando se necesita (agregar un elemento).
Inconveniente: Se deja la gestión de memoria en manos del Sistema Operativo. Puede ser
ineficiente.
Qué características tiene la implementación usando punteros:
1. Estructura de datos Lineal.
2. Todos los elementos de la lista son del mismo tipo.
3. Existe un orden en los elementos, ya que es una estructura lineal, pero los elementos no
están ordenados por su valor sino por la posición en que se han insertado.
4. Para cada elemento existe un anterior y un siguiente, excepto para el primero, que no tiene
anterior, y para el último, que no tiene siguiente.
5. Se puede acceder y eliminar cualquier elemento.
6. Se pueden insertar elementos en cualquier posición.
7. Estructura Auto referenciada.
Para cada una de las operaciones que podemos aplicar sobre una lista considerar los distintos
CASOS (situaciones) que pueden presentarse: ejemplo la lista está vacía, la lista contiene al
menos 1 elemento, la lista sigue un orden, etc.
Lista Enlazada: Una lista enlazada es una estructura de datos fundamental en la programación
que se utiliza para almacenar una colección de elementos de manera ordenada. A diferencia de
un arreglo estático, donde los elementos están almacenados en ubicaciones contiguas de
memoria, en una lista enlazada, los elementos se almacenan en nodos separados, y cada nodo
contiene el dato y una referencia (enlace) al siguiente nodo en la lista. Esto permite una
flexibilidad dinámica para agregar y eliminar elementos en cualquier posición de la lista sin
tener que reorganizar toda la estructura.
CONCEPTOS CLAVE
1. Nodo: Es la unidad básica de una lista enlazada. Cada nodo contiene dos partes: el dato que
se desea almacenar y una referencia (puntero) al siguiente nodo en la lista.
2. Enlace (Puntero): Es la referencia que un nodo tiene para apuntar al siguiente nodo en la
lista. En la última posición de la lista, el enlace suele ser nulo (null), indicando que no hay más
nodos después.
3. Cabeza (Head): Es el primer nodo de la lista. Sirve como punto de partida para acceder a los
demás nodos en la lista.
4. Inserción: Agregar un nuevo nodo a la lista enlazada. Puede ser al principio (insertar en la
cabeza), en el medio o al final de la lista.
5. Eliminación: Quitar un nodo existente de la lista enlazada. Similar a la inserción, puede ser
en cualquier posición.
6. Recorrido: Proceso de visitar todos los nodos de la lista enlazada para realizar operaciones
en ellos, como imprimir sus datos o hacer algún cálculo.
7. Lista enlazada simple: Cada nodo solo tiene un enlace que apunta al siguiente nodo en la
lista.
8. Lista enlazada doble: Cada nodo tiene dos enlaces: uno que apunta al siguiente nodo y otro
que apunta al nodo anterior en la lista. Esto permite recorridos en ambas direcciones.
9. Lista circular: La última posición de la lista enlazada apunta al primer nodo, formando un
bucle cerrado.
10. Tiempo de acceso: A diferencia de los arreglos, donde se puede acceder a cualquier
elemento en tiempo constante, en una lista enlazada, el acceso a un elemento en una posición
específica puede requerir tiempo lineal (O(n)), ya que es necesario recorrer los nodos hasta
llegar a la posición deseada.
11. Eficiencia: Las listas enlazadas son eficientes para la inserción y eliminación en posiciones
arbitrarias, pero pueden ser menos eficientes en el acceso aleatorio comparado con los
arreglos.
12. Memoria: Aunque las listas enlazadas permiten una asignación flexible de memoria, cada
nodo requiere espacio adicional para almacenar las referencias, lo que puede resultar en un
uso de memoria mayor que los arreglos en ciertos casos.
OPERACIONES SOBRE LISTAS ENLAZADAS
Se logran mediante funciones que hay que desarrollar.
Las operaciones son:
AÑADIR:
INSERTAR:
BORRAR:
BUSCAR ELEMENTOS:
Cada vez que se necesita crear un nuevo nodo se utiliza malloc.
f
DEFINICION DE PILA: Una estructura de datos de tipo pila, también conocida como "stack" en
inglés, es una colección de elementos en la que la inserción y eliminación de elementos siguen
un principio conocido como "último en entrar, primero en salir" (LIFO, por sus siglas en inglés),
lo que significa que el último elemento que se agrega a la pila es el primero en ser eliminado.
En otras palabras, los elementos se apilan uno encima del otro, como si fueran una pila de
platos.
Imagina una pila de platos en la vida real: cuando agregas un nuevo plato, lo colocas en la parte
superior de la pila. Cuando necesitas tomar un plato, tomas el que está en la parte superior,
que es el último que colocaste.
En una estructura de datos de tipo pila, generalmente tienes dos operaciones principales:
1. Push (Empujar): Agregar un elemento a la parte superior de la pila. En este proceso, se
coloca el nuevo elemento encima de los elementos existentes.
2. Pop (Sacar): Eliminar el elemento en la parte superior de la pila. Esto implica quitar el
elemento superior y exponer el siguiente elemento que se encuentra debajo.
Adicionalmente, a menudo se proporciona una operación opcional llamada "Peek" o "Top" que
permite ver el elemento en la parte superior de la pila sin eliminarlo.
Las pilas son utilizadas en muchos contextos, como la administración de llamadas de funciones
en la pila de ejecución de un programa (la pila de llamadas), el manejo de historiales en
navegadores web (la pila de navegación), la evaluación de expresiones matemáticas en
notación polaca inversa, y muchas otras aplicaciones en programación y algoritmos.
En resumen, una estructura de datos de tipo pila es una forma organizada de almacenar y
acceder a elementos donde el último elemento agregado es el primero en ser retirado, y se
utiliza en varios campos para resolver problemas específicos.
Las pilas son estructuras de datos lineales, como los arreglos, ya que los componentes ocupan
lugares sucesivos en la estructura y cada uno de ellos tiene un único sucesor y un único
predecesor, con excepción del último y del primero, respectivamente.
Una pila se define formalmente como una colección de datos a los cuales se puede acceder
mediante un extremo, que se conoce generalmente como tope.
Las pilas no son estructuras fundamentales de datos Para su representación requieren el uso
de otras estructuras de datos, como arreglos o listas.
¿CÓMO ES LA ESTRUCTURA DE LA MEMORIA?
La estructura de la memoria en un sistema de cómputo es una representación organizada de
cómo se almacenan y gestionan los datos en la memoria física y virtual. En sistemas modernos,
la memoria se organiza en varias capas, cada una con un propósito específico. A continuación,
se describe una vista general de las capas principales de la estructura de la memoria:
1. Memoria Física: La memoria física se compone de chips de memoria RAM (Random Access
Memory) en la computadora. Se divide en celdas o ubicaciones de memoria, cada una con su
dirección única. Los datos se almacenan en estas ubicaciones y se accede a ellos mediante
direcciones.
2. Memoria Virtual: La memoria virtual es una abstracción que permite que el sistema
operativo gestione la memoria física y la asignación de memoria a los procesos. Cada proceso
tiene su propio espacio de direcciones virtuales, que es una serie de direcciones que se utilizan
para acceder a los datos en la memoria. El sistema operativo se encarga de asignar, administrar
y traducir estas direcciones virtuales en direcciones físicas de memoria.
3. Segmentos de Memoria: En la arquitectura de muchos sistemas operativos, el espacio de
direcciones virtuales de un proceso se divide en varios segmentos, como el segmento de
código (instrucciones del programa), el segmento de datos (variables globales y estáticas) y el
segmento de pila (almacenamiento para llamadas a funciones y variables locales).
4. Pila (Stack): La pila es una estructura de datos utilizada para gestionar el flujo de ejecución
de un programa. Se utiliza para almacenar información sobre las llamadas a funciones y
variables locales. Los datos se agregan y eliminan de la pila en un orden conocido como "último
en entrar, primero en salir" (LIFO).
5. Montones (Heap): El montón es una región de memoria utilizada para la asignación
dinámica de memoria, como cuando se utiliza la función `malloc` en C. Aquí es donde se
pueden crear y liberar objetos de manera dinámica durante la ejecución del programa. Es
importante liberar manualmente la memoria asignada en el montón para evitar fugas de
memoria.
6. Cachés: Las cachés son niveles de memoria más pequeños y rápidos que almacenan copias
de datos y/o instrucciones que se acceden con frecuencia desde la memoria principal. Las
cachés ayudan a reducir la latencia de acceso a la memoria principal.
7. Registro: Los registros son ubicaciones de memoria extremadamente rápidas y pequeñas
que se encuentran directamente en la CPU. Se utilizan para almacenar datos y realizar
operaciones de manera muy rápida.
Esta es solo una vista general de la estructura de la memoria en un sistema de cómputo. Los
detalles exactos pueden variar según la arquitectura de la CPU, el sistema operativo y otros
factores.
Descargar