Download - Analisis de Sistemas Computarizados (Libro)

Transcript
Page 1: Analisis de Sistemas Computarizados (Libro)

UNIDAD 1: ESTRUCTURAS DE DATOS

Page 2: Analisis de Sistemas Computarizados (Libro)

1.1. Definición

En la práctica, la mayor parte de información útil no aparece aislada en forma de datos simples, sino que lo hace de forma organizada y estructurada. Los diccionarios, guías, enciclopedias, etc., son colecciones de datos que serían inútiles si no estuvieran organizadas de acuerdo con unas determinadas reglas. Además, tener estructurada la información supone ventajas adicionales, al facilitar el acceso y el manejo de los datos. Por ello parece razonable desarrollar la idea de la agrupación de datos, que tengan un cierto tipo de estructura y organización interna.

La selección de una estructura de datos frente a otra, a la hora de programar es una decisión importante, ya que ello influye decisivamente en el algoritmo que vaya a usarse para resolver un determinado problema. De hecho, se trata de dar una idea, acerca de los pros y contras de cada una de ellas con el propósito final de justificar la citada ecuación de:

PROGRAMACIÓN = ESTRUCTURAS DE DATOS + ALGORITMOS

Una estructura de datos es, a grandes rasgos, una colección de datos (normalmente de tipo simple) que se caracterizan por su organización y las operaciones que se definen en ellos. Por tanto, una estructura de datos vendrá caracterizada tanto por unas ciertas relaciones entre los datos que la constituyen (por ejemplo el orden de las componentes de un vector de números reales), como por las operaciones posibles en ella. Esto supone que podamos expresar formalmente, mediante un conjunto de reglas, las relaciones y operaciones posibles (tales como insertar nuevos elementos o como eliminar los ya existentes).

En programación, el término estructura de datos se utiliza para referirse a una forma de organizar un conjunto de datos que se relacionan entre sí, sean estos simples o estructurados, con el objetivo de facilitar su manipulación y de operarlo como un todo.

En otras palabras es una forma particular de organizar datos en una computadora para que pueda ser utilizado de manera eficiente.

En un lenguaje de programación, un tipo de dato está definido por el conjunto de valores que representa y por el conjunto de operaciones que se pueden realizar con dicho tipo de dato. Por ejemplo, el tipo de dato entero puede representar números en el rango de -2^31 a 2^31-1 y cuenta con operaciones como suma, resta, multiplicación, división, etc.

Por otro lado, podemos decir que en la solución de un problema a ser procesado por una computadora podemos encontrar dos grandes tipos de datos: datos simples y datos estructurados. Los datos simples son aquellos que no están compuestos de otras estructuras, que no sean los bits, al ser representados por la computadora en forma directa, ocupan solo una casilla de memoria, sin embargo existen unas operaciones propias de cada tipo que en cierta manera los caracterizan. Debido a esto, una variable de un tipo de dato simple hace referencia a un único valor a la vez. Ejemplo de estos tipos de datos son los enteros, reales, caracteres y booleanos.

Así mismo, los datos estructurados se caracterizan porque su definición está compuesta de otros tipos de datos simples, así como de otros datos estructurados. En este caso, un nombre (identificador de la variable estructurada) hace referencia no solo a una casilla de memoria, sino a un grupo de casillas. Por ejemplo: una cadena está formada por una sucesión de caracteres; una matriz por datos simples organizados en forma de filas y columnas; y un archivo está constituido por registros, éstos por campos, que se componen, a su vez, de datos de tipo simple. Por un abuso de lenguaje, se tiende a hacer sinónimos, el dato estructurado con su estructura correspondiente, aunque ello evidentemente no es así.

Para muchos propósitos es conveniente tratar una estructura de datos como si fuera un objeto individual y afortunadamente, muchos lenguajes de programación permiten manipular estructuras completas como si se trataran de datos individuales, de forma que los datos estructurados y simples se consideran a menudo por el programador de la misma manera. Así a partir de ahora un dato puede ser tanto un entero como una matriz, por nombrar dos ejemplos.

Las estructuras de datos son necesarias tanto en la memoria principal como en la secundaria, de forma que primero nos centraremos en las correspondientes a la memoria principal, y posteriormente nos centraremos en las estructuras más adecuadas para el almacenamiento masivo de datos.

Page 3: Analisis de Sistemas Computarizados (Libro)

1.2. Clasificación

Una clasificación de estructuras de datos es según dónde residan: Internas y externas. Si una estructura de datos reside en la memoria central del computador se denomina estructura de datos interna. Recíprocamente, si reside en un soporte externo, se denomina estructura de datos externa.

Las estructuras de datos internas pueden ser de dos tipos:

Estructuras de Datos Estáticas. Estructuras de Datos Dinámicas.

Estructuras de Datos Estáticas

Tienen un número fijo de elementos que queda determinado desde la declaración de la estructura en el comienzo del programa. Ejemplo los arreglos. Las estructuras de datos estáticas, presentan dos inconvenientes:

La reorganización de sus elementos, si ésta implica mucho movimiento puede ser muy costosa. Ejemplo: insertar un dato en un arreglo ordenado.

Son estructuras de datos estáticas, es decir, el tamaño ocupado en memoria es fijo, el arreglo podría llenarse y si se crea un arreglo de tamaño grande se estaría desperdiciando memoria.

Estructuras de Datos Dinámicas

Las estructuras de datos dinámicas nos permiten lograr un importante objetivo de la programación orientada a objetos: la reutilización de objetos. Al contrario de un arreglo, que contiene espacio para almacenar un número fijo de elementos, una estructura dinámica de datos se amplía y contrae durante la ejecución del programa.

A su vez, este tipo de estructuras se pueden dividir en dos grandes grupos según la forma en la cual se ordenan sus elementos.

Lineales. No lineales.

Estructuras de Datos Lineales

En este tipo de estructuras los elementos se encuentran ubicados secuencialmente. Al ser dinámica, su composición varía a lo largo de la ejecución del programa que lo utiliza a través de operaciones de inserción y eliminación. Dependiendo del tipo de acceso a la secuencia, haremos la siguiente distinción:

Pilas: sólo tienen un único punto de acceso fijo a través del cual se añaden, se eliminan o se consultan elementos. Colas: tienen dos puntos de acceso, uno para añadir y el otro para consultar o eliminar elementos.Listas: podemos acceder (insertar y eliminar) por cualquier lado.

Estructuras de Datos No Lineales

Dentro de las estructuras de datos no lineales tenemos los árboles y grafos. Este tipo de estructuras los datos no se encuentran ubicados secuencialmente. Permiten resolver problemas computacionales complejos.

Page 4: Analisis de Sistemas Computarizados (Libro)

1.3. Estructuras lineales

Las estructuras lineales de datos se caracterizan porque sus elementos están en secuencia, relacionados en forma lineal, uno luego del otro. Cada elemento de la estructura puede estar conformado por uno o varios sub-elementos o campos que pueden pertenecer a cualquier tipo de dato, pero que normalmente son tipos básicos.

Una estructura lineal de datos está conformada por ninguno, uno o varios elementos que tienen una relación dónde existe un primer elemento, seguido de un segundo elemento y así sucesivamente hasta llegar al último.

PILAS.

Una pila se caracteriza por el hecho que el último elemento en entrar es el primero en salir. En inglés una pila se suele denominar con las siglas LIFO (last in first out). La definición de pila nos indica que todas las operaciones trabajan sobre el mismo extremo de la secuencia. En otras palabras, los elementos se sacan de la estructura en el orden inverso al orden en que se han insertado, ya que el único elemento de la secuencia que se puede obtener es el último (figura 1).

Ejemplos de pilas

En nuestra vida diaria podemos ver este comportamiento muy a menudo. Por ejemplo, si apilamos los platos de una vajilla, únicamente podremos coger el último plato añadido a la pila, porque cualquier intento de coger un plato del medio de la pila (como el plato oscuro de la figura 2) acabará en un destrozo. Otro ejemplo lo tenemos en los juegos de cartas, en los que generalmente robamos las cartas (de una en una) de la parte superior del mazo (figura 2).

En el mundo informático también encontramos pilas, como por ejemplo, en los navegadores web. Cada vez que accedemos a una nueva página, el navegador la añade a una pila de páginas visitadas, de manera que cuando seleccionamos la opción Anterior, el navegador coge la página que se encuentra en la cima de la pila, porque es justamente la última página visitada. Otro ejemplo lo tenemos en los procesadores de textos, en los que los cambios introducidos en el texto también se almacenan en una pila. Cada vez que apretamos la combinación de teclas Ctrl+Z deshacemos el último cambio introducido, mientras que cada vez que apretamos la combinación Ctrl+Y volvemos a añadir a la pila el último cambio deshecho.

Page 5: Analisis de Sistemas Computarizados (Libro)

Operaciones

En la tabla 1 se encuentran las operaciones básicas para trabajar con pilas.

Las operaciones del TAD pila se clasifican en:

Operaciones constructoras: crear, apilar y desapilar.Operaciones consultoras: cima y vacía.

Las operaciones constructoras son las operaciones que modifican el estado de la pila, mientras que las operaciones consultoras son las que consultan el estado de la pila sin modificarla.

El estado de una pila está definido por los elementos que contiene la pila y por el orden en que están almacenados. Todo estado es el resultado de una secuencia de llamadas a operaciones constructoras.

Igualmente, las operaciones constructoras se clasifican en:

Operaciones generadoras: crear y apilar, porque son imprescindibles para conseguir cualquier estado de la pila.Operaciones modificadoras: desapilar, porque modifica el estado de la pila extrayendo el elemento de la cima, pero no es una operación imprescindible para construir una pila.

Dado un estado de la pila, se puede llegar a él a través de diversas secuencias de llamadas a operaciones constructoras, pero de entre todas las secuencias sólo habrá una que esté formada exclusivamente por operaciones generadoras (figura 3).

Observar que la tercera secuencia de llamadas de la figura 3 es la mínima para llegar al estado deseado, ya que no podemos eliminar ninguna de las llamadas que la forman. Por lo tanto, tal como se ha mencionado anteriormente, apilar y crear son las operaciones generadoras.

Page 6: Analisis de Sistemas Computarizados (Libro)

COLAS.

Una cola está caracterizada por el hecho que el primer elemento en entrar es el primero en salir. Las colas se diferencian de las pilas en la extracción de los datos. En inglés, una cola suele denominarse con las siglas FIFO (first in first out).

La definición de cola nos indica que las operaciones trabajan sobre ambos extremos de la secuencia: un extremo para añadir los elementos y el otro para consultarlos o extraerlos. En otras palabras, los elementos se extraen en el mismo orden en que se han insertado previamente, ya que se insertan por el final de la secuencia y se extraen por la cabecera (figura 6).

Ejemplos de colas

Las colas aparecen a menudo en nuestra vida diaria... Sin ir más lejos, podemos afirmar que pasamos una parte de nuestra vida haciendo colas: para comprar la entrada en un cine, para pagar en la caja de un supermercado, para visitarnos por el médico, etc. La idea siempre es la misma: se atiende la primera persona de la cola, que es la que hace más rato que espera, y una vez atendida sale de la cola y la persona siguiente pasa a ser la primera de la cola (figura 7).

Si en el mundo real es habitual ver colas, en el mundo informático todavía lo es más. Cuando el sistema operativo ha de gestionar el acceso a un recurso compartido (procesos que quieren ejecutarse en la CPU, trabajos que se envían a una impresora, descarga de ficheros, etc.), una de las estrategias más utilizadas es organizar las peticiones por medio de colas. Por ejemplo, la figura 8 nos muestra una captura de una cola de impresión en un instante dado. En este caso, la tarea 321 se está imprimiendo porque es la primera en la cola, mientras que la tarea 326 será la última en imprimirse porque ha sido la última en llegar.

Page 7: Analisis de Sistemas Computarizados (Libro)

Operaciones

Dada la representación de una cola, en la tabla 2 definimos las operaciones para trabajar con ella.

Las operaciones de una cola se clasifican según su comportamiento en generadoras, modificadoras y consultoras:

Operaciones constructoras:o Operaciones generadoras: crear y encolar.o Operaciones modificadoras: desencolar.

Operaciones consultoras: cabeza y vacía.

El comportamiento de las operaciones desencolar y cabeza se resume en tres casos:

1) sobre una cola vacía (crear),2) sobre una cola con un único elemento (encolar(crear,e)) y3) sobre una cola con más de un elemento (encolar(c,e), donde c no es una cola vacía).

LISTAS.

Una lista está caracterizada por el hecho de que permite añadir, borrar o consultar cualquier elemento de la secuencia. Es la estructura lineal más flexible, hasta el punto de considerar que las pilas y colas son casos particulares de las listas. Mientras que en las estructuras lineales anteriores hemos visto cómo las operaciones trabajaban solo con los extremos de la secuencia, las listas nos ofrecerán la posibilidad de acceder a cualquier punto de esta.

Ejemplos de listas

También en este caso encontramos listas en nuestra vida cotidiana. Por ejemplo, la lista de la compra. Cuando estamos en el supermercado generalmente eliminamos los artículos a medida que los encontramos en el recorrido que seguimos con el carro, que no tiene porqué coincidir con el orden en que los hemos escrito en nuestra lista (figura 14).

Desde el punto de vista informático también encontramos ejemplos, como los editores de textos. Cuando escribimos un código, en el fondo editamos una lista de palabras dentro de una lista de líneas. Hablamos de listas, porque en cualquier momento nos podemos desplazar sobre cualquier palabra del fichero para modificarla o para insertar nuevas palabras. Hay diferentes modelos de listas, uno de los más habituales es el llamado lista con punto de interés. Esta lista contiene un elemento distinguido que sirve de referencia para aplicar las operaciones. El elemento distinguido es apuntado por el llamado punto de interés, que puede desplazarse.

El punto de interés divide una secuencia en dos fragmentos, que a su vez también son secuencias. Por lo tanto, dada una lista l cualquiera, se puede dividir en una secuencia situada a la izquierda del punto de interés (s) y otra secuencia que va del punto de interés (elemento distinguido, e) hacia la derecha (et). Podemos representar esta unión de dos secuencias para formar la lista l de la manera siguiente: l =< s,et >. La secuencia vacía (es decir, sin ningún elemento) se representará con la letra l (lambda).

Page 8: Analisis de Sistemas Computarizados (Libro)

Operaciones

En la tabla 3 se encuentran las operaciones para trabajar con las listas.

Podemos ver que además de las operaciones típicas para trabajar con secuencias, esta vez se han añadido operaciones para cambiar el elemento distinguido y así poder desplazar el punto de interés: principio y avanzar. En este caso, el conjunto de operaciones constructoras generadoras está formado por crear, insertar y principio. Por tanto, la mínima secuencia de llamadas que necesitaremos para obtener la lista <ho,la> es una combinación de estas operaciones:

{insertar(insertar(principio(insertar(insertar(crear, l), a)), h), o)}

La implementación de las listas con punto de interés se puede hacer de varias maneras:

Implementación secuencial. Los elementos de la lista se almacenan en un vector respetando la norma que los elementos consecutivos ocupan posiciones consecutivas. La representación requiere tres elementos: un vector (A), un indicador del número de elementos almacenados (nelem) y un indicador del punto de interés o elemento distinguido (act).

El indicador nelem nos sirve para controlar que no sobrepasemos el número máximo de elementos (MAX) que puede almacenar el vector, mientras que el indicador act apunta al elemento distinguido de la lista.

Implementación encadenada. Los elementos se almacenan sin seguir la norma anterior, ya que cada elemento del vector guarda la posición en que se halla el siguiente elemento de la lista. Para romper el concepto de secuencial, en que el sucesor de un elemento es el que ocupa la siguiente posición del vector, introducimos a continuación el concepto de encadenamiento, en que cada elemento guarda la posición en que se halla su sucesor.

Como se puede observar en la representación, se almacena para cada posición del vector: el elemento (e) y un indicador de la posición en que se halla el siguiente elemento (suc). Este indicador se denominará, a partir de este momento, encadenamiento.

Page 9: Analisis de Sistemas Computarizados (Libro)

EJERCICIOS DE APRENDIZAJE NO. 1

I. Completa la siguiente tabla en base a las operaciones que se pueden realizar para cada una de las estructuras lineales.

Nombre Pilas Colas Listas

Ope

raci

ones

Crear

II. Describe cada una de las operaciones que se pueden realizar en las listas.

Operación DescripciónCrearInsertarBorrarActualVacíaPrincipioAvanzarFin

III. Coloca en la línea que se encuentra debajo de cada dibujo a que estructura de datos lineal corresponden cada una de las representaciones.

Page 10: Analisis de Sistemas Computarizados (Libro)

1.4. Estructuras no lineales

A las estructuras de datos no lineales se les llama también estructuras de datos multienlazadas en donde cada elemento puede estar enlazado a cualquier otro componente. Se trata de estructuras de datos en las que cada elemento puede tener varios sucesores y/o varios predecesores.

También se pueden definir como aquellas estructuras que ocupan bloques de memoria no continuos/lineales. Para lidiar con el problema de la fragmentación y, sobre todo del crecimiento dinámico. Los bloques deben estar enlazados unos con otros para poder “navegar” por la estructura, es decir, tener acceso a otro(s) dato(s) a partir del actual.

ARBOLES.

Un árbol es una estructura de datos no lineal que organiza sus elementos siguiendo una jerarquía de niveles entre sus elementos (nodos). Cada elemento tiene un único antecesor y puede tener varios sucesores. Existe un único camino entre el primer nodo de la estructura y cualquier otro nodo. Se utilizan para representar todo tipo de jerarquías.

Ejemplos de árboles:

Árbol genealógico TaxonomíasDiagramas de organizaciónÁrbol de directorios en una unidad de discoAplicaciones algorítmicas (ordenación, búsqueda)Compilación (árboles sintácticos, árboles de expresiones)

Terminología

Nodo: los vértices o elementos de un árbol. Enlace/arco/arista: Conexión entre dos nodos consecutivos. Los nodos pueden ser: Nodo raíz: nodo superior de la jerarquía. Nodo terminal u hoja: nodo que no contienen

ningún subárbol. Nodos interiores: nodos con uno o más

subárboles; nodos que no son hojas. Descendientes o hijos: cada uno de los

subárboles de un nodo. Ascendiente, antecesor o padre: nodo de

jerarquía superior a uno dado. Nodos hermanos: nodos del mismo padre. Bosque: colección de árboles. Camino: enlace entre dos nodos. No existe un

camino entre todos los nodos. Rama: camino que termina en una hoja. Grado de un nodo: número de subárboles que

tiene. Nivel de un nodo o longitud del camino:

número de arcos o enlaces que hay desde el nodo raíz hasta un nodo dado. Altura o profundidad de un árbol: número máximo de nodos de una rama; el nivel más alto de un árbol más

uno. Peso de un árbol: número de nodos terminales.

Ejemplo de un bosque.

Page 11: Analisis de Sistemas Computarizados (Libro)

Tipos de árboles.

Un árbol general sería un árbol en el que cada nodo puede tener un número ilimitado de subárboles.Un árbol binario sería un conjunto de 0 o más nodos en el cual existe un nodo raíz y cada uno de los nodos, incluido la raíz podrán tener 0, 1 o dos subárboles:

o Subárbol izquierdo y subárbol derecho.o Cada nodo es como máximo de grado 2.

Tipos de árboles binarios.

Árboles similares: árboles con la misma estructura.Árboles equivalentes: árboles con la misma estructura y contienen la misma información.Árboles completos o árboles perfectos: todos los nodos, excepto las hojas, tienen grado 2. Un árbol binario de nivel n tiene 2n-1 nodos.Árbol equilibrado: un árbol en el que las alturas de los dos subárboles de cada uno de los nodos tiene como máximo una diferencia de una unidad.Árbol degenerado: todos sus nodos sólo tienen un subárbol.

Recorrido de un árbol (Definiciones)

Page 12: Analisis de Sistemas Computarizados (Libro)

Se denomina recorrido de un árbol al proceso que permite acceder de una sola vez a cada uno de los nodos del árbol. Cuando un árbol se recorre, el conjunto completo de nodos se examina.

En ciencias de la computación, el recorrido de árboles refiere al proceso de visitar de una manera sistemática, exactamente una vez, cada nodo en una estructura de datos de árbol (examinando y/o actualizando los datos en los nodos). Tales recorridos están clasificados por el orden en el cual son visitados los nodos. Los siguientes algoritmos son descritos para un árbol binario, pero también pueden ser generalizados a otros árboles.

Recorrido en profundidad

Los algoritmos de recorrido de un árbol binario presentan tres tipos de actividades comunes:

Visitar el nodo raíz. Recorrer el subárbol izquierdo. Recorrer el subárbol derecho.

Estas tres acciones repartidas en diferentes órdenes proporcionan los diferentes recorridos del árbol. Los más frecuentes tienen siempre en común recorrer primero el subárbol izquierdo y luego el subárbol derecho. Los algoritmos anteriores se llaman pre-orden, post-orden, in-orden, y su nombre refleja el momento en que se visita el nodo raíz.

Preorden: (raíz, izquierdo, derecho). Para recorrer un árbol binario no vacío en preorden, hay que realizar las siguientes operaciones recursivamente en cada nodo, comenzando con el nodo de raíz:

1. Visite la raíz2. Atraviese el sub-árbol izquierdo3. Atraviese el sub-árbol derecho

Inorden: (izquierdo, raíz, derecho). Para recorrer un árbol binario no vacío en inorden (simétrico), hay que realizar las siguientes operaciones recursivamente en cada nodo:

1. Atraviese el sub-árbol izquierdo2. Visite la raíz3. Atraviese el sub-árbol derecho

Postorden: (izquierdo, derecho, raíz). Para recorrer un árbol binario no vacío en postorden, hay que realizar las siguientes operaciones recursivamente en cada nodo:

1. Atraviese el sub-árbol izquierdo2. Atraviese el sub-árbol derecho3. Visite la raíz

En general, la diferencia entre preorden, inorden y postorden es cuándo se recorre la raíz. En los tres, se recorre primero el sub-árbol izquierdo y luego el derecho.

En preorden, la raíz se recorre antes que los recorridos de los subárboles izquierdo y derecho. En inorden, la raíz se recorre entre los recorridos de los árboles izquierdo y derecho. En postorden, la raíz se recorre después de los recorridos por el subárbol izquierdo y el derecho.

Preorden (antes), inorden (en medio), postorden (después).

Recorrido en anchura

Los árboles también pueden ser recorridos en orden por nivel (de nivel en nivel), donde visitamos cada nodo en un nivel antes de ir a un nivel inferior. Esto también es llamado recorrido en anchura-primero o recorrido en anchura.

Ejemplo

Page 13: Analisis de Sistemas Computarizados (Libro)

Se tiene el siguiente árbol binario ordenado, realizar los recorridos y obtener sus resultados.

Recorrido de Profundidad

Secuencia de recorrido de preorden: F, B, A, D, C, E, G, I, H (raíz, izquierda, derecha)

Secuencia de recorrido de inorden: A, B, C, D, E, F, G, H, I (izquierda, raíz, derecha); note cómo esto produce una secuencia ordenada

Secuencia de recorrido de postorden: A, C, E, D, B, H, I, G, F (izquierda, derecha, raíz)

Recorrido de Anchura

Secuencia de recorrido de orden por nivel: F, B, G, A, D, I, C, E, H

Page 14: Analisis de Sistemas Computarizados (Libro)

EJERCICIOS DE APRENDIZAJE NO. 2

I. Realiza los recorridos y obtén los elementos que se te solicitan de los siguientes árboles binarios.

Pre-orden (raíz, izquierda, derecha)

In-orden (izquierda, raíz, derecha)

Post-orden (izquierda, derecha, raíz)

Anchura (amplitud o por niveles)

Nodo raíz:Hojas:Hijos del nodo raíz:Ejemplo de una rama:Grado del nodo raíz:Profundidad del árbol:Peso del árbol:Nivel del árbol:

Pre-orden (raíz, izquierda, derecha)

In-orden (izquierda, raíz, derecha)

Post-orden (izquierda, derecha, raíz)

Anchura (amplitud o por niveles)

Nodo raíz:Hojas:Hijos del nodo raíz:Ejemplo de una rama:Grado del nodo raíz:Profundidad del árbol:Peso del árbol:Nivel del árbol:

Page 15: Analisis de Sistemas Computarizados (Libro)

Pre-orden (raíz, izquierda, derecha)In-orden (izquierda, raíz, derecha)Post-orden (izquierda, derecha, raíz)Anchura (amplitud o por niveles)

Nodo raíz:Hojas:Hijos del nodo raíz:Ejemplo de una rama:Grado del nodo raíz:Profundidad del árbol:Peso del árbol:Nivel del árbol:

Pre-orden (raíz, izquierda, derecha)In-orden (izquierda, raíz, derecha)Post-orden (izquierda, derecha, raíz)

Anchura (amplitud o por niveles)

Nodo raíz:Hojas:Hijos del nodo raíz:Ejemplo de una rama:Grado del nodo raíz:Profundidad del árbol:Peso del árbol:Nivel del árbol:

Pre-orden (raíz, izquierda, derecha)In-orden (izquierda, raíz, derecha)Post-orden (izquierda, derecha, raíz)Anchura (amplitud o por niveles)

Nodo raíz:Hojas:Hijos del nodo raíz:Ejemplo de una rama:Grado del nodo raíz:Profundidad del árbol:Peso del árbol:Nivel del árbol:

Page 16: Analisis de Sistemas Computarizados (Libro)

GRAFOS.

Un grafo (del griego grafos: dibujo, imagen) es un conjunto de objetos llamados vértices o nodos unidos por enlaces llamados aristas o arcos, que permiten representar relaciones binarias entre elementos de un conjunto.

Típicamente, un grafo se representa gráficamente como un conjunto de puntos (vértices o nodos) unidos por líneas (aristas). Desde un punto de vista práctico, los grafos permiten estudiar las interrelaciones entre unidades que interactúan unas con otras. Por ejemplo, una red de computadoras puede representarse y estudiarse mediante un grafo, en el cual los vértices representan terminales y las aristas representan conexiones (las cuales, a su vez, pueden ser cables o conexiones inalámbricas).

Prácticamente cualquier problema puede representarse mediante un grafo, y su estudio trasciende a las diversas áreas de las ciencias exactas y las ciencias sociales.

Un grafo G es un par ordenado G=(V,E), donde: V es un conjunto de vértices o nodos, y E es un conjunto de aristas o arcos, que relacionan estos nodos.

Normalmente V suele ser finito. Muchos resultados importantes sobre grafos no son aplicables para grafos infinitos. Se llama orden del grafo G a su número de vértices, |V|. El grado de un vértice o nodo es igual al número de arcos que lo tienen como extremo. Un bucle es una arista que relaciona al mismo nodo; es decir, una arista donde el nodo inicial y el nodo final coinciden. Dos o más aristas son paralelas si relacionan el mismo par de vértices.

Grafo no dirigidoUn grafo no dirigido o grafo propiamente dicho es un grafo G = (V, E) donde:es un conjunto de pares no ordenados de elementos de V.

Un par no ordenado es un conjunto de la forma , de manera que . Para los grafos, estos conjuntos pertenecen al conjunto potencia de V, denotado P(V), y son de cardinalidad.

Grafo dirigidoUn grafo dirigido o digrafo es un grafo G = (V, E) donde:

, es un conjunto de pares ordenados de elementos de V.

Dada una arista (a,b), a es su nodo inicial y b su nodo final.Por definición, los grafos dirigidos no contienen bucles.

Grafo mixtoUn grafo mixto es aquel que se define con la capacidad de poder contener aristas dirigidas y no dirigidas. Tanto los grafos dirigidos como los no dirigidos son casos particulares de este.

Page 17: Analisis de Sistemas Computarizados (Libro)

Propiedades

Adyacencia: dos aristas son adyacentes si tienen un vértice en común, y dos vértices son adyacentes si una arista los une.Incidencia: una arista es incidente a un vértice si ésta lo une a otro.Ponderación: corresponde a una función que a cada arista le asocia un valor (costo, peso, longitud, etc.), para aumentar la expresividad del modelo. Esto se usa mucho para problemas de optimización, como el del vendedor viajero o del camino más corto.Etiquetado: distinción que se hace a los vértices y/o aristas mediante una marca que los hace unívocamente distinguibles del resto.

Ejemplos

La imagen es una representación del siguiente grafo: V:={1,2,3,4,5,6} E:={{1,2},{1,5},{2,3},{2,5},{3,4},{4,5},{4,6}}

En ciencias de la computación los grafos dirigidos son usados para representar máquinas de estado finito y algunas otras estructuras discretas.

Grafos particulares

Existen grafos que poseen propiedades destacables. Algunos ejemplos básicos son: Grafo nulo: aquel que no tiene vértices ni aristas. Nótese que algunas personas exigen que el conjunto de

vértices no sea vacío en la definición de grafo. Grafo vacío: aquel que no tiene aristas. Grafo trivial: aquel que tiene un vértice y ninguna arista. Grafo simple: aquel que no posee bucles ni aristas paralelas. Consultar variantes en esta definición. Multigrafo (o pseudografo): G es multigrafo si y solo si no es simple. Consultar variantes en esta definición. Grafo completo: grafo simple en el que cada par de vértices están unidos por una arista, es decir, contiene

todas las posibles aristas. Grafo bipartito: sea (W, X) una partición del conjunto de vértices V, es aquel donde cada arista tiene un vértice

en W y otro en X. Grafo bipartito completo: sea (W, X) una partición del conjunto de vértices V, es aquel donde cada vértice en W

es adyacente sólo a cada vértice en X, y viceversa. Grafo plano: aquel que puede ser dibujado en el plano cartesiano sin cruce de aristas. Grafo rueda: grafo con n vértices que se forma conectando un único vértice a todos los vértices de un ciclo-(n-

1). Grafo perfecto: aquel que el número cromático de cada subgrafo inducido es igual al tamaño del mayor clique

de ese subgrafo.

Una generalización de los grafos son los llamados hipergrafos. Los hipergrafos es una generalización de un grafo, cuyas aristas aquí se llaman hiperaristas, y pueden relacionar a cualquier cantidad de vértices, en lugar de sólo un máximo de dos como en el caso particular.

Page 18: Analisis de Sistemas Computarizados (Libro)

1.5. Estructuras estáticas

Son aquellas en las que el tamaño ocupado en memoria se define antes de que el programa se ejecute y no puede modificarse dicho tamaño durante la ejecución del programa.

Estas estructuras están implementadas en casi todos los lenguajes. Su principal característica es que ocupan solo una casilla de memoria, por lo tanto una variable simple  hace referencia a un único valor a la vez, dentro de este grupo  de datos se encuentra:

a) Enterosb) Realesc) Caracteresd) Boléanose) Enumeradosf) Subrangos

Nota: Los últimos no existen en algunos lenguajes de programación.

Clasificación de las estructuras de datos estáticas:

1.- Simples o primitivas a) Boolean b) Char c) Integer d) Real 2.- Compuestas a) Arreglos b) Conjuntos c) Strings d) Registros e) Archivos

Page 19: Analisis de Sistemas Computarizados (Libro)

1.6. Estructuras dinámicas

No tienen las limitaciones o restricciones en el tamaño de memoria ocupada que son propias de las estructuras estáticas.

Mediante el uso de un tipo de datos especifico, denominado puntero, es posible construir estructuras de datos dinámicas que no son soportadas por la mayoría de los lenguajes, pero que en aquellos que si tienen estas características ofrecen soluciones eficaces y efectivas en la solución de problemas complejos.

Se caracteriza por el hecho de que con un nombre se hace referencia a un grupo de casillas de memoria. Es decir un dato estructurado tiene varios componentes.

Clasificación de las estructuras de datos dinámicas:

1.- Lineales a) Pila b) Cola c) Lista 2.- No lineales a) Árboles b) Grafos

Page 20: Analisis de Sistemas Computarizados (Libro)

UNIDAD 2: ESTRUCTURAS LINEALES DE DATOS

Page 21: Analisis de Sistemas Computarizados (Libro)

2.2.1. Introducción a los tipos de datos abstractos

Un tipo de datos abstractos (TDA) es un tipo de dato definido por el programador que se puede manipular de un modo similar a los tipos de datos definidos por el sistema. Está formado por un conjunto válido de elementos y un número de operaciones primitivas que se pueden realizar sobre ellos.

Ejemplo:

- Definición del tipo

Numero racional: Conjunto de pares de elementos (a,b) de tipo entero, con b<>0.

- Operaciones:

CrearRacional: a, b = (a,b)Suma: (a,b) + (c,d) = (a*d+b*c , b*d)Resta: (a,b) - (c,d) = (a*d-b*c , b*d)Producto: (a,b) * (c,d) = (a*c , b*d)División: (a,b) / (c,d) = (a*d , b*c)Numerador: (a,b) = aDenominador: (a,b) = bValorReal: (a,b) = a/bMCD: (a,b) ...Potencia: (a,b)^c = (a^c , b^c)Simplifica: (a,b) = ( a/mcd(a,b) , b/mcd(a,b) )

Una vez definido se podrán declarar variables de ese tipo y operar con ellas utilizando las operaciones que aporta el tipo.

Ejemplo: TRacional r1,r2, rsuma;CrearRacional(4,7, &r1);CrearRacional(5,8,&r2);Suma(r1, r2, &rsuma);printf(“El valor real es %f”, ValorReal(rsuma) );

Un TDA es el elemento básico de la abstracción de datos. Su desarrollo es independiente del lenguaje de programación utilizado, aunque este puede aportar mecanismos que faciliten su realización. Debe verse como una caja negra.

En un TDA existen dos elementos diferenciados:

La Interfaz de utilización La representación

A la hora de utilizar el TDA, la representación debe permanecer oculta. Solo podremos utilizar las operaciones del tipo para trabajar con sus elementos.

Interfaz

Para construir un tipo abstracto debemos (Definición):

1. Exponer una definición del tipo.2. Definir las operaciones (funciones y procedimientos) que permitan operar con instancias de ese tipo.3. Ocultar la representación de los elementos del tipo de modo que sólo se pueda actuar sobre ellos con las

operaciones proporcionadas.

Page 22: Analisis de Sistemas Computarizados (Libro)

4. Poder hacer instancias múltiples del tipo.

Tipos básicos de operaciones en un TDA (Operaciones):

Constructores: Crean una nueva instancia del tipo. Transformación: Cambian el valor de uno o más elementos de una instancia del tipo. Observación: Nos permiten observar el valor de uno o varios elementos de una instancia sin modificarlos. Iteradores: Nos permiten procesar todos los componentes en un TDA de forma secuencial.

Implementación

Una vez definido el TAD se escoge una representación interna utilizando los tipos que proporciona el lenguaje y/o otros TAD ya definidos previamente.

La representación deberá ocultarse utilizando los mecanismos que nos proporcione el lenguaje. Ocultamiento de Información.

Normalmente la implementación del tipo se realiza en un módulo aparte que será enlazado al programa principal. Se necesitará un fichero cabecera que contenga la definición de las operaciones y la declaración del tipo

(representación). Con esta información se podrán definir elementos del tipo y acceder a sus operaciones.

Ejemplo: Fichero cabecera

struct _TRacional { int a,b};typedef struct _TRacional TRacional;void CreaRacional (int a, int b, TRacional *r );void SumaRacional(TRacional r1, TRacional r2, TRacional *rsum);

Page 23: Analisis de Sistemas Computarizados (Libro)

2.2. Estructuras de datos dinámicas

Las estructuras dinámicas de datos son estructuras que crecen a medida que ejecuta un programa. Una estructura dinámica de datos es una colección de elementos – llamadas nodos - que son normalmente registros. Al contrario de un arreglo que contiene espacio para almacenar un número fijo de elementos, una estructura dinámica de datos se amplía y contrae durante la ejecución del programa, basada en los registros de almacenamiento de datos del programa.

Las estructuras dinámicas de datos son de gran utilidad para almacenar datos del mundo real, que están cambiando constantemente.

Un ejemplo típico, es la lista de pasajeros de una línea aérea. Si esta lista se mantuviera en orden alfabético en un arreglo, sería necesario hacer espacio para insertar un nuevo pasajero por orden alfabético. Esto requiere utilizar un ciclo para copiar los datos del registro de cada pasajero al siguiente elemento del arreglo. Si en su lugar se utilizará una estructura dinámica de datos, los nuevos datos del pasajero se pueden insertar simplemente entre dos registros existentes con un mínimo de esfuerzo.

Otro ejemplo es si tenemos almacenados en un array (también llamados vectores o arreglos) los datos de los alumnos de un curso, los cuales están ordenados de acuerdo al promedio, para insertar un nuevo alumno sería necesario correr cada elemento un espacio: Si en su lugar se utilizara una estructura dinámica de datos, los nuevos datos del alumno se pueden insertar fácilmente.

Page 24: Analisis de Sistemas Computarizados (Libro)

2.3. Estructuras de datos lineales

Las estructuras de datos lineales se caracterizan porque sus elementos están en secuencia, relacionados en forma lineal, uno luego del otro. Cada elemento de la estructura puede estar conformado por uno o varios subelementos o campos que pueden pertenecer a cualquier tipo de dato, pero que normalmente son tipos básicos.

Una estructura lineal de datos está conformada por ninguno, uno o varios elementos que tienen una relación dónde existe un primer elemento, seguido de un segundo elemento y así sucesivamente hasta llegar al último.

Las estructuras lineales son importantes porque aparecen con mucha frecuencia en situaciones de la vida, también se utilizan muy frecuente en los esquemas algorítmicos. Ejemplos: Una cola de clientes de un banco, las instrucciones de un programa, los caracteres de una cadena o las páginas de un libro.

Características:

Existe un único elemento, llamado primero, Existe un único elemento, llamado último, Cada elemento, excepto el primero, tiene un único predecesor y Cada elemento, excepto el último, tiene un único sucesor

Existen tres estructuras lineales especialmente importantes:

1. Las pilas2. Las colas3. Las listas

Las operaciones básicas para dichas estructuras son:

Crear la secuencia vacía Añadir un elemento a la secuencia Borrar un elemento a la secuencia Consultar un elemento de la secuencia Comprobar si la secuencia está vacía

El valor contenido en los elementos pueden ser el mismo o diferente. En estas estructuras se realizan operaciones de agregar y/o eliminar elementos a la lista según un criterio particular. Para definir claramente el comportamiento de la estructura es necesario determinar en qué posición se inserta un elemento nuevo y qué elemento se borra o se obtiene.

La diferencia entre las tres estructuras vendrá dada por la posición del elemento a añadir, borrar y consultar:

Pilas: Las tres operaciones actúan sobre el final de la secuencia. Colas: Se añade por el final y se borra y consulta por el principio. Listas: Las tres operaciones se realizan sobre una posición privilegiada de la secuencia, la cual puede

desplazarse.

Page 25: Analisis de Sistemas Computarizados (Libro)

2.4. Listas contiguas

Una lista lineal es un conjunto de elementos de un tipo dado que se encuentran ordenados y pueden variar en número. Los elementos de una lista lineal se almacenan normalmente contiguos en posiciones consecutivas de la memoria. Las sucesivas entradas en una guía o directorio telefónico, por ejemplo, están en líneas sucesivas, excepto en las partes superior e inferior de cada columna. Una lista lineal se almacena en la memoria principal de una computadora en posiciones sucesivas de memoria; cuando se almacenan en cinta magnética, los elementos sucesivos se presentan en sucesión en la cinta. Esta sucesión se denomina almacenamiento secuencial.

Las líneas así definidas se denominan contiguas. Las operaciones que se pueden realizar con listas lineales contiguas son:

1. Insertar, eliminar o localizar un elemento.2. Determinar el tamaño de la lista (número de elementos).3. Recorrer la lista para localizar un determinado elemento.4. Clasificar los elementos de la lista en orden ascendente o descendente.5. Unir dos o más listas en una sola.6. Dividir una lista en varias sublistas.7. Copiar una lista.8. Borrar una lista.

Una lista lineal se almacena en la memoria de la computadora en posiciones sucesivas o adyacentes y se procesa como un arreglo unidimensional. En este caso, el acceso a cualquier elemento de la lista y la adición de nuevos elementos es fácil; Sin embargo la inserción o borrado requiere un desplazamiento de lugar de los elementos que le siguen y, en consecuencia un diseño de algoritmo especifico.

Se implementan a través de arreglos, donde la inserción o eliminación de un elemento excepto en la cabecera o al final de la lista necesitará una traslación de los elementos de la lista.

La declaración en C de una lista implementada por arreglos es la siguiente:

struct lista{int elem[long_max];int ultimo;

}

Creación de lista contigua:

struct lista* Crear(){struct lista* L = (struct lista*) malloc(sizeof(struct lista));L-> ultimo=0;return L;

}

Page 26: Analisis de Sistemas Computarizados (Libro)

2.5. Listas enlazadas

Una lista enlazada es un conjunto de elementos llamados nodos en los que cada uno de ellos contiene un dato y también la dirección del siguiente nodo, donde el orden de los mismos se establece mediante punteros.

La idea básica es que cada componente de la lista incluya un puntero que indique donde puede encontrarse el siguiente componente por lo que el orden relativo de estos puede ser fácilmente alterado modificando los punteros lo que permite, a su vez, añadir o suprimir elementos de la lista. El primer elemento de la lista es la cabecera, que sólo contiene un puntero que señala el primer elemento de la lista.El último nodo de la lista apunta a NULL (nulo) porque no hay más nodos en la lista. Se usará el término NULL para designar el final de la lista.

Listas Enlazadas frente a Arrays

Las listas enlazadas tienen las siguientes ventajas sobre los arrays:

No requieren memoria extra para soportar la expansión. Por el contrario, los arrays requieren memoria extra si se necesita expandirlo (una vez que todos los elementos tienen datos no se pueden añadir datos nuevos a un array).

Ofrecen una inserción/borrado de elementos más rápida que sus operaciones equivalentes en los arrays. Sólo se tienen que actualizar los enlaces después de identificar la posición de inserción/borrado. Desde la perspectiva de los arrays, la inserción de datos requiere el movimiento de todos los otros datos del array para crear un elemento vacío. De forma similar, el borrado de un dato existente requiere el movimiento de todos los otros datos para eliminar el elemento vacío.

En contraste, los arrays ofrecen las siguientes ventajas sobre las listas enlazadas:

Los elementos de los arrays ocupan menos memoria que los nodos porque no requieren campos de enlace. Los arrays ofrecen un acceso más rápido a los datos, mediante índices basados en enteros.

Las listas enlazadas son más apropiadas cuando se trabaja con datos dinámicos. En otras palabras, inserciones y borrados con frecuencia. Por el contrario, los arrays son más apropiados cuando los datos son estáticos (las inserciones y borrados son raras). De todas formas, no olvide que si se queda sin espacio cuando añade ítems a un array, debe crear un array más grande, copiar los datos del array original el nuevo array mayor y eliminar el original. Esto cuesta tiempo, lo que afecta especialmente al rendimiento si se hace repetidamente.

Mezclando una lista de enlace simple con un array uni-dimensional para acceder a los nodos mediante los índices del array no se consigue nada. Gastará más memoria, porque necesitará los elementos del array más los nodos, y tiempo, porque necesitará mover los ítems del array siempre que inserte o borre un nodo. Sin embargo, si es posible integrar el array con una lista enlazada para crear una estructura de datos útil.

Existen diferentes tipos de listas enlazadas:

Lista Enlazadas Simples.Listas Doblemente Enlazadas.Listas Enlazadas Circulares.

Las listas enlazadas pueden ser implementadas en muchos lenguajes. Lenguajes tales como Lisp y Scheme tiene estructuras de datos ya construidas, junto con operaciones para acceder a las listas enlazadas. Lenguajes imperativos u orientados a objetos tales como C, C++ o Java disponen de referencias para crear listas enlazadas.

Operaciones con listas enlazadas

Las operaciones que podemos realizar sobre una lista enlazada son las siguientes:

Recorrido. Esta operación consiste en visitar cada uno de los nodos que forman la lista. Para recorrer todos los nodos de la lista, se comienza con el primero, se toma el valor del campo liga para avanzar al segundo nodo, el campo liga de este nodo nos dará la dirección del tercer nodo, y así sucesivamente.Inserción. Esta operación consiste en agregar un nuevo nodo a la lista. Para esta operación se pueden considerar tres casos:

Page 27: Analisis de Sistemas Computarizados (Libro)

o Insertar un nodo al inicio.o Insertar un nodo antes o después de cierto nodo.o Insertar un nodo al final.

Borrado. La operación de borrado consiste en quitar un nodo de la lista, redefiniendo las ligas que correspondan. Se pueden presentar cuatro casos:

o Eliminar el primer nodo.o Eliminar el último nodo.o Eliminar un nodo con cierta información.o Eliminar el nodo anterior o posterior al nodo cierta con información.

Búsqueda. Esta operación consiste en visitar cada uno de los nodos, tomando al campo liga como puntero al siguiente nodo a visitar.

Figura 1. Esquema de un nodo y una lista enlazada.

LISTA ENLAZADA SIMPLE

La lista enlazada básica es la lista enlazada simple la cual tiene un enlace por nodo. Este enlace apunta al siguiente nodo en la lista, o al valor NULL o a la lista vacía, si es el último nodo.

LISTAS ENLAZADAS DOBLES

Cada nodo tiene dos enlaces: uno apunta al nodo anterior, o apunta al valor NULL o a la lista vacía si es el primer nodo; y otro que apunta al siguiente nodo siguiente, o apunta al valor NULL o a la lista vacía si es el último nodo.

LISTAS ENLAZADAS CIRCULARES

En una lista enlazada circular, el primer y el último nodo están unidos juntos. Esto se puede hacer tanto para listas enlazadas simples como para las doblemente enlazadas. Para recorrer un lista enlazada circular podemos empezar por cualquier nodo y seguir la lista en cualquier dirección hasta que se regrese hasta el nodo original. Desde otro punto de vista, las listas enlazadas circulares pueden ser vistas como listas sin comienzo ni fin. Este tipo de listas es el más usado para dirigir buffers para “ingerir” datos, y para visitar todos los nodos de una lista a partir de uno dado.

Page 28: Analisis de Sistemas Computarizados (Libro)

Cada nodo tiene un enlace, similar al de las listas enlazadas simples, excepto que el siguiente nodo del último apunta al primero. Como en una lista enlazada simple, los nuevos nodos pueden ser solo eficientemente insertados después de uno que ya tengamos referenciado. Por esta razón, es usual quedarse con una referencia solamente al último elemento en una lista enlazada circular simple, esto nos permite rápidas inserciones al principio, y también permite accesos al primer nodo desde el puntero del último nodo.

LISTAS ENLAZADAS DOBLEMENTE CIRCULAR

En una lista enlazada doblemente circular, cada nodo tiene dos enlaces, similares a los de la lista doblemente enlazada, excepto que el enlace anterior del primer nodo apunta al último y el enlace siguiente del último nodo, apunta al primero. Como en una lista doblemente enlazada, las inserciones y eliminaciones pueden ser hechas desde cualquier punto con acceso a algún nodo cercano. Aunque estructuralmente una lista circular doblemente enlazada no tiene ni principio ni fin, un puntero de acceso externo puede establecer el nodo apuntado que está en la cabeza o al nodo cola, y así mantener el orden tan bien como una lista doblemente enlazada con falsos nodos.

Implementación

Código con explicación para implementar una lista enlazada:

Lo primero que  debemos hacer es declarar las dos librerías necesarias.

#include<iostream>#include<stdlib.h>

Luego declaramos la estructura o el estruct donde vamos a guardar los nodosstruct nodo{    int info;                               // En la variable info guardaremos el contenido de los nodos.    struct nodo *sgt;                 // El puntero siguiente sera para crear los nodos de la lista.};

Seguimos como el main(), en el main declaramos los dos punteros que nos van a servir en la creación de los nodos:struct nodo *cabe;                 // este para la cabeza del nodostruct nodo *nuevo;               //este para los nuevos nodos que se creen.

Procedemos a declarar la cabeza  como NULLcabe=NULL;Importante si no declaras la cabeza del nodo como NULL posiblemente obtendrás un error en tiempo de ejecución.

Ahora  vamos a  declarar tres variables tipo entero que nos ayudaran en la creación de nodos.    int dato;                     // Esta variable para almacenar los datos que se le vallan a introducir a los nodos.    int  i=1;              //Esta es la variable del contador para crear los nodos.    int cant;             //Esta para la cantidad de nodos que el usuario desee.

Ahora imprimimos un mensaje para pedir la cantidad de nodos que desea el usuario.cout<<"Entrar cantidad de nodos=";cin>>cant;   

Como viste ya se había declarado la variable "cant" que es la que nos permite saber cuántos nodos desea el usuario.

Ahora vamos a realizar el proceso más engorroso y que requiere la mayor atención posible, por favor pon atención en

Page 29: Analisis de Sistemas Computarizados (Libro)

cada paso de esto porque son lo más vitales. Abrimos un ciclo while que llegará hasta donde lo especifico el usuario con la sentencia anterior escrita. Este ciclo while dependerá de dos variables las variables i y cant anteriormente declaradas. La variable cant es el final del ciclo por ejemplo si la variable cant contiene el valor de 5 entonces este  será el número de repeticiones del ciclo o mejor dicho será la cantidad de vueltas que dará el ciclo. Con esta variable controlamos los nodos que vamos a crear.

    while(i<=cant){  //Aqui se inicia el ciclo while.                       nuevo=(struct nodo *)malloc(sizeof(struct nodo));  //Crea un nodo nuevo                       nuevo->sgt=cabe;                       cout<<"Entre dato=";                       cin>>dato;                       nuevo->info=dato;                       cabe=nuevo;                       i++;    }

Se especificaran las líneas que se han creado en el ciclo: nuevo=(struct nodo *)malloc(sizeof(struct nodo)); esta linease usa  para crear el nuevo nodo que vamos a

usar en la primera vuelta del ciclo. La parte de  (struct nodo *)malloc(sizeof(struct nodo)) se usa para asignar memoria dinámicamente cuando

se esté ejecutando el programa. nuevo->sgt=cabe;  Esta línea es para que el nuevo nodo se le el valor de cabeza. Y así ir creando los demás

nodos de forma ordenada. Importante ver video para mirar gráficamente este proceso donde el enlace del nodo original pasa al enlace del nodo nuevo.

Entonces el nodo original pasa a ser el nodo de atrás y el nuevo nodo pasa a ser el primero, y así sucesivamente en la repetición del ciclo.

cout<<"Entre dato="; cin>>dato; Aquí solicitamos los datos que vamos a

entrar en el nuevo nodo. nuevo->info=dato;   Entonces ahora hacemos que la

información pedida anteriormente pase a la variable "info" que será el contenido del nodo nuevo.

cabe=nuevo;  aquí damos al apuntador cabeza la dirección de nuevo quedando así  el apuntador cabeza que apuntaba a el nodo original, apuntando al nodo nuevo.

Ahora procedemos a imprimir los nodos con las siguientes líneas.

    while(nuevo!=NULL){  //El apuntador nodo ira siguiendo los nodos ya para hasta encontrar NULL        cout<<"\nDATO="<<nuevo->puntos;  //Imprimimos los datos de cada nodo        nuevo=nuevo->sgt;   //Importante y no olivadar con esta sentencia corremos el apuntador al siguiente               }                                   //nodo    system("pause>>null");}

Page 30: Analisis de Sistemas Computarizados (Libro)

Código para implementar una lista enlazada.

#include<iostream>#include<stdlib.h>using namespace std;//*******************************struct nodo{ int info; struct nodo *sgt;};//************************************main(){ struct nodo *cabe; struct nodo *nuevo; struct nodo *aux; cabe=NULL; int dato; int cant, i=1, cont; cout<<"Ejemplo de Listas en C++\n\n"; cout<<"Entrar cantidad de nodos=";cin>>cant; while(i<=cant){ nuevo=(struct nodo *)malloc(sizeof(struct nodo)); nuevo->sgt=cabe; cout<<"Ingrese dato numerico="; cin>>dato; nuevo->info=dato; cabe=nuevo; i++; }

while(nuevo!=NULL){ cout<<"\nDATO="<<nuevo->info; nuevo=nuevo->sgt; } cout<<"\n\nRealizado por: David Ortiz \n"; system("pause>>null");}

Código para implementar una lista enlazada con Operaciones.

#include <iostream>#include <stdlib.h>#include <conio.h>using namespace std;struct nodo{ int nro; nodo *sgte; };struct nodo *pi, *pa, *pf;

void menu();void insertar(int);void mostrar();void buscar();void eliminar();void modificar();void clrscr();void clrscr(){ system("cls"); }void insertar(int numero){ if(pi==NULL){ pi=new(nodo); pi->nro=numero; pf=pi; } else{ pa=new(nodo); pf->sgte=pa; pa->nro=numero; pf=pa; } pf->sgte=NULL; }void mostrar(){ pa=pi; if(pi==NULL) { cout<<"\n\t lista vacia"; } while(pa!=NULL){ cout<<endl<<" \t-> "<<pa->nro; pa=pa->sgte; } cout<<"\n\n\n\t Presione 'Enter' para Regresar"; }void buscar(int numero){ int flag=0; pa=pi; if(pi==NULL) { cout<<"\n\t lista vacia"; } else {

Page 31: Analisis de Sistemas Computarizados (Libro)

while(pa!=NULL){ if(numero==pa->nro){ cout<<endl<<"\n\t Valor encontrado : "<<pa->nro; flag=1; } pa=pa->sgte; } if(flag==0){ cout<<endl<<"\n\t El numero no fue encontrado"; } } cout<<"\n\n\n\t Presione 'Enter' para Regresar"; }void modificar(int numero){ pa=pi; if(pi==NULL) { cout<<"\n\t lista vacia"; } else { int nro_mod,flag=0; while(pa!=NULL){ if(numero==pa->nro && flag==0){ cout<<endl<<"\n\t Valor Encontrado : "<<pa->nro; cout<<endl<<"\n\t Ingrese nuevo valor :"; cin>>pa->nro; flag=1; } pa=pa->sgte; } if(flag==0){ cout<<endl<<"\n\t El numero no fue encontrado"; } } pa=pi; cout<<"\n\n\n\t Presione 'Enter' para Regresar"; }void eliminar(int numero){ struct nodo *pant=NULL; if(pi==NULL){ cout<<"\n\t lista vacia"; } else{ if(pi->nro==numero){ pa=pi; pi=pi->sgte; cout<<"\n\t numero eliminado : "<<pa->nro; } else{ pa=pi->sgte; pant=pi; while(pa!=NULL &&pa->nro!=numero){ pant=pa; pa=pa->sgte; } if(pa!=NULL){ pant->sgte=pa->sgte; cout<<"\n\t numero eliminado : "<<pa->nro; } } delete(pa);

} cout<<"\n\n\n\t Presione 'Enter' para Regresar"; }

void menu(){ int opc,numero; do{ cout<<endl<<"\n\t Listas Enlazadas "; cout<<endl<<"\n\t Menu de Opciones "<<endl; cout<<"\n\t 1. Insertar"; cout<<"\n\t 2. Mostrar"; cout<<"\n\t 3. Buscar"; cout<<"\n\t 4. Modificar"; cout<<"\n\t 5. Eliminar"; cout<<"\n\t 6. Salir"; cout<<endl<<"\n\n\tIngrese opcion : "; cin>>opc; clrscr(); switch(opc){ case 1: cout<<"\n\t Ingrese Numero : "; cin>>numero; insertar(numero); clrscr(); break; case 2:mostrar();getch();clrscr();break; case 3:cout<<"\n\t Ingrese Numero a Buscar : "; cin>>numero; buscar(numero);getch();clrscr();break; case 4:cout<<"\n\t Ingrese Numero a Modificar : "; cin>>numero; modificar(numero);getch();clrscr();break; case 5:cout<<"\n\t Ingrese Numero a Eliminar : "; cin>>numero; eliminar(numero);getch();clrscr();break; } }while(opc!=6); cout<<"\n\n Realizado por: David Ortiz \n"; cout<<"\n\n\n\t Presione 'Enter' para Salir";getch(); } int main() { menu(); }

Page 32: Analisis de Sistemas Computarizados (Libro)

2.6. Pilas estáticas

Una Pila o Stack es una estructura en donde cada elemento es insertado y retirado del tope de la misma, y debido a esto el comportamiento de una pila se conoce como LIFO (último en entrar, primero en salir).

Un ejemplo de pila o stack se puede observar en el mismo procesador, es decir, cada vez que en los programas aparece una llamada a una función el microprocesador guarda el estado de ciertos registros en un segmento de memoria conocido como Stack Segment, mismos que serán recuperados al regreso de la función.

Pila en arreglo estáticoEn el programa que se verá en seguida, se simula el comportamiento de una estructura de pila. Aunque en el mismo se usa un arreglo estático de tamaño fijo se debe mencionar que normalmente las implementaciones hechas por fabricantes y/o terceras personas se basan en listas dinámicas o enlazadas.

Para la implementación de la clase Stack se han elegido los métodos:

put(), poner un elemento en la pila get(), retirar un elemento de la pila empty(), regresa 1 (TRUE) si la pila está vacía size(), número de elementos en la pila

El atributo SP de la clase Stack es el puntero de lectura/escritura, es decir, el SP indica la posición dentro de la pila en donde la función put() insertará el siguiente dato, y la posición dentro de la pila de donde la función get() leerá el siguiente dato.

Cada vez que put() inserta un elemento el SP se decrementa. Cada vez que get() retira un elemento el SP se incrementa.

En el siguiente ejemplo se analiza lo que sucede con el SP (puntero de pila) cuando se guardan en la pila uno por uno los caracteres 'A', 'B', 'C' y 'D'. Observe que al principio el SP es igual al tamaño de la pila.

Llenando la pila.

SP |+---+---+---+---+---+| | | | | | Al principio (lista vacía)+---+---+---+---+---+ SP |+---+---+---+---+---+ push('A');| | | | | A | después de haber agregado el primer elemento+---+---+---+---+---+SP |+---+---+---+---+---+| | D | C | B | A | después de haber agregado cuatro elementos+---+---+---+---+---+

Page 33: Analisis de Sistemas Computarizados (Libro)

Vaciando la pila.

SP |+---+---+---+---+---+ pop();| | D | C | B | A | después de haber retirado un elemento+---+---+---+---+---+ SP |+---+---+---+---+---+| | D | C | B | A | después de haber retirado todos los elementos+---+---+---+---+---+

Nota: observe que al final la lista está vacía, y que dicho estado se debe a que el puntero está al final de la pila y no al hecho de borrar físicamente cada elemento de la pila.

Implementación en C++

Ejemplo: Pila basada en un arreglo estático.

#include <iostream>using namespace std; #define STACK_SIZE 256 /* capacidad máxima */typedef char arreglo[STACK_SIZE]; class Stack {

int sp; /* puntero de lectura/escritura */int items; /* número de elementos en lista */int itemsize; /* tamaño del elemento */arreglo pila; /* el arreglo */ public:

// constructorStack() {

sp = STACK_SIZE-1;items = 0;itemsize = 1;

}

// destructor~Stack() {};

/* regresa el número de elementos en lista */int size() { return items; } /* regresa 1 si no hay elementos en la lista, o sea, si la lista está vacia */int empty() { return items == 0; } /* insertar elemento a la lista */int put(char d){

Page 34: Analisis de Sistemas Computarizados (Libro)

if ( sp >= 0) {pila[sp] = d;sp --;items ++;

}return d;

} /* retirar elemento de la lista */int get(){

if ( ! empty() ) {sp ++;items --;

}return pila[sp];

}

}; // fin de clase Stack // probando la pila. // Nota: obseve cómo los elementos se ingresan en orden desde la A hasta la Z,// y como los mismos se recuperán en orden inverso.int main(){ int d; Stack s; // s es un objeto (instancia) de la clase Stack cout<<"\n\nLlenando la pila... presiona <Enter> \n"; cin.get();// llenando la pila for (d='A'; d<='Z'; d++) s.put(d); cout << "Items =" << s.size() << endl; cout<<"\n\nVaciando la pila... presiona <Enter>\n"; cin.get();// vaciando la pila while ( s.size() ) cout << (char)s.get() << " "; cout << "\n\nPara terminar presiona <Enter>..."; cout<<"\nRealizado por: David Ortiz \n"; cin.get(); return 0;}

Page 35: Analisis de Sistemas Computarizados (Libro)

2.7. Pilas dinámicas

Es importante hacer notar que, a diferencia de una pila basada en un arreglo estático, una pila enlazada dinámicamente no posee de forma natural el mecanismo de acceso por índices, en ese sentido, el programador puede crear los algoritmos necesarios para permitir tal comportamiento. En la clase que se presenta en el ejemplo no se ha implementado el mecanismo de acceso por índices, ya que la misma se presenta como una alternativa para la simulación de una pila.

Uno de los puntos más destacables en cuando al uso de listas enlazadas dinámicamente es el hecho de crear estructuras conocidas como nodos. Por ejemplo, para crear una estructura de nodo para almacenar enteros y a la vez para apuntar a otro posible nodo podemos emplear la sintaxis:

struct nodo { int data; nodo *siguiente;};

Observe que con la declaración anterior estamos creando el tipo estructurado nodo, mismo que posee a los miembros: data para guardar valores enteros, y siguiente para apuntar o enlazar a un supuesto siguiente nodo.

Ya que las pilas dinámicas inicialmente se encuentran vacías, y más aún, una pila dinámica no posee una dirección establecida en tiempo de compilación ya que la dirección de memoria que ocupará cada uno de los elementos se establecerá en tiempo de ejecución, entonces cómo determinar la condición de vacío. En nuestro ejemplo usaremos un contador (ITEMS) que dicho sea de paso, si ITEMS = 0, entonces la pila está vacía (la condición de vacío también podría determinarse al verificar el SP, es decir, si el SP = NULL, significa que la lista no posee elementos).

Al hacer un análisis previo de los eventos que acontecerán en la pila y su puntero de lectura y escritura (SP, que en esta ocasión es una estructura tipo nodo), se tiene lo siguiente:

1) Al principio la pila está vacía, en ese caso el SP es igual a NULL y, en consecuencia, el puntero next también es NULL.

SP = NULL +------+------+ | ???? | next |--> NULL +------+------+

2) Después de agregar el primer elemento la situación se vería así:

SP = asignado 1 +------+------+ | data | next |--> NULL +------+------+

3) Después de agregar otro elemento la situación se vería así:

SP = asignado 2 1 +------+------+ +------+------+ | data | next |--> | data | next |--> NULL +------+------+ +------+------+

Page 36: Analisis de Sistemas Computarizados (Libro)

Implementación en C++

Ejemplo: Pila basada en un arreglo estático

#include <iostream>using namespace std;

/* tipo de dato que contendrá la lista */typedef char DATA_TYPE;

// declaraci¢n de estructura nodostruct nodo {

DATA_TYPE data;nodo *next;

};

class StackDin { // atributos int ITEMS; /* número de elementos en la lista */ int ITEMSIZE; /* tamaño de cada elemento */ nodo *SP; /* puntero de lectura/escritura */public: // constructor StackDin() : SP(NULL), ITEMS(0), ITEMSIZE(sizeof(DATA_TYPE)) {} // destructor ~StackDin() {} // regresa el numero de elementos de la pila int size() { return ITEMS; } /* agregar componente a la lista */ DATA_TYPE put(DATA_TYPE valor) {

nodo *temp;

temp = new nodo;if (temp == NULL) return -1;

temp->data = valor;temp->next = SP;SP = temp;ITEMS ++;return valor;

}

int empty() { return ITEMS == 0; } /* retirar elemento de la lista */ DATA_TYPE get() {

nodo *temp;DATA_TYPE d;

if ( empty() ) return -1;

d = SP->data;temp = SP->next;if (SP) delete SP;SP = temp;ITEMS --;return d;

}}; // fin de la clase StackDin/* punto de prueba para la clase StackDin */int main()

Page 37: Analisis de Sistemas Computarizados (Libro)

{ StackDin s; DATA_TYPE d; cout<<"\n\nLlenando la pila... presiona <Enter> \n"; cin.get();// llenando la pila for (d='A'; d<='Z'; d++) s.put(d); cout << "Items =" << s.size() << endl; cout<<"\n\nVaciando la pila... presiona <Enter>\n"; cin.get();// vaciando la pila while ( ! s.empty() ) cout << (DATA_TYPE)s.get() << " ";

cout << "\n\nPara terminar presione <Enter>..."; cout<<"\nRealizado por: David Ortiz \n"; cin.get(); return 0;}

Page 38: Analisis de Sistemas Computarizados (Libro)

2.8. Colas estáticas

Una cola sencilla (Queue) es una estructura en donde cada elemento es insertado inmediatamente después del último elemento insertado; y donde los elementos se retiran siempre por el frente de la misma, debido a esto el comportamiento de un una cola se conoce como FIFO (primero en entrar, primero en salir).

Un ejemplo a citar de cola es el comportamiento del buffer del teclado. Cuando en el teclado se oprime una tecla, el código del carácter ingresado es trasladado y depositado en un área de memoria intermedia conocida como "el buffer del teclado", para esto el microprocesador llama a una rutina específica. Luego, para leer el carácter depositado en el buffer existe otra función, es decir, hay una rutina para escribir y otra para leer los caracteres del buffer cada una de las cuales posee un puntero; uno para saber en dónde dentro del buffer se escribirá el siguiente código y otro para saber de dónde dentro del buffer se leerá el siguiente código.

Cola en un arreglo estático.

En el programa que se ve en seguida, se simula el comportamiento de una estructura de cola simple. Aunque en el mismo se usa un arreglo estático de tamaño fijo se debe mencionar que normalmente las implementaciones hechas por fabricantes y/o terceras personas se basan en listas dinámicas o dinámicamente enlazadas.

Para la implementación de la clase Queue se han elegido los métodos:

put(), poner un elemento en la colaget(), retirar un elemento de la colaempty(), regresa 1 (TRUE) si la cola está vacíasize(), número de elementos en la cola

El atributo cabeza de la clase Queue es el puntero de lectura.El atributo cola de la clase Queue es el puntero de escritura.

Es decir, la cola indica la posición dentro de la lista en donde la función put() insertará el siguiente dato, y la cabeza indica la posición dentro de la lista de donde la función get() leerá el siguiente dato.

Cada vez que put() inserta un elemento la cola se incrementa.Cada vez que get() retira un elemento la cabeza se incrementa.

En el siguiente ejemplo se analiza lo que sucede con la cola y la cabeza (punteros de escritura y de lectura de la Cola) cuando se guardan en la cola uno por uno los caracteres 'A', 'B', 'C' y 'D'. Observe que al principio: cola = cabeza = cero.

Llenando la cola.

cola |+---+---+---+---+---+| | | | | | al principio+---+---+---+---+---+ | cabeza

cola |+---+---+---+---+---+ put('A');| A | | | | | después de haber agregado el primer elemento+---+---+---+---+---+ | cabeza

...

Page 39: Analisis de Sistemas Computarizados (Libro)

cola |+---+---+---+---+---+| A | B | C | D | | después de haber agregado cuatro elementos+---+---+---+---+---+ | cabeza

Vaciando la cola.

cabeza |+---+---+---+---+---+| A | B | C | D | | antes de haber retirado elementos+---+---+---+---+---+ cabeza |+---+---+---+---+---+ get();| A | B | C | D | | después de haber retirado un elemento+---+---+---+---+---+

...

cabeza |+---+---+---+---+---+ al final| A | B | C | D | | después de haber retirado todos los elementos+---+---+---+---+---+ | cola

Obsérvese que al final el cabeza apunta hacia el mismo elemento que la cola, es decir, la cola vuelve a estar vacía. Puesto que la cola que estamos proyectando reside en un arreglo estático los componentes del arreglo aún están dentro de la misma, salvo que para su recuperación se debería escribir otro método. En una cola dinámica (como se demostrará más adelante) los elementos retirados de la misma se eliminan de la memoria y podría no ser posible su recuperación posterior.

Nota: En el programa que aparece en seguida, el comportamiento de sus punteros a través de los métodos para escribir o leer si detectan que el puntero correspondiente ha sobrepasado el tamaño máximo de elementos permitidos dentro de la cola, éste es puesto a cero.

Implementación en C++

Ejemplo: cola en un arreglo estático.

#include <iostream.h>

#define MAX_SIZE 256 /* capacidad máxima */typedef char almacen[MAX_SIZE];

class Queue {

int cabeza; /* puntero de lectura */int cola; /* puntero de escritura */int ITEMS; /* número de elementos en la lista */int ITEMSIZE; /* tamaño de cada elemento */almacen alma; /* el almacen */

public: // constructor Queue() {

Page 40: Analisis de Sistemas Computarizados (Libro)

cabeza = 0;cola = 0;ITEMS = 0;ITEMSIZE = 1;

}

// destructor ~Queue() {}

// regresa 1 (true) si la lista está vaciaint empty() { return ITEMS == 0; }

// insertar elemento a la listaint put(int d){ if ( ITEMS == MAX_SIZE) return -1; if ( cola >= MAX_SIZE) { cola = 0; } alma[cola] = d; cola ++; ITEMS ++; return d;}

// retirar elemento de la listaint get(){ char d; if ( empty() ) return -1; if ( cabeza >= MAX_SIZE ) { cabeza = 0; } d = alma[cabeza]; cabeza ++; ITEMS --; return d;}

// regresa el n£mero de elementos en listaint size() { return ITEMS; }

}; // fin de la clase Queue

// probando la colaint main(){ int d; Queue q;

for (d='A'; d<='Z'; d++) q.put(d);

cout << "Items = " << q.size() << endl;

while ( q.size() ) {cout << (char)q.get() << " ";

}

cout<<"\n\nRealizado:por David Ortiz\n"; cout << "\nPara terminar oprima <Enter> ..."; cin .get(); return 0;}

Page 41: Analisis de Sistemas Computarizados (Libro)

2.9. Colas dinámicas

Una cola dinámica no tendrá un tamaño fijo, sino que éste irá variando según se añadan o eliminen objetos de la cola.

Para poder realizar la cola dinámica, emplearemos un objeto Nodo, que contendrá el dato a almacenar en cada lugar de la cola y la referencia al siguiente nodo que le sigue.

Implementación en C++

Ejemplo: cola en un arreglo dinámico.

#include <iostream>

using namespace std;

typedef char DATA_TYPE;

struct nodo {DATA_TYPE data;nodo *next;

};

class QueueDin {

// atributos int ITEMS, ITEMSIZE; nodo *cola, *cabeza;

public: // constructor QueueDin() : cola(NULL), cabeza(NULL), ITEMS(0), ITEMSIZE(sizeof(DATA_TYPE)) {}

// destructor ~QueueDin() {}

/* agregar componente a la lista */ DATA_TYPE put(DATA_TYPE valor) {

nodo *temp;

temp = new nodo;if (temp == NULL) return -1;

ITEMS ++;temp->data = valor;temp->next = NULL;

if (cabeza == NULL){ cabeza = temp; cola = temp;} else{ cola->next = temp; cola = temp;}return valor;

}

// regresa 1 (true) si la lista está vacia int empty() { return ITEMS == 0; }

/* retirar elemento de la lista */ DATA_TYPE get() {

Page 42: Analisis de Sistemas Computarizados (Libro)

nodo *temp;DATA_TYPE d;

if ( empty() ) return -1;

d = cabeza->data;temp = cabeza->next;if (cabeza) delete cabeza;cabeza = temp;ITEMS --;return d;

}

// regresa el n£mero de elementos en listaint size() { return ITEMS; }

}; // fin de la clase QueueDin

/* punto de prueba */int main(){ QueueDin s; DATA_TYPE d;

// llenando la cola for (d='A'; d<='Z'; d++)s.put(d); cout << "Items = " << s.size() << endl;

// vaciando la cola while ( ! s.empty() )

cout << (DATA_TYPE)s.get() << " "; cout<<"\n\nRealizado:por David Ortiz\n"; cout << "\nPara terminar presione <Enter>..."; cin.get(); return 0;}

Page 43: Analisis de Sistemas Computarizados (Libro)

2.10. Aplicaciones

LISTAS

Un sistema operativo es un programa que gestiona y asigna los recursos de un sistema informático. Los sistemas operativos utilizan listas enlazadas de muchas formas. La asignación de espacio de memoria (uno de los recursos del sistema) puede gestionarse usando una lista doblemente enlazada de bloques de tamaño variables de memoria. El enlace doble de las listas facilita la sustitución de bloques de la mitad de la lista. En un sistema multiusuario, el sistema operativo puede llevar los trabajos en espera del usuario para que se ejecuten mediante colas enlazadas de “bloques de control” (registros que contienen información sobre la identificación del usuario, el programa a ejecutar los archivos asociados con el programa, etc.).

Aunque normalmente una representación enlazada o secuencial de una lista puede usarse con buenos resultados, hay veces en las que la enlazada es una elección mucho mejor. Por ejemplo, las listas enlazadas se utilizan frecuentemente para implementar una matriz rala. Una matriz, al ser tan grande de tamaño, no es posible (o es inútil) definirla como un array de dos dimensiones, porque la mayor parte de sus elementos son nulos, y estaríamos ocupando memoria estática demasiado grande sin sentido alguno. De esta forma, una matriz rala puede especificarse de una forma más eficiente mediante una implementación dinámica, con listas enlazadas, ya que podemos almacenar en el campo info los índices de la matriz donde se encuentra el elemento no nulo (fila y columna), y obviamente el coeficiente correspondiente.

PILAS

Un ejemplo natural de la aplicación de la estructura pila aparece durante la ejecución de un programa de ordenador, en la forma en que la máquina procesa las llamadas a los procedimientos. Cada llamada a un procedimiento (o función) hace que el sistema almacene toda la información asociada con ese procedimiento (parámetros, variables, constantes, dirección de retorno, etc...) de forma independiente a otros procedimientos y permitiendo que unos procedimientos puedan invocar a otros distintos (o a sí mismos) y que toda esa información almacenada pueda ser recuperada convenientemente cuando corresponda. Como en un procesador sólo se puede estar ejecutando un procedimiento, esto quiere decir que sólo es necesario que sean accesibles los datos de un procedimiento (el último activado, el que está en la cima). De ahí que una estructura muy apropiada para este fin sea la estructura pila.

Algunos de los casos más representativos de aplicación de las mismas pilas son:

Llamadas a subprogramas Recursividad Tratamiento de expresiones aritméticas Ordenación Expresiones aritméticas

COLAS

Las Colas también se utilizan en muchas maneras en los sistemas operativos para planificar el uso de los distintos recursos de la computadora. Uno de estos recursos es la propia CPU (Unidad Central de Procesamiento).

Si está trabajando en un sistema multiusuario, cuando le dice a la computadora que ejecute un programa concreto, el sistema operativo añade su petición a su "cola de trabajo".

Cuando su petición llega al frente de la cola, el programa solicitado pasa a ejecutarse. Igualmente, las colas se utilizan para asignar tiempo a los distintos usuarios de los dispositivos de entrada/salida (E/S), impresoras, discos, cintas y demás. El sistema operativo mantiene colas para peticiones de imprimir, leer o escribir en cada uno de estos dispositivos.

Otro ejemplo se refiere a las redes de computadores. Hay muchas redes de computadores personales en las que el disco está conectado a una máquina, conocida como servidor de archivos. Los usuarios en otras máquinas obtienen acceso a los archivos sobre la base de que el primero en llegar es el primero atendido, así que la estructura de datos es una cola.

Page 44: Analisis de Sistemas Computarizados (Libro)

Entre otros ejemplos podemos mencionar:

Por lo general, las llamadas telefónicas a compañías grandes se colocan en una cola, cuando todas las operadoras están ocupadas.

En universidades grandes, cuando los recursos son limitados, los estudiantes deben firmar una lista de espera si todas las terminales están ocupadas. Aquel estudiante que haya estado más tiempo en una terminal es el primero que debe desocuparla, y el que haya estado esperando más tiempo será el que tenga acceso primero.

En un supermercado, intentar simulas el funcionamiento de una cola para saber cuántas cajas son necesarias dependiendo de varias condiciones, el número de clientes, y el tiempo medio de clientes.

IMPLEMENTACION APLICANDO UNA LISTA SIMPLE EN C++

#include <iostream>#include <stdlib.h> using namespace std; struct nodo{ int nro; struct nodo *sgte; }; typedef struct nodo *Tlista; /*-------------------- Insertar siguiente Elemento-------------------------*/void insertarSgte(Tlista &lista, int valor) { Tlista t, q = new(struct nodo); q->nro = valor; q->sgte = NULL; if(lista==NULL) { lista = q; } else { t = lista; while(t->sgte!=NULL) { t = t->sgte; } t->sgte = q; } } /*----------------------Mostrar Lista--------------------------------------*/void reportarLista(Tlista lista) { int i = 0; while(lista != NULL) { cout <<' '<< i+1 <<") " << lista->nro << endl; lista = lista->sgte; i++; } }

Page 45: Analisis de Sistemas Computarizados (Libro)

void calcularMayMenProm(Tlista lista, int mayor, int menor, int promedio, int n){ while(lista!=NULL){ if(mayor<(lista->nro)) mayor=lista->nro; if(menor>(lista->nro)) menor=lista->nro; promedio+=lista->nro; lista=lista->sgte; } promedio=promedio/n; cout<<endl<<"mayor:"<<mayor<<endl; cout<<endl<<"menor:"<<menor<<endl; cout<<endl<<"promedio:"<<promedio<<endl<<endl; } /*------------------------- Funcion Principal ---------------------------*/ int main(void) { Tlista lista = NULL; int n, mayor, menor, promedio; system("color 0a"); cout<<"\n\n\t\t[ EJERCICIO UTILIZANDO LISTAS SIMPLES ]\n"; cout<<"\t\t-----------------------------\n\n"; cout<<" EJERCICIO: Calcular mayor, menor y promedio de una lista"<<endl<<endl; cout<<"\n Ingrese tamanio de lista: "; cin>>n; for(int i=1;i<=n;i++){ insertarSgte(lista,i); } cout<<endl<<"Elementos de lista"<<endl; reportarLista(lista); mayor=lista->nro; menor=lista->nro; promedio=lista->nro; lista=lista->sgte; calcularMayMenProm(lista, mayor, menor, promedio, n); cout<<"\n\nRealizado:por David Ortiz\n"; cout << "\nPara terminar presione <Enter>..."; system("pause"); return 0; }

Page 46: Analisis de Sistemas Computarizados (Libro)

UNIDAD 3: ESTRUCTURAS DE DATOS NO LINEALES

Page 47: Analisis de Sistemas Computarizados (Libro)

3.3.1. Estructuras de datos no lineales

Dentro de las estructuras de datos no lineales tenemos los árboles y grafos. Este tipo de estructuras los datos no se encuentran ubicados secuencialmente. Nos permiten resolver problemas computacionales complejos. Las estructuras de datos no lineales se caracterizan por no existir una relación de sus elementos es decir que un elemento puede estar con cero o más elementos. Pueden estar enlazados con más de un elemento anterior y posterior.

Características:

Superan las desventajas de las listas.Sus elementos se pueden recorrer en distintas formas, no necesariamente uno detrás de otro.Son muy útiles para la búsqueda y recuperación de información.

Dato

s no

linea

les

Árboles

Grafos

Figura 1: Ejemplo de un árbol. Figura 2: Ejemplo de un grafo.

Page 48: Analisis de Sistemas Computarizados (Libro)
Page 49: Analisis de Sistemas Computarizados (Libro)

3.2. Concepto de recursividad

La recursividad no es una estructura de datos, sino que es una técnica de programación que nos permite que un bloque de instrucciones se ejecute n veces. Es una alternativa diferente para implementar estructuras de repetición (ciclos) ya que remplaza en ocasiones a estructuras repetitivas. Los módulos se hacen llamadas recursivas. Se puede usar en toda situación en la cual la solución pueda ser expresada como una secuencia de movimientos, pasos o transformaciones gobernadas por un conjunto de reglas no ambiguas.

La recursividad es un concepto difícil de entender en principio, pero luego de analizar diferentes problemas aparecen puntos comunes. En C++ los métodos pueden llamarse a sí mismos. Si dentro de un método existe la llamada a sí mismo decimos que el método es recursivo. Cuando un método se llama a sí mismo, se asigna espacio en la pila para las nuevas variables locales y parámetros. Al volver de una llamada recursiva, se recuperan de la pila las variables locales y los parámetros antiguos y la ejecución se reanuda en el punto de la llamada al método.

La recursividad es un método que lo podemos usar ya que se llama a sí mismo. En otras palabras, la recursividad nos ayuda a que los procesos se repitan así mismos y sea más fácil su codificación. La recursividad la podemos usar cuando tenemos problemas matemáticos como la derivada de cualquier función.

Figura 3: Imágenes con recursividad.

Page 50: Analisis de Sistemas Computarizados (Libro)

3.3. Naturaleza de la recursividad

Los cálculos se realizan entonces recursivamente donde la solución óptima de un subproblema se utiliza como dato de entrada para el siguiente problema. La solución para todo el problema está disponible cuando se soluciona el último problema. La forma en que se realizan los cálculos recursivos depende de cómo se descomponga el problema original. En particular, normalmente los subproblemas están vinculados por restricciones comunes.

Factible de utilizar recursividad:

Para simplificar el código.Cuando la estructura de datos es recursiva.

No factible utilizar recursividad:

Cuando los métodos usen arreglos largos.Cuando el método cambia de manera impredecible de campos.Cuando las iteraciones sean la mejor opción.

Recursión vs iteración

Repetición Iteración: ciclo explícito (se expresa claramente). Recursión: repetidas invocaciones a método.

Terminación Iteración: el ciclo termina o la condición del ciclo falla. Recursión: se reconoce el caso base.

En ambos casos podemos tener ciclos infinitos. La recursividad se debe usar cuando sea realmente necesaria, es decir, cuando no exista una solución iterativa simple.

Figura 4: Ejemplo de recursividad.

Page 51: Analisis de Sistemas Computarizados (Libro)

3.4. Procedimientos y funciones recursivas

Procedimiento

Un procedimiento es un fragmento de código cuya función es la de realizar una tarea específica independientemente del programa en el que se encuentre. Con los procedimientos se pueden crear algoritmos de ordenación de arrays, de modificación de datos, cálculos paralelos a la aplicación, activación de servicios, etc. Prácticamente cualquier cosa puede realizarse desde procedimientos independientes.

Función

Una función es exactamente lo mismo que un procedimiento salvo por un detalle, una función puede devolver un valor al programa principal y un procedimiento no. Con las funciones podemos realizar todas las tareas que se hacen con los procedimientos pero además pudiendo devolver valores (como por ejemplo el área de un triángulo).

Estructura y uso

Como se puede ver los conceptos de procedimiento y función son bastante similares, incluso podríamos definir un procedimiento como una función que no retorna ningún valor. Ambos poseen la misma estructura:

Tipo_Dato_Retorno Nombre (Parámetros){ sentencias; retorno; (sólo en el caso de las funciones)}

Para un procedimiento el Tipo_Dato_Retorno es siempre void, o lo que es lo mismo, nada. En el caso de las funciones se puede poner cualquier tipo de dato, int, float, char, etc.

Es muy recomendable definir todos los procedimientos y funciones antes de la función principal main, el motivo de esto es que en C todas las funciones y procedimientos deben definirse antes de la primera línea de código en que se usa. En caso de que incumplamos ésta norma a la hora de compilar tendremos errores.

Para llamar a un procedimiento o función debemos hacerlo de la siguiente forma:

En un procedimiento escribimos su nombre seguido de los parámetros entre paréntesis:

areatriangulo();

En una función debemos declarar antes una variable que almacene el dato que la función va a devolver, la variable debe ser del mismo tipo que el que retorna la función. Seguidamente llamamos a la función escribiendo la variable seguido del operador de asignación, el nombre de la función y los parámetros entre paréntesis:

float area; area=areatriangulo();

Implementar funciones y procedimientos en nuestros programas es una muy buena práctica que se debe hacer siempre que se pueda, ayuda a mantener organizado el código y nos permite reutilizar funcionalidades en otras secciones del programa.

Parámetros.

A la hora de crear funciones y procedimientos puede que sea necesario que trabajemos con datos que se encuentran alojados en el programa que los llama, para eso existen los parámetros. Los parámetros son valores que se mandan a un procedimiento o función para que éstos puedan trabajar con ellos.

Page 52: Analisis de Sistemas Computarizados (Libro)

Para utilizar los parámetros en un procedimiento o función debemos declarar el tipo de dato y su identificador, exactamente igual que si declaramos una variable.

Paso de parámetros por valor y por referencia.

El paso de parámetros a procedimientos y funciones es un recurso bastante potente de cara a trabajar con datos, además tenemos la posibilidad de pasar estos parámetros de varias formas, por valor o por referencia, esto lo especificamos cuando definimos el procedimiento o función.

El paso por valor es el que acabamos de ver, mediante el paso por valor el procedimiento o función reciben los valores indicados para poder trabajar con ellos, su declaración es la siguiente:

Procedimiento - void areatriangulo (float base, float altura) Función - float areatriangulo (float base, float altura)

A veces necesitaremos modificar varios de estos datos que hemos pasado en los parámetros, para ello es posible declarar los parámetros de tal forma que se puedan modificar, a esto se le llama paso por referencia.

Gracias al paso por referencia un procedimiento o función no sólo realiza unas operaciones y devuelve (en el caso de las funciones) un valor, sino que también puede modificar los valores que se pasan como parámetros.

Para poder realizar el paso de parámetros por referencia es necesario que en la declaración del procedimiento o función se especifique el espacio de memoria donde se encuentra alojado el dato, esto se realiza así:

Procedimiento - void areatriangulo (float *base, float *altura) Función - float areatriangulo (float *base, float *altura)

Lo que acabamos de definir como parámetros no son las variables en sí, sino un puntero que apunta a la dirección de memoria donde se encuentra alojado el valor, así podremos modificarlo.

Para llamar a un procedimiento o función pasando los parámetros por valor se hace de la forma que hemos visto:

Procedimiento - areatriangulo(base, altura); Función - area=areatriangulo(base, altura);

Para llamar a un procedimiento o función pasando los parámetros por referencia se hace de la siguiente forma:

Procedimiento - areatriangulo(&base, &altura); Función - area=areatriangulo(&base, &altura);

Así lo que pasamos como parámetros no es el valor de la variable, sino la dirección de memoria donde se encuentra el dato, de ésta forma el procedimiento o función podrá modificar su valor directamente.

Ejemplos de códigos en C++ implementado lo anterior:

1. Área de un triángulo utilizando procedimientos.

#include <iostream>using namespace std;

void areatriangulo (void) { //empieza procedimiento float base, altura; cout<<"Introduce base: "; cin>>base; cout<<"Introduce altura: ";

Page 53: Analisis de Sistemas Computarizados (Libro)

cin>>altura; cout<<"El Area es: "<<(base*altura)/2<<"\n";} // termina procedimiento

int main(int argc, char *argv[]){ areatriangulo(); // se llama al procedimiento cout<<"\n\nRealizado por: David Ortiz\n"; system("PAUSE"); return 0;}

2. Área de un triángulo utilizando funciones.

#include <iostream>using namespace std;

float areatriangulo (void) {// empieza la función float base, altura; cout<<"Introduce base: "; cin>>base; cout<<"Introduce altura: "; cin>>altura; return (base*altura)/2; // retorno de valor}// termina la función

int main(int argc, char *argv[]){ float area; area=areatriangulo(); // se llama a la función cout<<"El Area es: "<<area<<"\n"; cout<<"\n\nRealizado por: David Ortiz\n"; system("PAUSE"); return 0;}

3. Área de un triángulo utilizando funciones con parámetros.

#include <iostream>using namespace std;

void areatriangulo (float base, float altura) { // empieza función cout<<"El área es: "<<(base*altura)/2<<"\n";}// termina función

int main(int argc, char *argv[]){ float base, altura; cout<<"Introduce base: "; cin>>base; cout<<"Introduce altura: "; cin>>altura;

areatriangulo(base,altura); // llamado a función con parametros cout<<"\n\nRealizado por: David Ortiz\n"; system("PAUSE"); return 0;

Page 54: Analisis de Sistemas Computarizados (Libro)

}4. Intercambiar dos números utilizando procedimientos con parámetros punteros.

#include <iostream>using namespace std;

void intercambio(int *num1, int *num2){// empieza procedimiento int aux;

aux=*num1; *num1=*num2; *num2=aux;

cout<<"El intercambio es: num1="<<*num1<<" num2="<<*num2<<"\n";}// termina procedimiento

int main(int argc, char *argv[]){ int num1=3, num2=5; cout<<"Vamos a intercambiar: num1="<<num1<<" num2="<<num2<<"\n"; intercambio(&num1,&num2); // se llama a procedimiento con parametros por punteros cout<<"El resultado tras el intercambio es: num1="<<num1<<" num2="<<num2<<"\n"; cout<<"\n\nRealizado por: David Ortiz\n"; system("PAUSE"); return 0;}

Procedimientos y Funciones Recursivas.

Procedimiento Recursivo: Un procedimiento recursivo es aquel que se llama así mismo, solo que no regresa valor.Función Recursiva: Una función recursiva es aquella que se llama así misma y regresa un valor.

Cada método (función o procedimiento), tiene ciertas reglas, las cuales se mencionan a continuación:

La Función Recursiva Debe tener ciertos argumentos llamados valores base para que esta ya no se refiera a sí misma.

El Procedimiento Recursivo es donde Cada vez que la función se refiera a sí misma debe estar más cerca de los valores base.

Propiedades de procedimientos recursivos

Debe existir criterio base para que este se llame a sí mismo. Cada vez que el procedimiento se llame a si mismo debe estar más cerca del criterio base.

El Método De Las Tres PreguntasSe usa para verificar si hay dentro de un programa funciones recursivas, se debe responder a 3 preguntas.

1. La pregunta caso base:¿Hay salida NO recursiva del procedimiento o función y la rutina funciona correctamente para este caso base?

2. La pregunta llamador más pequeño¿Cada llamada al procedimiento o función se refiere a un caso más pequeño del problema original?

3. La pregunta caso generalSuponiendo que las llamadas recursivas funcionan correctamente ¿funciona correctamente todo el procedimiento o función?

Page 55: Analisis de Sistemas Computarizados (Libro)

Crear una subrutina recursiva requiere principalmente la definición de un "caso base", y entonces definir reglas para subdividir casos más complejos en el caso base. Para una subrutina recursiva es esencial que con cada llamada recursiva, el problema se reduzca de forma que al final llegue al caso base.

Caso base: una solución simple para un caso particular (puede haber más de un caso base).

Escritura de procedimiento y funciones recursivas (pasos a seguir para hacer programas recursivos). Se puede utilizar el siguiente método para escribir cualquier rutina recursiva.

Primero obtener una función exacta del problema a resolver. A continuación, determinar el tamaño del problema completo que hay que resolver, este tamaño determina los

valores de los parámetros en la llamada inicial al procedimiento o función. Resolver el caso base en el que el problema puede expresarse no recursivamente, esto asegura una respuesta

afirmativa a la pregunta base. Por último, resolver el caso general correctamente en términos de un caso más pequeño del mismo problema, es

decir una respuesta afirmativa a las preguntas 2 y 3 del método de las 3 preguntas.

Tipos de Recursividad

En programación existen dos tipos de recursividad:

1. La recursividad directa: es cuando un procedimiento o una función se llama a sí mismas.2. La recursividad indirecta: es cuando un procedimiento o función invoca a otro procedimiento o

función, y este último vuelve a invocar al primero.

1.

Page 56: Analisis de Sistemas Computarizados (Libro)

3.5. Resolución de problemas recursivos

El tema de la recursividad nos ofrece un abanico de posibilidades bastante amplio de cara a comenzar con desarrollos más grandes.

El haber visto el concepto de procedimientos y funciones nos facilitará enormemente la tarea de organizar el código, esto apoyado en el paso de parámetros nos asegura unas posibilidades casi ilimitadas.

Ejemplos de implementación de procedimientos y funciones recursivas en C++.

1. Implementación de un procedimiento recursivo que imprime los dígitos de un número natural n en orden inverso. Por ejemplo, para n=675 la salida debería ser 576.

#include <iostream>using namespace std; unsigned leerDato(){ // empieza función unsigned res; cout << "Introduce numero: "; cin >> res; return res;} // termina función void inverso (unsigned n){ //empieza procedimiento if(n == 0){ cout << " "; }else{ cout << n%10; inverso(unsigned(n/10)); // recursividad }} // termina procedimiento

int main() { cout << "Programa para Invertir un numero. \n\n\n" << endl; unsigned n =leerDato(); // llama a función cout << "El numero invertido es: "; inverso(n); // llama a procedimiento cout<<"\n\nRealizado por: David Ortiz\n"; system("PAUSE"); return 0;}

2. Implementación de una función que calcule recursivamente el valor de un número elevado a la “n” potencia xn.

#include <iostream>using namespace std; void leerDatos(unsigned& x, unsigned& n){ // empieza procedimiento cout << "Introduce el numero(x): "; cin >> x; cout << "Introduce la potencia(n): "; cin >> n;} // termina procedimiento unsigned potencia(unsigned x, unsigned n){ // empieza función unsigned pot; if(n == 0){ pot = 1; }else{

Page 57: Analisis de Sistemas Computarizados (Libro)

pot = x * potencia(x, n-1); // recursividad } return pot; // retorna valor} // termina función int main() { unsigned x, n; cout << "Programa que calcula una potencia x^n \n\n\n" << endl; leerDatos(x, n); // llama procedimiento cout << "El resultado es: "<<potencia(x, n); // llama a la función cout<<"\n\nRealizado por: David Ortiz\n"; system("PAUSE"); return 0;}

TAREA:

1. Realizar la implementación para calcular el factorial de un número utilizando funciones o procedimientos recursivos.

2. Realizar la implementación de la sucesión de Fibonacci utilizando funciones o procedimientos recursivos

3.

Page 58: Analisis de Sistemas Computarizados (Libro)

3.6. Árboles

Un árbol es una estructura no lineal en la que cada nodo puede apuntar a uno o varios nodos. También se suele dar una definición recursiva: un árbol es una estructura en compuesta por un dato y varios árboles.

Se definen varios conceptos en relación con otros nodos:

Nodo hijo: cualquiera de los nodos apuntados por uno de los nodos del árbol. En el ejemplo, 'L' y 'M' son hijos de 'G'.

Nodo padre: nodo que contiene un puntero al nodo actual. En el ejemplo, el nodo 'A' es padre de 'B', 'C' y 'D'.

Los árboles con los que se trabajaran tienen otra característica importante: cada nodo sólo puede ser apuntado por otro nodo, es decir, cada nodo sólo tendrá un padre. Esto hace que estos árboles estén fuertemente jerarquizados, y es lo que en realidad les da la apariencia de árboles. En cuanto a la posición dentro del árbol:

Nodo raíz: nodo que no tiene padre. Este es el nodo que usaremos para referirnos al árbol. En el ejemplo, ese nodo es el 'A'.

Nodo hoja: nodo que no tiene hijos. En el ejemplo hay varios: 'F', 'H', 'I', 'K', 'L', 'M', 'N' y 'O'. Nodo rama: aunque esta definición apenas la usaremos, estos son los nodos que no pertenecen a ninguna de

las dos categorías anteriores. En el ejemplo: 'B', 'C', 'D', 'E', 'G' y 'J'.

Otra característica que normalmente tendrán los árboles es que todos los nodos contengan el mismo número de punteros, es decir, se usará la misma estructura para todos los nodos del árbol. Esto hace que la estructura sea más sencilla, y por lo tanto también los programas para trabajar con ellos.

Tampoco es necesario que todos los nodos hijos de un nodo concreto existan. Es decir, que pueden usarse todos, algunos o ninguno de los punteros de cada nodo. Un árbol en el que en cada nodo o bien todos o ninguno de los hijos existe, se llama árbol completo. En otras palabras un árbol completo se caracteriza porque todos sus nodos terminales tienen la misma altura.

Los árboles se parecen al resto de las estructuras que se han visto dado que un nodo cualquiera de la estructura, podemos considerarlo como una estructura independiente. Es decir, un nodo cualquiera puede ser considerado como la raíz de un árbol completo. Existen otros conceptos que definen las características del árbol, en relación a su tamaño:

Orden: es el número potencial de hijos que puede tener cada elemento de árbol. De este modo, diremos que un árbol en el que cada nodo puede apuntar a otros dos es de orden dos, si puede apuntar a tres será de orden tres, etc.

Grado: el número de hijos que tiene el elemento con más hijos dentro del árbol. En el árbol del ejemplo, el grado es tres, ya que tanto 'A' como 'D' tienen tres hijos, y no existen elementos con más de tres hijos.

Nivel: se define para cada elemento del árbol como la distancia a la raíz, medida en nodos. El nivel de la raíz es cero y el de sus hijos uno. Así sucesivamente. En el ejemplo, el nodo 'D' tiene nivel 1, el nodo 'G' tiene nivel 2, y el nodo 'N', nivel 3.

Altura: la altura de un árbol se define como el nivel del nodo de mayor nivel. Como cada nodo de un árbol puede considerarse a su vez como la raíz de un árbol, también podemos hablar de altura de ramas. El árbol del ejemplo tiene altura 3, la rama 'B' tiene altura 2, la rama 'G' tiene altura 1, la 'H' cero, etc.

Los árboles de orden dos son bastante especiales, estos árboles se conocen también como árboles binarios.

Page 59: Analisis de Sistemas Computarizados (Libro)

Frecuentemente, aunque tampoco es estrictamente necesario, para hacer más fácil moverse a través del árbol, añadiremos un puntero a cada nodo que apunte al nodo padre. De este modo podremos avanzar en dirección a la raíz, y no sólo hacia las hojas. Es importante conservar siempre el nodo raíz ya que es el nodo a partir del cual se desarrolla el árbol, si perdemos este nodo, perderemos el acceso a todo el árbol. El nodo típico de un árbol difiere de los nodos que hemos visto hasta ahora para listas, aunque sólo en el número de nodos. Veamos un ejemplo de nodo para crear árboles de orden tres utilizando estructuras:

struct nodo { int dato; struct nodo *rama1; struct nodo *rama2; struct nodo *rama3;};

O generalizando más:

#define ORDEN 5

struct nodo { int dato; struct nodo *rama[ORDEN];};

Declaración de un árbol en C++ utilizando clases

class Nodo { public: // Constructor: Nodo(const int dat, Nodo *izq=NULL, Nodo *der=NULL) : dato(dat), izquierdo(izq), derecho(der) {} // Miembros: int dato; Nodo *izquierdo; Nodo *derecho; };

El movimiento a través de árboles, salvo que se implementen punteros al nodo padre, será siempre partiendo del nodo raíz hacia un nodo hoja. Cada vez que se llegue a un nuevo nodo se podrá optar por cualquiera de los nodos a los que apunta para avanzar al siguiente nodo.

Operaciones básicas con árboles

El repertorio de operaciones de las que disponen los árboles son las siguientes:

Añadir o insertar elementos. Buscar o localizar elementos. Borrar elementos. Moverse a través del árbol. Recorrer el árbol completo.

Los algoritmos de inserción y borrado dependen en gran medida del tipo de árbol que estemos implementando, de modo que por ahora los pasaremos por alto y nos centraremos más en el modo de recorrer árboles.

Page 60: Analisis de Sistemas Computarizados (Libro)

Recorridos por árboles

El modo evidente de moverse a través de las ramas de un árbol es siguiendo los punteros. Esos recorridos dependen en gran medida del tipo y propósito del árbol, pero hay ciertos recorridos que se usan frecuentemente. Se trata de aquellos recorridos que incluyen todo el árbol.

Hay tres formas de recorrer un árbol completo, y las tres se suelen implementar mediante recursividad. En los tres casos se sigue siempre a partir de cada nodo todas las ramas una por una. Supongamos que tenemos un árbol de orden tres, y queremos recorrerlo por completo.

Partiremos del nodo raíz:RecorrerArbol(raiz);

La función RecorrerArbol, aplicando recursividad, será tan sencilla como invocar de nuevo a la función RecorrerArbol para cada una de las ramas:

void RecorrerArbol(Arbol a) { if(a == NULL) return; RecorrerArbol(a->rama[0]); RecorrerArbol(a->rama[1]); RecorrerArbol(a->rama[2]);}

Lo que diferencia los distintos métodos de recorrer el árbol no es el sistema de hacerlo, sino el momento que elegimos para procesar el valor de cada nodo con relación a los recorridos de cada una de las ramas. Los tres tipos son:

Pre-ordenEn este tipo de recorrido, el valor del nodo se procesa antes de recorrer las ramas:

void PreOrden(Arbol a) { if(a == NULL) return; Procesar(dato); RecorrerArbol(a->rama[0]); RecorrerArbol(a->rama[1]); RecorrerArbol(a->rama[2]);}

Si seguimos el árbol del ejemplo en pre-orden, y el proceso de los datos es sencillamente mostrarlos por pantalla, obtendremos algo así:

A B E K F C G L M D H I J N O

In-ordenEn este tipo de recorrido, el valor del nodo se procesa después de recorrer la primera rama y antes de recorrer la última. Esto tiene más sentido en el caso de árboles binarios, y también cuando existen ORDEN-1 datos, en cuyo caso procesaremos cada dato entre el recorrido de cada dos ramas (este es el caso de los árboles-b):

void InOrden(Arbol a) { if(a == NULL) return; RecorrerArbol(a->rama[0]); Procesar(dato); RecorrerArbol(a->rama[1]); RecorrerArbol(a->rama[2]);}

Page 61: Analisis de Sistemas Computarizados (Libro)

Si seguimos el árbol del ejemplo en in-orden, y el proceso de los datos es sencillamente mostrarlos por pantalla, obtendremos algo así:

K E B F A L G M C H D I N J O

Post-ordenEn este tipo de recorrido, el valor del nodo se procesa después de recorrer todas las ramas:

void PostOrden(Arbol a) { if(a == NULL) return; RecorrerArbol(a->rama[0]); RecorrerArbol(a->rama[1]); RecorrerArbol(a->rama[2]); Procesar(dato);}

Si seguimos el árbol del ejemplo en post-orden, y el proceso de los datos es sencillamente mostrarlos por pantalla, obtendremos algo así:

K E F B L M G C H I N O J D A

Eliminar nodos en un árbol

El proceso general es muy sencillo en este caso, pero con una importante limitación, sólo podemos borrar nodos hoja.El proceso sería el siguiente:

1. Buscar el nodo padre del que queremos eliminar.2. Buscar el puntero del nodo padre que apunta al nodo que queremos borrar.3. Liberar el nodo.4. padre->nodo[i] = NULL;.

Cuando el nodo a borrar no sea un nodo hoja, diremos que hacemos una "poda", y en ese caso eliminaremos el árbol cuya raíz es el nodo a borrar. Se trata de un procedimiento recursivo, aplicamos el recorrido PostOrden, y el proceso será borrar el nodo.

El procedimiento es similar al de borrado de un nodo:1. Buscar el nodo padre del que queremos eliminar.2. Buscar el puntero del nodo padre que apunta al nodo que queremos borrar.3. Podar el árbol cuyo padre es nodo.4. padre->nodo[i] = NULL;.

En el árbol del ejemplo, para podar la rama 'B', recorreremos el subárbol 'B' en postorden, eliminando cada nodo cuando se procese, de este modo no perdemos los punteros a las ramas apuntadas por cada nodo, ya que esas ramas se borrarán antes de eliminar el nodo. De modo que el orden en que se borrarán los nodos será:

K E F y B

Árboles ordenados

Sólo se hablará de árboles ordenados, ya que son los que tienen más interés desde el punto de vista de TDA, y los que tienen más aplicaciones genéricas.

Un árbol ordenado, en general, es aquel a partir del cual se puede obtener una secuencia ordenada siguiendo uno de los recorridos posibles del árbol: inorden, preorden o postorden. En estos árboles es importante que la secuencia se mantenga ordenada aunque se añadan o se eliminen nodos. Existen varios tipos de árboles ordenados, que veremos a continuación:

Árboles binarios de búsqueda (ABB): son árboles de orden 2 que mantienen una secuencia ordenada si se recorren en inorden.

Árboles AVL: son árboles binarios de búsqueda equilibrados, es decir, los niveles de cada rama para cualquier nodo no difieren en más de 1.

Árboles perfectamente equilibrados: son árboles binarios de búsqueda en los que el número de nodos de cada rama para cualquier nodo no difieren en más de 1. Son por lo tanto árboles AVL también.

Árboles 2-3: son árboles de orden 3, que contienen dos claves en cada nodo y que están también equilibrados. También generan secuencias ordenadas al recorrerlos en inorden.

Árboles-B: caso general de árboles 2-3, que para un orden M, contienen M-1 claves.

Page 62: Analisis de Sistemas Computarizados (Libro)

3.7. Arboles binarios

Un árbol binario es una estructura de datos en la cual cada nodo puede tener un hijo izquierdo y un hijo derecho. No pueden tener más de dos hijos (de ahí el nombre "binario"). Si algún hijo tiene como referencia a null, es decir que no almacena ningún dato, entonces este es llamado un nodo externo. En el caso contrario el hijo es llamado un nodo interno. Usos comunes de los árboles binarios son los árboles binarios de búsqueda, los montículos binarios y Codificación de Huffman.

Un árbol binario es un árbol en el que ningún nodo puede tener más de dos subárboles. En un árbol binario cada nodo puede tener cero, uno o dos hijos (subárboles). Se conoce el nodo de la izquierda como hijo izquierdo y el nodo de la derecha como hijo derecho.

Árbol binario de búsqueda (ABB)

Se trata de árboles de orden 2 en los que se cumple que para cada nodo, el valor de la clave de la raíz del subárbol izquierdo es menor que el valor de la clave del nodo y que el valor de la clave raíz del subárbol derecho es mayor que el valor de la clave del nodo.

Un árbol binario de búsqueda también llamados BST (acrónimo del inglés Binary Search Tree) es un tipo particular de árbol binario que presenta una estructura de datos en forma de árbol usada en informática.

Un árbol binario no vacío, de raíz R, es un árbol binario de búsqueda si:

En caso de tener subárbol izquierdo, la raíz R debe ser mayor que el valor máximo almacenado en el subárbol izquierdo, y que el subárbol izquierdo sea un árbol binario de búsqueda.

En caso de tener subárbol derecho, la raíz R debe ser menor que el valor mínimo almacenado en el subárbol derecho, y que el subárbol derecho sea un árbol binario de búsqueda.

Para una fácil comprensión queda resumido en que es un árbol binario que cumple que el subárbol izquierdo de cualquier nodo (si no está vacío) contiene valores menores que el que contiene dicho nodo, y el subárbol derecho (si no está vacío) contiene valores mayores.

Operaciones en ABB

Las operaciones que se pueden realizar sobre un ABB son parecidas a las que se realizan sobre otras estructuras de datos, más alguna otra propia de árboles:

Buscar un elemento. Insertar un elemento. Borrar un elemento. Movimientos a través del árbol:

o Izquierda.o Derecha.o Raíz.

Información:o Comprobar si un árbol está vacío.

Page 63: Analisis de Sistemas Computarizados (Libro)

o Calcular el número de nodos.o Comprobar si el nodo es hoja.o Calcular la altura de un nodo.o Calcular la altura de un árbol.

Buscar un elemento

Partiendo siempre del nodo raíz, el modo de buscar un elemento se define de forma recursiva.

Si el árbol está vacío, terminamos la búsqueda: el elemento no está en el árbol. Si el valor del nodo raíz es igual que el del elemento que buscamos, terminamos la búsqueda con éxito. Si el valor del nodo raíz es mayor que el elemento que buscamos, continuaremos la búsqueda en el árbol

izquierdo. Si el valor del nodo raíz es menor que el elemento que buscamos, continuaremos la búsqueda en el árbol

derecho. El valor de retorno de una función de búsqueda en un ABB puede ser un puntero al nodo encontrado, o NULL, si

no se ha encontrado.

Insertar un elemento

Para insertar un elemento nos basamos en el algoritmo de búsqueda. Si el elemento está en el árbol no lo insertaremos. Si no lo está, lo insertaremos a continuación del último nodo visitado.Necesitamos un puntero auxiliar para conservar una referencia al padre del nodo raíz actual. El valor inicial para ese puntero es NULL.

Padre = NULL nodo = Raiz Bucle: mientras actual no sea un árbol vacío o hasta que se encuentre el elemento.

o Si el valor del nodo raíz es mayor que el elemento que buscamos, continuaremos la búsqueda en el árbol izquierdo: Padre=nodo, nodo=nodo->izquierdo.

o Si el valor del nodo raíz es menor que el elemento que buscamos, continuaremos la búsqueda en el árbol derecho: Padre=nodo, nodo=nodo->derecho.

Si nodo no es NULL, el elemento está en el árbol, por lo tanto salimos. Si Padre es NULL, el árbol estaba vacío, por lo tanto, el nuevo árbol sólo contendrá el nuevo elemento, que será

la raíz del árbol. Si el elemento es menor que el Padre, entonces insertamos el nuevo elemento como un nuevo árbol izquierdo de

Padre. Si el elemento es mayor que el Padre, entonces insertamos el nuevo elemento como un nuevo árbol derecho de

Padre.

Este modo de actuar asegura que el árbol sigue siendo ABB.

Borrar un elemento

Para borrar un elemento también nos basamos en el algoritmo de búsqueda. Si el elemento no está en el árbol no lo podremos borrar. Si está, hay dos casos posibles:

1. Se trata de un nodo hoja: en ese caso lo borraremos directamente.2. Se trata de un nodo rama: en ese caso no podemos eliminarlo, puesto que perderíamos todos los elementos del

árbol de que el nodo actual es padre. En su lugar buscamos el nodo más a la izquierda del subárbol derecho, o el más a la derecha del subárbol izquierdo e intercambiamos sus valores. A continuación eliminamos el nodo hoja.

Necesitamos un puntero auxiliar para conservar una referencia al padre del nodo raíz actual. El valor inicial para ese puntero es NULL.

Padre = NULL Si el árbol está vacío: el elemento no está en el árbol, por lo tanto salimos sin eliminar ningún elemento. Si el valor del nodo raíz es igual que el del elemento que buscamos, estamos ante uno de los siguientes casos:

Page 64: Analisis de Sistemas Computarizados (Libro)

o El nodo raíz es un nodo hoja: Si 'Padre' es NULL, el nodo raíz es el único del árbol, por lo tanto el puntero al árbol debe ser

NULL. Si raíz es la rama derecha de 'Padre', hacemos que esa rama apunte a NULL. Si raíz es la rama izquierda de 'Padre', hacemos que esa rama apunte a NULL. Eliminamos el nodo, y salimos.

o El nodo no es un nodo hoja: Buscamos el 'nodo' más a la izquierda del árbol derecho de raíz o el más a la derecha del árbol

izquierdo. Hay que tener en cuenta que puede que sólo exista uno de esos árboles. Al mismo tiempo, actualizamos 'Padre' para que apunte al padre de 'nodo'.

Intercambiamos los elementos de los nodos raíz y 'nodo'. Borramos el nodo 'nodo'. Esto significa volver a (1), ya que puede suceder que 'nodo' no sea un

nodo hoja. (Ver ejemplo 3) Si el valor del nodo raíz es mayor que el elemento que buscamos, continuaremos la búsqueda en el árbol

izquierdo. Si el valor del nodo raíz es menor que el elemento que buscamos, continuaremos la búsqueda en el árbol

derecho.

Ejemplo 1: Borrar un nodo hojaEn el árbol de ejemplo, borrar el nodo 3.

1. Localizamos el nodo a borrar, al tiempo que mantenemos un puntero a 'Padre'.2. Hacemos que el puntero de 'Padre' que apuntaba a 'nodo', ahora apunte a NULL.3. Borramos el 'nodo'.

Ejemplo 2: Borrar un nodo rama con intercambio de un nodo hoja.En el árbol de ejemplo, borrar el nodo 4.

1. Localizamos el nodo a borrar ('raíz').2. Buscamos el nodo más a la derecha del árbol izquierdo de 'raíz', en este caso el 3, al tiempo que mantenemos

un puntero a 'Padre' a 'nodo'.3. Intercambiamos los elementos 3 y 4.4. Hacemos que el puntero de 'Padre' que apuntaba a 'nodo', ahora apunte a NULL.5. Borramos el 'nodo'.

Page 65: Analisis de Sistemas Computarizados (Libro)

Ejemplo 3: Borrar un nodo rama con intercambio de un nodo rama.Para este ejemplo usaremos otro árbol. En éste borraremos el elemento 6.

1. Localizamos el nodo a borrar ('raíz').2. Buscamos el nodo más a la izquierda del árbol derecho de 'raíz', en este caso el 12, ya que el árbol derecho no

tiene nodos a su izquierda, si optamos por la rama izquierda, estaremos en un caso análogo. Al mismo tiempo que mantenemos un puntero a 'Padre' a 'nodo'.

3. Intercambiamos los elementos 6 y 12.4. Ahora tenemos que repetir el bucle para el nodo 6 de nuevo, ya que no podemos eliminarlo.

5. Localizamos de nuevo el nodo a borrar ('raíz').6. Buscamos el nodo más a la izquierda del árbol derecho de 'raíz', en este caso el 16, al mismo tiempo que

mantenemos un puntero a 'Padre' a 'nodo'.7. Intercambiamos los elementos 6 y 16.8. Hacemos que el puntero de 'Padre' que apuntaba a 'nodo', ahora apunte a NULL.9. Borramos el 'nodo'.

Page 66: Analisis de Sistemas Computarizados (Libro)

Este modo de actuar asegura que el árbol sigue siendo ABB.

Ejemplo de código en C++ de un árbol binario de búsqueda con sus operaciones:

#include <iostream>using namespace std;

class ArbolABB { private: //// Clase local de Lista para Nodo de ArbolBinario: class Nodo { public: // Constructor: Nodo(const int dat, Nodo *izq=NULL, Nodo *der=NULL) : dato(dat), izquierdo(izq), derecho(der) {} // Miembros: int dato; Nodo *izquierdo; Nodo *derecho; };

// Punteros de la lista, para cabeza y nodo actual: Nodo *raiz; Nodo *actual; int contador; int altura;

public: // Constructor y destructor básicos: ArbolABB() : raiz(NULL), actual(NULL) {} ~ArbolABB() { Podar(raiz); } // Insertar en árbol ordenado: void Insertar(const int dat); // Borrar un elemento del árbol: void Borrar(const int dat); // Función de búsqueda: bool Buscar(const int dat); // Comprobar si el árbol está vacío: bool Vacio(Nodo *r) { return r==NULL; } // Comprobar si es un nodo hoja: bool EsHoja(Nodo *r) { return !r->derecho && !r->izquierdo; } // Contar número de nodos: const int NumeroNodos(); const int AlturaArbol(); // Calcular altura de un int: int Altura(const int dat); // Devolver referencia al int del nodo actual: int &ValorActual() { return actual->dato; } // Moverse al nodo raiz: void Raiz() { actual = raiz; } // Aplicar una función a cada elemento del árbol: void InOrden(void (*func)(int&) , Nodo *nodo=NULL, bool r=true); void PreOrden(void (*func)(int&) , Nodo *nodo=NULL, bool r=true); void PostOrden(void (*func)(int&) , Nodo *nodo=NULL, bool r=true);

Page 67: Analisis de Sistemas Computarizados (Libro)

private: // Funciones auxiliares void Podar(Nodo* &); void auxContador(Nodo*); void auxAltura(Nodo*, int);};

// Poda: borrar todos los nodos a partir de uno, incluidovoid ArbolABB::Podar(Nodo* &nodo){ // Algoritmo recursivo, recorrido en postorden if(nodo) { Podar(nodo->izquierdo); // Podar izquierdo Podar(nodo->derecho); // Podar derecho delete nodo; // Eliminar nodo nodo = NULL; }}

// Insertar un int en el árbol ABBvoid ArbolABB::Insertar(const int dat){ Nodo *padre = NULL;

actual = raiz; // Buscar el int en el árbol, manteniendo un puntero al nodo padre while(!Vacio(actual) && dat != actual->dato) { padre = actual; if(dat > actual->dato) actual = actual->derecho; else if(dat < actual->dato) actual = actual->izquierdo; }

// Si se ha encontrado el elemento, regresar sin insertar if(!Vacio(actual)) return; // Si padre es NULL, entonces el árbol estaba vacío, el nuevo nodo será // el nodo raiz if(Vacio(padre)) raiz = new Nodo(dat); // Si el int es menor que el que contiene el nodo padre, lo insertamos // en la rama izquierda else if(dat < padre->dato) padre->izquierdo = new Nodo(dat); // Si el int es mayor que el que contiene el nodo padre, lo insertamos // en la rama derecha else if(dat > padre->dato) padre->derecho = new Nodo(dat);}

// Eliminar un elemento de un árbol ABBvoid ArbolABB::Borrar(const int dat){ Nodo *padre = NULL; Nodo *nodo; int aux;

actual = raiz; // Mientras sea posible que el valor esté en el árbol while(!Vacio(actual)) { if(dat == actual->dato) { // Si el valor está en el nodo actual if(EsHoja(actual)) { // Y si además es un nodo hoja: lo borramos if(padre) // Si tiene padre (no es el nodo raiz) // Anulamos el puntero que le hace referencia if(padre->derecho == actual) padre->derecho = NULL; else if(padre->izquierdo == actual) padre->izquierdo = NULL; delete actual; // Borrar el nodo actual = NULL; return; } else { // Si el valor está en el nodo actual, pero no es hoja // Buscar nodo padre = actual; // Buscar nodo más izquierdo de rama derecha

Page 68: Analisis de Sistemas Computarizados (Libro)

if(actual->derecho) { nodo = actual->derecho; while(nodo->izquierdo) { padre = nodo; nodo = nodo->izquierdo; } } // O buscar nodo más derecho de rama izquierda else { nodo = actual->izquierdo; while(nodo->derecho) { padre = nodo; nodo = nodo->derecho; } } // Intercambiar valores de no. a borrar u nodo encontrado // y continuar, cerrando el bucle. El nodo encontrado no tiene // por qué ser un nodo hoja, cerrando el bucle nos aseguramos // de que sólo se eliminan nodos hoja. aux = actual->dato; actual->dato = nodo->dato; nodo->dato = aux; actual = nodo; } } else { // Todavía no hemos encontrado el valor, seguir buscándolo padre = actual; if(dat > actual->dato) actual = actual->derecho; else if(dat < actual->dato) actual = actual->izquierdo; } }}

// Recorrido de árbol en inorden, aplicamos la función func, que tiene// el prototipo:// void func(int&);void ArbolABB::InOrden(void (*func)(int&) , Nodo *nodo, bool r){ if(r) nodo = raiz; if(nodo->izquierdo) InOrden(func, nodo->izquierdo, false); func(nodo->dato); if(nodo->derecho) InOrden(func, nodo->derecho, false);}

// Recorrido de árbol en preorden, aplicamos la función func, que tiene// el prototipo:// void func(int&);void ArbolABB::PreOrden(void (*func)(int&), Nodo *nodo, bool r){ if(r) nodo = raiz; func(nodo->dato); if(nodo->izquierdo) PreOrden(func, nodo->izquierdo, false); if(nodo->derecho) PreOrden(func, nodo->derecho, false);}

// Recorrido de árbol en postorden, aplicamos la función func, que tiene// el prototipo:// void func(int&);void ArbolABB::PostOrden(void (*func)(int&), Nodo *nodo, bool r){ if(r) nodo = raiz; if(nodo->izquierdo) PostOrden(func, nodo->izquierdo, false); if(nodo->derecho) PostOrden(func, nodo->derecho, false); func(nodo->dato);}

// Buscar un valor en el árbolbool ArbolABB::Buscar(const int dat){

Page 69: Analisis de Sistemas Computarizados (Libro)

actual = raiz;

// Todavía puede aparecer, ya que quedan nodos por mirar while(!Vacio(actual)) { if(dat == actual->dato) return true; // int encontrado else if(dat > actual->dato) actual = actual->derecho; // Seguir else if(dat < actual->dato) actual = actual->izquierdo; } return false; // No está en árbol}

// Calcular la altura del nodo que contiene el int datint ArbolABB::Altura(const int dat){ int altura = 0; actual = raiz;

// Todavía puede aparecer, ya que quedan nodos por mirar while(!Vacio(actual)) { if(dat == actual->dato) return altura; // int encontrado else { altura++; // Incrementamos la altura, seguimos buscando if(dat > actual->dato) actual = actual->derecho; else if(dat < actual->dato) actual = actual->izquierdo; } } return -1; // No está en árbol}

// Contar el número de nodosconst int ArbolABB::NumeroNodos(){ contador = 0;

auxContador(raiz); // FUnción auxiliar return contador;}

// Función auxiliar para contar nodos. Función recursiva de recorrido en// preorden, el proceso es aumentar el contadorvoid ArbolABB::auxContador(Nodo *nodo){ contador++; // Otro nodo // Continuar recorrido if(nodo->izquierdo) auxContador(nodo->izquierdo); if(nodo->derecho) auxContador(nodo->derecho);}

// Calcular la altura del árbol, que es la altura del nodo de mayor altura.const int ArbolABB::AlturaArbol(){ altura = 0;

auxAltura(raiz, 0); // Función auxiliar return altura;}

// Función auxiliar para calcular altura. Función recursiva de recorrido en// postorden, el proceso es actualizar la altura sólo en nodos hojas de mayor// altura de la máxima actualvoid ArbolABB::auxAltura(Nodo *nodo, int a){ // Recorrido postorden if(nodo->izquierdo) auxAltura(nodo->izquierdo, a+1); if(nodo->derecho) auxAltura(nodo->derecho, a+1); // Proceso, si es un nodo hoja, y su altura es mayor que la actual del // árbol, actualizamos la altura actual del árbol if(EsHoja(nodo) && a > altura) altura = a;}

Page 70: Analisis de Sistemas Computarizados (Libro)

// Función de prueba para recorridos del árbolvoid Mostrar(int &d){ cout << d << ",";}

int main(){ // Un árbol de enteros ArbolABB ArbolInt;

// Inserción de nodos en árbol: ArbolInt.Insertar(10); ArbolInt.Insertar(5); ArbolInt.Insertar(12); ArbolInt.Insertar(4); ArbolInt.Insertar(7); ArbolInt.Insertar(3); ArbolInt.Insertar(6); ArbolInt.Insertar(9); ArbolInt.Insertar(8); ArbolInt.Insertar(11); ArbolInt.Insertar(14); ArbolInt.Insertar(13); ArbolInt.Insertar(2); ArbolInt.Insertar(1); ArbolInt.Insertar(15); ArbolInt.Insertar(10); ArbolInt.Insertar(17); ArbolInt.Insertar(18); ArbolInt.Insertar(16); cout << "Ejemplo de Arbol Binario de Búsqueda en C++ " << ArbolInt.AlturaArbol() << endl; cout << "\n\nAltura de arbol: " << ArbolInt.AlturaArbol() << endl;

// Mostrar el árbol en tres ordenes distintos: cout << "InOrden: "; ArbolInt.InOrden(Mostrar); cout << endl; cout << "PreOrden: "; ArbolInt.PreOrden(Mostrar); cout << endl; cout << "PostOrden: "; ArbolInt.PostOrden(Mostrar); cout << endl;

// Borraremos algunos elementos: cout << "Numero de nodos: " << ArbolInt.NumeroNodos() << endl; ArbolInt.Borrar(5); cout << "\nBorrar 5: "; ArbolInt.InOrden(Mostrar); cout << endl; ArbolInt.Borrar(8); cout << "Borrar 8: "; ArbolInt.InOrden(Mostrar); cout << endl; ArbolInt.Borrar(15); cout << "Borrar 15: "; ArbolInt.InOrden(Mostrar); cout << endl; ArbolInt.Borrar(245); cout << "Borrar 245: "; ArbolInt.InOrden(Mostrar); cout << endl; ArbolInt.Borrar(4); cout << "Borrar 4: "; ArbolInt.InOrden(Mostrar); ArbolInt.Borrar(17); cout << endl;

Page 71: Analisis de Sistemas Computarizados (Libro)

cout << "Borrar 17: "; ArbolInt.InOrden(Mostrar); cout << endl;

// Veamos algunos parámetros cout << "Numero nodos: " << ArbolInt.NumeroNodos() << endl; cout << "Altura de 1: " << ArbolInt.Altura(1) << endl; cout << "Altura de 10: " << ArbolInt.Altura(10) << endl; cout << "Altura de arbol: " << ArbolInt.AlturaArbol() << endl; cout<<"\n\nRealizado por: David Ortiz\n"; cin.get(); return 0;}

Page 72: Analisis de Sistemas Computarizados (Libro)

UNIDAD 4: MÉTODOS DE ORDENAMIENTO Y

BÚSQUEDA

Page 73: Analisis de Sistemas Computarizados (Libro)

4.4.1. Algoritmos de ordenamiento

Uno de los procedimientos más comunes y útiles en el procesamiento de datos, es la ordenación de los mismos. Se considera ordenar al proceso de reorganizar un conjunto dado de objetos en una secuencia determinada (patrón de arreglo). El objetivo de este proceso generalmente es facilitar la búsqueda de uno o más elementos pertenecientes a un conjunto.

El ordenamiento es una labor común que realizamos continuamente. ¿Pero te has preguntado qué es ordenar? ¿No? Es que es algo tan corriente en nuestras vidas que no nos detenemos a pensar en ello. Ordenar es simplemente colocar información de una manera especial basándonos en un criterio de ordenamiento.

En la computación el ordenamiento de datos también cumple un rol muy importante, ya sea como un fin en sí o como parte de otros procedimientos más complejos. Se han desarrollado muchas técnicas en este ámbito, cada una con características específicas, y con ventajas y desventajas sobre las demás.

Como ejemplos de conjunto de datos ordenados tenemos:

Meses del año (ordenados de 1 al 12). Listado de estudiantes (ordenados alfabéticamente). Guías Telefónicas (ordenadas por País/por región/por sector/por orden alfabético)

La ordenación, tanto numérica como alfanumérica, sigue las mismas reglas que empleamos nosotros en la vida normal. Esto es, un dato numérico es mayor que otro cuando su valor es más grande, y una cadena de caracteres es mayor que otra cuando esta después por orden alfabético.

Antes de comenzar a ver cada algoritmo vamos estudiar algunos conceptos, para que no haya confusiones:

Clave: La parte de un registro por la cual se ordena la lista. Por ejemplo, una lista de registros con campos nombre, dirección y teléfono se puede ordenar alfabéticamente de acuerdo a la clave nombre. En este caso los campos dirección y teléfono no se toman en cuenta en el ordenamiento.

Criterio de ordenamiento (o de comparación): EL criterio que utilizamos para asignar valores a los registros con base en una o más claves. De esta manera decidimos si un registro es mayor o menor que otro. En el pseudocódigo presentado más adelante simplemente se utilizarán los símbolos < y >, para mayor simplicidad.

Registro: Un grupo de datos que forman la lista. Pueden ser datos atómicos (enteros, caracteres, reales, etc.) o grupos de ellos, que en C equivalen a las estructuras.

Cuando se estudian algoritmos de todo tipo, no sólo de ordenamiento, es bueno tener una forma de evaluarlos antes de pasarlos a código, que se base en aspectos independientes de la plataforma o el lenguaje. De esta manera podremos decidir cuál se adapta mejor a los requerimientos de nuestro programa. Así que veamos estos aspectos:

Estabilidad: Cómo se comporta con registros que tienen claves iguales. Algunos algoritmos mantienen el orden relativo entre éstos y otros no. Veamos un ejemplo. Si tenemos la siguiente lista de datos (nombre, edad): "Pedro 19, Juan 23, Felipe 15, Marcela 20, Juan 18, Marcela 17", y la ordenamos alfabéticamente por el nombre con un algoritmo estable quedaría así: "Felipe 15, Marcela 20, Marcela 17, Juan 23, Juan 18, Pedro 19". Un algoritmo no estable podría dejar a Juan 18 antes de Juan 23, o a Marcela 20 después de Marcela 17.

Tiempo de ejecución: La complejidad del algoritmo, que no tiene que ver con dificultad, sino con rendimiento. Es una función independiente de la implementación. Se tiene que identificar una operación fundamental que realice nuestro algoritmo, que en este caso es comparar. Ahora contamos cuántas veces el algoritmo se necesita comparar. Si en una lista de n términos realiza n comparaciones la complejidad es O(n). Algunos ejemplos de complejidades comunes son:

o O(1) : Complejidad constante.o O(n) : Complejidad lineal.o O(n2) : Complejidad cuadrática.o O(log(n)) : Complejidad logarítmica.

Ahora podemos decir que un algoritmo de complejidad O(n) es más rápido que uno de complejidad O(n 2). Otro aspecto a considerar es la diferencia entre el peor y el mejor caso. Cada algoritmo se comporta de modo diferente de acuerdo a cómo se le entregue la información; por eso es conveniente estudiar su comportamiento en casos extremos, como cuando los datos están prácticamente ordenados o muy desordenados.

Page 74: Analisis de Sistemas Computarizados (Libro)

Requerimientos de memoria: El algoritmo puede necesitar memoria adicional para realizar su labor. En general es preferible que no sea así, pero es común en la programación tener que sacrificar memoria por rendimiento.

Hay bastantes otros aspectos que se pueden tener en cuenta. Por último estableceremos algunas convenciones sobre el pseudocódigo:

Vamos a ordenar la lista en forma ascendiente, es decir, de menor a mayor. Obviamente es esencialmente lo mismo que hacerlo en forma inversa.

La forma de intercambiar los elementos depende de la estructura de datos: si es un arreglo (dinámico o estático) es necesario guardar una copia del primer elemento, asignarle el segundo al primero y el temporal al segundo. La variable temporal es necesaria, porque de lo contrario se perdería uno de los elementos. Si la estructura es una lista dinámica el procedimiento es parecido, pero se utilizan las direcciones de los elementos. En el pseudocódigo se utilizará el primer método.

La lista se manejará como un arreglo de C: si tiene TAM elementos, el primer elemento es lista[0] y el último es lista[TAM-1]. Esto será así para todo el pseudocódigo presentado en este artículo.

Los métodos de ordenación, pueden agruparse en dos grandes grupos:

Los internos: Es cuando los datos están disponibles de un área de la memoria principal, como cuando se leen un conjunto de datos desde el teclado.

Los externos: Los datos están guardados en un medio externo, como puede ser un fichero, una base de datos, etc. En donde los datos están alojados en el disco duro u otro medio físico.

Existen varios métodos de ordenamiento pero solo se mencionaran los cuatro modos de ordenamiento más usados los cuales son:

Algoritmo de Ordenamiento Burbuja (Buble Sort).Algoritmo de Ordenamiento por Selección.Algoritmo de Ordenamiento por Inserción.Algoritmo de Ordenamiento Rápido (Quick Sort).

Tabla comparativa de algoritmos

Eligiendo el más adecuado

¿Cómo saber cuál es el que necesitas? ¿Cuál es el algoritmo más adecuado? Cada algoritmo se comporta de modo diferente de acuerdo a la cantidad y la forma en que se le presenten los datos, entre otras cosas. No existe el mejor algoritmo de ordenamiento. Sólo existe el más adecuado para cada caso particular. Debes conocer a fondo el problema que quieres resolver, y aplicar el más adecuado. Aunque hay algunas preguntas que te pueden ayudar a elegir:

¿Qué grado de orden tendrá la información que vas a manejar? Si la información va a estar casi ordenada y no quieres complicarte, un algoritmo sencillo como el ordenamiento burbuja será suficiente. Si por el contrario los datos van a estar muy desordenados, un algoritmo poderoso como Quicksort puede ser el más indicado. Y si no puedes hacer una presunción sobre el grado de orden de la información, lo mejor será elegir un algoritmo que se comporte de manera similar en cualquiera de estos dos casos extremos.¿Qué cantidad de datos vas a manipular? Si la cantidad es pequeña, no es necesario utilizar un algoritmo complejo, y es preferible uno de fácil implementación. Una cantidad muy grande puede hacer prohibitivo utilizar un algoritmo que requiera de mucha memoria adicional.¿Qué tipo de datos quieres ordenar? Algunos algoritmos sólo funcionan con un tipo específico de datos (enteros, enteros positivos, etc.) y otros son generales, es decir, aplicables a cualquier tipo de dato.¿Qué tamaño tienen los registros de tu lista? Algunos algoritmos realizan múltiples intercambios (burbuja, inserción). Si los registros son de gran tamaño estos intercambios son más lentos.

Page 75: Analisis de Sistemas Computarizados (Libro)

ORDENAMIENTO BURBUJA (BUBBLESORT)

Este es el algoritmo más sencillo, ideal para empezar a entender los ordenamientos. Consiste en ciclar repetidamente a través de la lista, comparando elementos adyacentes de dos en dos. Si un elemento es mayor que el que está en la siguiente posición se intercambian.

Pseudocódigo en C++.

Tabla de variables

1. for (i=1; i<TAM; i++) 2. for j=0 ; j<TAM - 1; j++) 3. if (lista[j] > lista[j+1]) 4. temp = lista[j]; 5. lista[j] = lista[j+1]; 6. lista[j+1] = temp;

Ejemplo

Vamos a ver un ejemplo. Esta es nuestra lista:4 - 3 - 5 - 2 – 1

Tenemos 5 elementos. Es decir, TAM toma el valor 5. Comenzamos comparando el primero con el segundo elemento. 4 es mayor que 3, así que intercambiamos. Ahora tenemos:3 - 4 - 5 - 2 – 1

Ahora comparamos el segundo con el tercero: 4 es menor que 5, así que no hacemos nada. Continuamos con el tercero y el cuarto: 5 es mayor que 2. Intercambiamos y obtenemos:3 - 4 - 2 - 5 – 1

Comparamos el cuarto y el quinto: 5 es mayor que 1. Intercambiamos nuevamente:3 - 4 - 2 - 1 – 5

Repitiendo este proceso vamos obteniendo los siguientes resultados:3 - 2 - 1 - 4 - 52 - 1 - 3 - 4 - 51 - 2 - 3 - 4 – 5

Optimizando.Se pueden realizar algunos cambios en este algoritmo que pueden mejorar su rendimiento:

Si observas bien, te darás cuenta que en cada pasada a través de la lista un elemento va quedando en su posición final. Si no te queda claro mira el ejemplo de arriba. En la primera pasada el 5 (elemento mayor) quedó en la última posición, en la segunda el 4 (el segundo mayor elemento) quedó en la penúltima posición. Podemos evitar hacer comparaciones innecesarias si disminuimos el número de éstas en cada pasada. Tan sólo hay que cambiar el ciclo interno de esta manera:for (j=0; j<TAM - i; j++)

Puede ser que los datos queden ordenados antes de completar el ciclo externo. Podemos modificar el algoritmo para que verifique si se han realizado intercambios. Si no se han hecho entonces terminamos con la ejecución, pues eso significa que los datos ya están ordenados. Te dejo como tarea que modifiques el algoritmo para hacer esto :-).

Page 76: Analisis de Sistemas Computarizados (Libro)

Otra forma es ir guardando la última posición en que se hizo un intercambio, y en la siguiente pasada sólo comparar hasta antes de esa posición.

Análisis del algoritmo.

Éste es el análisis para la versión no optimizada del algoritmo: Estabilidad: Este algoritmo nunca intercambia registros con claves iguales. Por lo tanto es estable. Requerimientos de Memoria: Este algoritmo sólo requiere de una variable adicional para realizar los

intercambios. Tiempo de Ejecución: El ciclo interno se ejecuta n veces para una lista de n elementos. El ciclo externo también

se ejecuta n veces. Es decir, la complejidad es n * n = O(n2). El comportamiento del caso promedio depende del orden de entrada de los datos, pero es sólo un poco mejor que el del peor caso, y sigue siendo O(n2).

Ventajas: Fácil implementación. No requiere memoria adicional.

Desventajas: Muy lento. Realiza numerosas comparaciones. Realiza numerosos intercambios.

En resumen este algoritmo es uno de los más pobres en rendimiento y no es muy recomendable usarlo.

ORDENAMIENTO POR SELECCIÓN

Este algoritmo también es sencillo. Consiste en lo siguiente:

Buscas el elemento más pequeño de la lista. Lo intercambias con el elemento ubicado en la primera posición de la lista. Buscas el segundo elemento más pequeño de la lista. Lo intercambias con el elemento que ocupa la segunda posición en la lista. Repites este proceso hasta que hayas ordenado toda la lista.

Pseudocódigo en C++.

Tabla de variables

1. for (i=0; i<TAM - 1; i++) 2. pos_men = Menor(lista, TAM, i); 3. temp = lista[i]; 4. lista[i] = lista [pos_men]; 5. lista [pos_men] = temp;

Nota: Menor(lista, TAM, i) es una función que busca el menor elemento entre las posiciones i y TAM-1. La búsqueda es lineal (elemento por elemento). No lo incluyo en el pseudocódigo porque es bastante simple.

Ejemplo.

Vamos a ordenar la siguiente lista (la misma del ejemplo anterior):4 - 3 - 5 - 2 – 1

Page 77: Analisis de Sistemas Computarizados (Libro)

Comenzamos buscando el elemento menor entre la primera y última posición. Es el 1. Lo intercambiamos con el 4 y la lista queda así:1 - 3 - 5 - 2 - 4Ahora buscamos el menor elemento entre la segunda y la última posición. Es el 2. Lo intercambiamos con el elemento en la segunda posición, es decir el 3. La lista queda así:1 - 2 - 5 - 3 – 4

Buscamos el menor elemento entre la tercera posición y la última. Es el 3, que intercambiamos con el 5:1 - 2 - 3 - 5 – 4

El menor elemento entre la cuarta y quinta posición es el 4, que intercambiamos con el 5:1 - 2 - 3 - 4 – 5

Análisis del algoritmo.

Estabilidad: Aquí discrepo con un libro de la bibliografía que dice que no es estable. Yo lo veo así: si tengo dos registros con claves iguales, el que ocupe la posición más baja será el primero que sea identificado como menor. Es decir que será el primero en ser desplazado. El segundo registro será el menor en el siguiente ciclo y quedará en la posición adyacente. Por lo tanto se mantendrá el orden relativo. Lo que podría hacerlo inestable sería que el ciclo que busca el elemento menor revisara la lista desde la última posición hacia atrás. ¿Qué opinas tú? Yo digo que es estable, pero para hacerle caso al libro (el autor debe sabe más que yo ¿cierto?:-)) vamos a decir que no es estable.

Requerimientos de Memoria: Al igual que el ordenamiento burbuja, este algoritmo sólo necesita una variable adicional para realizar los intercambios.

Tiempo de Ejecución: El ciclo externo se ejecuta n veces para una lista de n elementos. Cada búsqueda requiere comparar todos los elementos no clasificados. Luego la complejidad es O(n2). Este algoritmo presenta un comportamiento constante independiente del orden de los datos. Luego la complejidad promedio es también O(n2).

Ventajas: Fácil implementación. No requiere memoria adicional. Realiza pocos intercambios. Rendimiento constante: poca diferencia entre el peor y el mejor caso.

Desventajas: Lento. Realiza numerosas comparaciones.

Este es un algoritmo lento. No obstante, ya que sólo realiza un intercambio en cada ejecución del ciclo externo, puede ser una buena opción para listas con registros grandes y claves pequeñas.

ORDENAMIENTO POR INSERCIÓN

Este algoritmo también es bastante sencillo. ¿Has jugado cartas? ¿Cómo las vas ordenando cuando las recibes? Yo lo hago de esta manera: tomo la primera y la coloco en mi mano. Luego tomo la segunda y la comparo con la que tengo: si es mayor, la pongo a la derecha, y si es menor a la izquierda. Después tomo la tercera y la comparo con las que tengo en la mano, desplazándola hasta que quede en su posición final. Continúo haciendo esto, insertando cada carta en la posición que le corresponde, hasta que las tengo todas en orden. ¿Lo haces así tú también? Bueno, pues si es así entonces comprenderás fácilmente este algoritmo, porque es el mismo concepto.

Para simular esto en un programa necesitamos tener en cuenta algo: no podemos desplazar los elementos así como así o se perderá un elemento. Lo que hacemos es guardar una copia del elemento actual y desplazar todos los elementos mayores hacia la derecha. Luego copiamos el elemento guardado en la posición del último elemento que se desplazó.

Pseudocódigo en C.

Tabla de variables

Page 78: Analisis de Sistemas Computarizados (Libro)

1. for (i=1; i<TAM; i++) 2. temp = lista[i]; 3. j = i - 1; 4. while ( (lista[j] > temp) && (j >= 0) ) 5. lista[j+1] = lista[j]; 6. j--; 7. lista[j+1] = temp;

Nota: Observa que en cada iteración del ciclo externo los elementos 0 a i forman una lista ordenada.

Ejemplo.

Vamos a ordenar la siguiente lista (la misma del ejemplo anterior):4 - 3 - 5 - 2 - 1

temp toma el valor del segundo elemento, 3. La primera carta es el 4. Ahora comparamos: 3 es menor que 4. Luego desplazamos el 4 una posición a la derecha y después copiamos el 3 en su lugar.4 - 4 - 5 - 2 - 13 - 4 - 5 - 2 - 1

El siguiente elemento es 5. Comparamos con 4. Es mayor que 4, así que no ocurren intercambios.Continuamos con el 2. Es menor que cinco: desplazamos el 5 una posición a la derecha:3 - 4 - 5 - 5 - 1

Comparamos con 4: es menor, así que desplazamos el 4 una posición a la derecha:3 - 4 - 4 - 5 - 1

Comparamos con 3. Desplazamos el 3 una posición a la derecha:3 - 3 - 4 - 5 - 1

Finalmente copiamos el 2 en su posición final:2 - 3 - 4 - 5 - 1

El último elemento a ordenar es el 1. Cinco es menor que 1, así que lo desplazamos una posición a la derecha:2 - 3 - 4 - 5 - 5

Continuando con el procedimiento la lista va quedando así:2 - 3 - 4 - 4 - 52 - 3 - 3 - 4 - 52 - 2 - 3 - 4 - 51 - 2 - 3 - 4 - 5

Análisis del algoritmo.

Estabilidad: Este algoritmo nunca intercambia registros con claves iguales. Por lo tanto es estable. Requerimientos de Memoria: Una variable adicional para realizar los intercambios. Tiempo de Ejecución: Para una lista de n elementos el ciclo externo se ejecuta n-1 veces. El ciclo interno se

ejecuta como máximo una vez en la primera iteración, 2 veces en la segunda, 3 veces en la tercera, etc. Esto produce una complejidad O(n2).

Ventajas: Fácil implementación.

Page 79: Analisis de Sistemas Computarizados (Libro)

Requerimientos mínimos de memoria.

Desventajas: Lento. Realiza numerosas comparaciones.

Este también es un algoritmo lento, pero puede ser de utilidad para listas que están ordenadas o semiordenadas, porque en ese caso realiza muy pocos desplazamientos.

ORDENAMIENTO RÁPIDO (QUICK SORT)

Esta es probablemente la técnica más rápida conocida. Fue desarrollada por C.A.R. Hoare en 1960. El algoritmo original es recursivo, pero se utilizan versiones iterativas para mejorar su rendimiento (los algoritmos recursivos son en general más lentos que los iterativos, y consumen más recursos). El algoritmo fundamental es el siguiente:

Eliges un elemento de la lista. Puede ser cualquiera (en Optimizando veremos una forma más efectiva). Lo llamaremos elemento de división.

Buscas la posición que le corresponde en la lista ordenada (explicado más abajo). Acomodas los elementos de la lista a cada lado del elemento de división, de manera que a un lado queden todos

los menores que él y al otro los mayores (explicado más abajo también). En este momento el elemento de división separa la lista en dos sublistas (de ahí su nombre).

Realizas esto de forma recursiva para cada sublista mientras éstas tengan un largo mayor que 1. Una vez terminado este proceso todos los elementos estarán ordenados.

Una idea preliminar para ubicar el elemento de división en su posición final sería contar la cantidad de elementos menores y colocarlo un lugar más arriba. Pero luego habría que mover todos estos elementos a la izquierda del elemento, para que se cumpla la condición y pueda aplicarse la recursividad. Reflexionando un poco más se obtiene un procedimiento mucho más efectivo. Se utilizan dos índices: i, al que llamaremos contador por la izquierda, y j, al que llamaremos contador por la derecha. El algoritmo es éste:

Recorres la lista simultáneamente con i y j: por la izquierda con i (desde el primer elemento), y por la derecha con j (desde el último elemento).

Cuando lista[i] sea mayor que el elemento de división y lista[j] sea menor los intercambias. Repites esto hasta que se crucen los índices. El punto en que se cruzan los índices es la posición adecuada para colocar el elemento de división, porque

sabemos que a un lado los elementos son todos menores y al otro son todos mayores (o habrían sido intercambiados).

Al finalizar este procedimiento el elemento de división queda en una posición en que todos los elementos a su izquierda son menores que él, y los que están a su derecha son mayores.

Tabla de variables

Nombre Procedimiento: OrdRap Parámetros: lista a ordenar (lista) índice inferior (inf) índice superior (sup)

Page 80: Analisis de Sistemas Computarizados (Libro)

// Inicialización de variables 1. elem_div = lista[sup]; 2. i = inf - 1; 3. j = sup; 4. cont = 1; // Verificamos que no se crucen los límites 5. if (inf >= sup) 6. retornar; // Clasificamos la sublista 7. while (cont) 8. while (lista[++i] < elem_div); 9. while (lista[--j] > elem_div); 10. if (i < j) 11. temp = lista[i]; 12. lista[i] = lista[j]; 13. lista[j] = temp; 14. else 15. cont = 0; // Copiamos el elemento de división // en su posición final 16. temp = lista[i]; 17. lista[i] = lista[sup]; 18. lista[sup] = temp; // Aplicamos el procedimiento // recursivamente a cada sublista 19. OrdRap (lista, inf, i - 1); 20. OrdRap (lista, i + 1, sup);

Nota: La primera llamada debería ser con la lista, cero (0) y el tamaño de la lista menos 1 como parámetros.

Ejemplo.

Esta la lista es la siguiente:5 - 3 - 7 - 6 - 2 - 1 - 4Comenzamos con la lista completa. El elemento divisor será el 4:5 - 3 - 7 - 6 - 2 - 1 - 4Comparamos con el 5 por la izquierda y el 1 por la derecha.5 - 3 - 7 - 6 - 2 - 1 - 45 es mayor que cuatro y 1 es menor. Intercambiamos:1 - 3 - 7 - 6 - 2 - 5 - 4Avanzamos por la izquierda y la derecha:1 - 3 - 7 - 6 - 2 - 5 - 43 es menor que 4: avanzamos por la izquierda. 2 es menor que 4: nos mantenemos ahí.1 - 3 - 7 - 6 - 2 - 5 - 47 es mayor que 4 y 2 es menor: intercambiamos.1 - 3 - 2 - 6 - 7 - 5 - 4Avanzamos por ambos lados:1 - 3 - 2 - 6 - 7 - 5 - 4En este momento termina el ciclo principal, porque los índices se cruzaron. Ahora intercambiamos lista[i] con lista[sup] (pasos 16-18):1 - 3 - 2 - 4 - 7 - 5 - 6

Aplicamos recursivamente a la sublista de la izquierda (índices 0 - 2). Tenemos lo siguiente:1 - 3 - 21 es menor que 2: avanzamos por la izquierda. 3 es mayor: avanzamos por la derecha. Como se intercambiaron los índices termina el ciclo. Se intercambia lista[i] con lista[sup]:

Page 81: Analisis de Sistemas Computarizados (Libro)

1 - 2 - 3Al llamar recursivamente para cada nueva sublista (lista[0]-lista[0] y lista[2]-lista[2]) se retorna sin hacer cambios (condición 5.).Para resumir te muestro cómo va quedando la lista:Segunda sublista: lista[4]-lista[6]7 - 5 - 65 - 7 - 65 - 6 - 7

Para cada nueva sublista se retorna sin hacer cambios (se cruzan los índices).Finalmente, al retornar de la primera llamada se tiene el arreglo ordenado:1 - 2 - 3 - 4 - 5 - 6 - 7

Optimizando.

Sólo se van a mencionar algunas optimizaciones que pueden mejorar bastante el rendimiento de quicksort: Hacer una versión iterativa: Para ello se utiliza una pila en que se van guardando los límites superior e inferior de

cada sublista. No clasificar todas las sublistas: Cuando el largo de las sublistas va disminuyendo, el proceso se va

encareciendo. Para solucionarlo sólo se clasifican las listas que tengan un largo menor que n. Al terminar la clasificación se llama a otro algoritmo de ordenamiento que termine la labor. El indicado es uno que se comporte bien con listas casi ordenadas, como el ordenamiento por inserción por ejemplo. La elección de n depende de varios factores, pero un valor entre 10 y 25 es adecuado.

Elección del elemento de división: Se elige desde un conjunto de tres elementos: lista[inferior], lista[mitad] y lista[superior]. El elemento elegido es el que tenga el valor medio según el criterio de comparación. Esto evita el comportamiento degenerado cuando la lista está prácticamente ordenada.

Análisis del algoritmo.

Estabilidad: No es estable. Requerimientos de Memoria: No requiere memoria adicional en su forma recursiva. En su forma iterativa la

necesita para la pila. Tiempo de Ejecución:

o Caso promedio. La complejidad para dividir una lista de n es O(n). Cada sublista genera en promedio dos sublistas más de largo n/2. Por lo tanto la complejidad se define en forma recurrente como:f(1) = 1f(n) = n + 2 f(n/2)La forma cerrada de esta expresión es:f(n) = n log2nEs decir, la complejidad es O(n log2n).

o El peor caso ocurre cuando la lista ya está ordenada, porque cada llamada genera sólo una sublista (todos los elementos son menores que el elemento de división). En este caso el rendimiento se degrada a O(n2). Con las optimizaciones mencionadas arriba puede evitarse este comportamiento.

Ventajas: Muy rápido No requiere memoria adicional.

Desventajas: Implementación un poco más complicada. Recursividad (utiliza muchos recursos). Mucha diferencia entre el peor y el mejor caso.

La mayoría de los problemas de rendimiento se pueden solucionar con las optimizaciones mencionadas. Este es un algoritmo que puedes utilizar en la vida real. Es muy eficiente. En general será la mejor opción.

Ejemplo de código en C++ de los ordenamientos más usados:

#include <iostream>#include <time.h>using namespace std;/* Aprovechamos las funciones de consola si las trae el compilador. EL programa no las necesita, pero queda mejor si se puede borrar la pantalla y esperar a que se presione una tecla */

Page 82: Analisis de Sistemas Computarizados (Libro)

#if defined DJGPP || defined __TURBOC__ || \ defined __BORLAND__ || defined __LCC__ #define __CONIO_SOPORTADO__#endif

#ifdef __CONIO_SOPORTADO__ #include <conio.h> #define PAUSA() \ printf ("Presione una tecla para volver al men£..."); getch() #define LEER_TECLA() getch() #define LIMPIAR_PANTALLA() clrscr()#elif defined __MINGW32__ #define LEER_TECLA() getchar() #define PAUSA() system("PAUSE") #define LIMPIAR_PANTALLA() system("CLS") #else #define LEER_TECLA() getchar() #define PAUSA() getchar() #define LIMPIAR_PANTALLA()#endif

/* El tama¤o del arreglo: s¢lo 20 para que quepa en la pantalla */#define TAM 20/* La opci¢n para salir */#define SALIR '7'/* ---------------------------------------------------------------------- Definición de funciones. ---------------------------------------------------------------------- */void menu(void); /* Presenta el men£ */int leer_opcion(void); /* Lee la opci¢n del usuario */int generar_elementos (int arreglo[]); /* Genera n£meros aleatorios para llenar el arreglo */void mostrar (int arreglo[]); /* Imprime el arreglo */void burbuja (int arreglo[]); /* Ordenamiento burbuja */void seleccion (int arreglo[]); /* Ordenamiento por selecci¢n */int menor (int arreglo[], int desde); /* Utilizada por seleccion(): busca el elemento menor del arreglo */void insercion (int arreglo[]); /* Ordenamiento por inserci¢n */void quicksort (int arreglo[]); /* Ordenamiento r pido */void ord_rap (int arreglo[], int inf, int sup); /* Parte recursiva de quicksort() */void copiar_arr (int *desde, int *hacia); /* Copia desde en hasta *//* ---------------------------------------------------------------------- */int main(){ int arreglo[TAM]; int copia_arreglo[TAM]; char opcion = 0; /* Llenamos el arreglo con n£meros al azar por primera vez */ generar_elementos(arreglo); copiar_arr(arreglo, copia_arreglo); /* Guardamos una copia */ while (opcion != SALIR) { menu(); opcion = leer_opcion(); /* Ejecutamos la funci¢n correspondiente a la opci¢n */ switch (opcion) { case '1': /* Llenamos el arreglo con n£meros al azar */ generar_elementos(arreglo); /* Guardamos una copia */ copiar_arr(arreglo, copia_arreglo); LIMPIAR_PANTALLA(); cout<<"\n\n\tSe generaron los siguientes elementos:\n"; mostrar (arreglo); break; case '2': LIMPIAR_PANTALLA();

Page 83: Analisis de Sistemas Computarizados (Libro)

cout<<"\n\n\tLos elementos del arreglo son:\n"; mostrar (arreglo); break; case '3': burbuja (arreglo); copiar_arr(copia_arreglo, arreglo); /* Restauramos */ break; case '4': seleccion (arreglo); copiar_arr(copia_arreglo, arreglo); /* Restauramos */ break; case '5': insercion (arreglo); copiar_arr(copia_arreglo, arreglo); /* Restauramos */ break; case '6': quicksort (arreglo); copiar_arr(copia_arreglo, arreglo); /* Restauramos */ break; } } return 0;}

void menu(void){ LIMPIAR_PANTALLA(); cout<<"\n\n\t Men£:\n"; cout<<"\n\t1) Generar los elementos en forma aleatoria."; cout<<"\n\t2) Mostrar los elementos."; cout<<"\n\t3) Ordenamiento burbuja."; cout<<"\n\t4) Ordenamiento por selecci¢n."; cout<<"\n\t5) Ordenamiento por inserci¢n."; cout<<"\n\t6) Ordenamiento r pido (QUICKSORT)."; cout<<"\n\t7) Salir."; cout<<"\n\n\t Ingrese su opci¢n por favor: ";}

int leer_opcion(void){ char opcion;

opcion = LEER_TECLA(); while (opcion < '1' || opcion > SALIR) { opcion = LEER_TECLA(); } return opcion;}

int generar_elementos (int arreglo[]){ int i; /* Introducimos una semilla para los n£meros aleatorios */ srand ((unsigned) time(NULL)); for (i=0; i<TAM; i++) arreglo[i] = rand()%TAM; /* S¢lo n£meros peque¤os... */ return TAM;}

void mostrar (int arreglo[]){ int i; for (i=0; i<TAM; i++) cout<<"\n\tElemento"<<i<<" = "<<arreglo[i]; cout<<"\n\n\t"; PAUSA();}

Page 84: Analisis de Sistemas Computarizados (Libro)

void burbuja (int arreglo[]){ int i, j; int temp; for (i=1; i<TAM; i++) for (j=0; j<TAM - i; j++) if (arreglo[j] > arreglo[j+1]) { /* Intercambiamos */ temp = arreglo[j]; arreglo[j] = arreglo[j+1]; arreglo[j+1] = temp; } LIMPIAR_PANTALLA(); cout<<"\n\n\tOrdenamiento burbuja."; cout<<"\n\tEl arreglo ordenado es:\n"; mostrar (arreglo);}

void seleccion (int arreglo[]){ int i; int temp, pos_men; for (i=0; i<TAM - 1; i++) { /* Buscamos el elemento menor */ pos_men = menor(arreglo, i); /* Lo colocamos en el lugar que le corresponde */ temp = arreglo[i]; arreglo[i] = arreglo [pos_men]; arreglo [pos_men] = temp; } LIMPIAR_PANTALLA(); cout<<"\n\n\tOrdenamiento por selecci¢n."; cout<<"\n\tEl arreglo ordenado es:\n"; mostrar (arreglo);}

int menor (int arreglo[], int desde){ int i, menor; menor = desde++; for (i=desde; i<TAM; i++) if (arreglo[i] < arreglo[menor]) menor = i; return menor;}

void insercion (int arreglo[]){ int i, j, temp; for (i=1; i<TAM; i++) { temp = arreglo[i]; j = i - 1; /* Desplazamos los elementos mayores que arreglo[i] */ while ( (arreglo[j] > temp) && (j >= 0) ) { arreglo[j+1] = arreglo[j]; j--; } /* Copiamos arreglo[i] en su posici¢n final */ arreglo[j+1] = temp; } LIMPIAR_PANTALLA(); cout<<"\n\n\tOrdenamiento por inserci¢n."; cout<<"\n\tEl arreglo ordenado es:\n"; mostrar (arreglo);}

Page 85: Analisis de Sistemas Computarizados (Libro)

/* Esta funci¢n es recursiva. La separamos en dos partes: la recursiva y la de impresi¢n de resultados. Si no fuera as¡ cada vez que llamaramos a la funci¢n se imprimir¡a 'ordenado'(aunque no est‚ ordenado). */void quicksort (int arreglo[]){ ord_rap (arreglo, 0, TAM - 1); LIMPIAR_PANTALLA(); cout<<"\n\n\tOrdenamiento r pido (QUICKSORT)."; cout<<"\n\tEl arreglo ordenado es:\n"; mostrar (arreglo);}

void ord_rap (int arreglo[], int inf, int sup){ int elem_div = arreglo[sup]; int temp ; int i = inf - 1, j = sup; int cont = 1; if (inf >= sup) /* ¨Se cruzaron los ¡ndices ? */ return; while (cont) { while (arreglo[++i] < elem_div); while (arreglo[--j] > elem_div); /* ¨Se cumple la condici¢n ? */ if (i < j) { temp = arreglo[i]; arreglo[i] = arreglo[j]; arreglo[j] = temp; } else cont = 0; } /* Dejamos el elemento de divisi¢n en su posici¢n final */ temp = arreglo[i]; arreglo[i] = arreglo[sup]; arreglo[sup] = temp; /* Aplicamos recursivamente a los subarreglos generados */ ord_rap (arreglo, inf, i - 1); ord_rap (arreglo, i + 1, sup);}

void copiar_arr (int *desde, int *hacia){ int i; if ( (desde == NULL) || (hacia == NULL) ) return; for (i=0; i<TAM; i++) hacia[i] =desde[i];}

Page 86: Analisis de Sistemas Computarizados (Libro)

4.2. Métodos de búsqueda

La búsqueda de información está relacionada con las tablas para consultas. Estas tablas contienen una cantidad de información que se almacenan en forma de listas de parejas de datos. Por ejemplo un catálogo con una lista de libros de matemáticas, en donde es necesario buscar con frecuencia elementos en una lista.

Existen diferentes tipos de búsqueda, pero los más utilizados son:

Método de búsqueda SecuencialMétodo de búsqueda Binaria.

METODO DE BUSQUEDA SECUENCIAL

La búsqueda lineal probablemente es sencilla de implementar e intuitiva. Básicamente consiste en buscar de manera secuencial un elemento, es decir, preguntar si el elemento buscado es igual al primero, segundo, tercero y así sucesivamente hasta encontrar el deseado.

Entonces este algoritmo tiene una complejidad de O(n).

Ejemplo de código en C++

#include <iostream>using namespace std;int n,i,a[10],nb,b;

void ingresar(){cout<<"Ingrese tamano del arreglo: ";cin>>n;cout<<"\n\n Ingresar elementos numericos"<<endl;

for(i=1;i<=n;i++){cout<<"a["<<i<<"] : ";cin>>a[i];

}cout<<"\n\n Ingrese numero a buscar: ";cin>>nb;//inicia búsqueda linealfor(i=1;i<=n;i++){

if(nb=a[i]){b=1;

}} //termina busqueda linealif(b){

cout<<"\n Numero encontrado"<<endl;}

else{

cout<<endl<<" Numero buscado "<<nb; cout<<" no se encuentra en el arreglo."<<endl;

}}

int main(int argc, char *argv[]) {ingresar();cout<<"\n\nRealizado por: David Ortiz\n";

system("PAUSE");return 0;

}

Page 87: Analisis de Sistemas Computarizados (Libro)

METODO DE BUSQUEDA BINARIA

La búsqueda binaria utiliza un método de “divide y vencerás” para localizar el valor deseado. Con este método se examina primero el elemento central de la lista; si éste es el elemento buscado, entonces la búsqueda ha terminado.

En caso contrario, se determina si el elemento buscado será en la primera o la segunda mitad de la lista y a continuación se repite este proceso, utilizando el elemento central de esa sublista.

Se puede aplicar tanto a datos en listas lineales como en árboles binarios de búsqueda. Los pre-requisitos principales para la búsqueda binaria son:

La lista debe estar ordenada en un orden específico de acuerdo al valor de la llave. Debe conocerse el número de registros.

Ejemplo de código en C++

#include<iostream>#include<conio.h>using namespace std;

int main(){

int i,n,A[100],nbuscar,primero=0,ultimo,centro,encontrado=0;

cout<<" Ingrese dimension del arreglo: ";cin>>n;

for(i=1;i<=n;i++){cout<<" Ingrese Valor numerico en A["<<i<<"] = ";cin>>A[i];

}

cout<<"\n\n Ingrese numero a buscar: ";cin>>nbuscar;ultimo=n;// inicia busqueda binariawhile(primero<=ultimo && (!encontrado)){

centro=(primero+ultimo)/2;if(nbuscar==A[centro]){

encontrado=1;}else{

if(nbuscar>A[centro])primero=centro+1;else

ultimo=centro-1;}

}// termina busqueda binariaif(encontrado){

cout<<"\n\n Numero encontrado ";cout<<" esta en la posicion: "<<centro<<endl;

}else

{ cout<<endl<<"\n\n Numero buscado "<<nbuscar; cout<<" no se encuentra en el arreglo "<<endl;}

cout<<"\n\n Realizado por: David Ortiz\n"; system("PAUSE");

return 0;}

Page 88: Analisis de Sistemas Computarizados (Libro)

4.3. Recuperación de datos

Recuperación de datos es obtener los datos que se encuentran almacenados en un archivo el cual puede estar en un dispositivo de almacenamiento primario (Memoria RAM) o secundario (Disco Duro, USB, etc).

Archivos de acceso aleatorio

Los archivos de acceso aleatorio son más versátiles, permiten acceder a cualquier parte del fichero en cualquier momento, como si fueran arrays en memoria. Las operaciones de lectura y/o escritura pueden hacerse en cualquier punto del archivo.

En general se suelen establecer ciertas normas para la creación, aunque no todas son obligatorias:

1. Abrir el archivo en un modo que te permita leer y escribir. Esto no es imprescindible, es posible usar archivos de acceso aleatorio sólo de lectura o de escritura.

2. Abrirlo en modo binario, ya que algunos o todos los campos de la estructura pueden no ser caracteres.3. Usar funciones como fread y fwrite, que permiten leer y escribir registros de longitud constante desde y hacia un

fichero.4. Usar la función fseek para situar el puntero de lectura/escritura en el lugar apropiado de tu archivo.

Por ejemplo, supongamos que nuestros registros tienen la siguiente estructura:struct stRegistro { char Nombre[34]; int dato; int matriz[23];} reg;

Teniendo en cuenta que los registros empiezan a contarse desde el cero, para hacer una lectura del registro número 6 usaremos:

fseek(fichero, 5*sizeof(stRegistro), SEEK_SET);fread(&reg, sizeof(stRegistro), 1, fichero);

Para hacer una operación de escritura, usaremos:fseek(fichero, 5*sizeof(stRegistro), SEEK_SET);fwrite(&reg, sizeof(stRegistro), 1, fichero);

Muy importante: después de cada operación de lectura o escritura, el cursor del fichero se actualiza automáticamente a la siguiente posición, así que es buena idea hacer siempre un fseek antes de un fread o un fwrite.

Para calcular el tamaño de un fichero, ya sea en bytes o en registros se suele usar el siguiente procedimiento:long nRegistros;long nBytes;fseek(fichero, 0, SEEK_END); // Colocar el cursor al final del ficheronBytes = ftell(fichero); // Tamaño en bytesnRegistros = ftell(fich)/sizeof(stRegistro); // Tamaño en registros

Borrar registros

Borrar registros puede ser complicado, ya que no hay ninguna función de biblioteca estándar que lo haga.Es su lugar se suele usar uno de estos dos métodos:

1. Marcar el registro como borrado o no válido, para ello hay que añadir un campo extra en la estructura del registro:

struct stRegistro { char Valido; // Campo que indica si el registro es válido char Nombre[34]; int dato; int matriz[23];};

Page 89: Analisis de Sistemas Computarizados (Libro)

Si el campo Valido tiene un valor prefijado, por ejemplo 'S' o ' ', el registro es válido. Si tiene un valor prefijado, por ejemplo 'N' o '*', el registro será inválido o se considerará borrado.De este modo, para borrar un registro sólo tienes que cambiar el valor de ese campo.Pero hay que tener en cuenta que será el programa el encargado de tratar los registros del modo adecuado dependiendo del valor del campo Valido, el hecho de marcar un registro no lo borra físicamente.Si se quiere elaborar más, se puede mantener un fichero auxiliar con la lista de los registros borrados. Esto tiene un doble propósito:

Que se pueda diseñar una función para sustituir a fseek() de modo que se tengan en cuenta los registros marcados.

Que al insertar nuevos registros, se puedan sobrescribir los anteriormente marcados como borrados, si existe alguno.

2. Hacer una copia del fichero en otro fichero, pero sin copiar el registro que se quiere borrar. Este sistema es más tedioso y lento, y requiere cerrar el fichero y borrarlo o renombrarlo, antes de poder usar de nuevo la versión con el registro eliminado.

Lo normal es hacer una combinación de ambos, durante la ejecución normal del programa se borran registros con el método de marcarlos, y cuando se cierra la aplicación, o se detecta que el porcentaje de registros borrados es alto o el usuario así lo decide, se "empaqueta" el fichero usando el segundo método.

Ejemplo de código en C++

#include <iostream>#include <fstream>#include <iomanip>#include <cstdlib>#include <cstring>using namespace std;// Funciones auxiliares:int Menu();long LeeNumero();// Clase registro.class Registro { public: Registro(char *n=NULL, int d1=0, int d2=0, int d3=0) : valido('S') { if(n) strcpy(nombre, n); else strcpy(nombre, ""); dato[0] = d1; dato[1] = d2; dato[2] = d3; } void Leer(); void Mostrar(); void Listar(long n); const bool Valido() { return valido == 'S'; } const char *Nombre() { return nombre; } private: char valido; // Campo que indica si el registro es válido // S->Válido, N->Inválido char nombre[34]; int dato[4];};// Implementaciones de clase Registro:// Permite que el usuario introduzca un registro por pantallavoid Registro::Leer() { system("cls"); cout << "Leyendo registro..." << endl << endl; valido = 'S'; cout << "Nombre: "; cin.getline(nombre, 34); for(int i = 0; i < 3; i++) { cout << "Dato[" << i << "]: "; dato[i] = LeeNumero(); }}// Muestra un registro en pantalla, si no está marcado como borradovoid Registro::Mostrar(){ system("cls"); if(Valido()) { cout << "Nombre: " << nombre << endl; for(int i = 0; i < 3; i++) cout << "Dato[" << i << "]: " << dato[i] << endl;

Page 90: Analisis de Sistemas Computarizados (Libro)

} cout << "\n\n Pulsa una tecla"; cin.get();}// Muestra un registro por pantalla en forma de listado, // si no está marcado como borradovoid Registro::Listar(long n) { int i;

if(Valido()) { cout << "[" << setw(6) << n << "] "; cout << setw(34) << nombre; for(i = 0; i < 3; i++) cout << ", " << setw(4) << dato[i]; cout << endl; }}// Clase Datos, almacena y trata los datos.class Datos :public fstream { public: Datos() : fstream("aleatorio.dat", ios::in | ios::out | ios::binary) { if(!good()) { open("aleatorio.dat", ios::in | ios::out | ios::trunc | ios::binary); cout << "Fichero creado" << endl; system("PAUSE"); } } ~Datos() { Empaquetar(); } void Guardar(Registro &reg); bool Recupera(long n, Registro &reg); void Borrar(long n); private: void Empaquetar();}; // Implementación de la clase Datos.void Datos::Guardar(Registro &reg) {// Insertar al final: clear(); seekg(0, ios::end); write(reinterpret_cast<char *> (&reg), sizeof(Registro)); cout << reg.Nombre() << endl;}bool Datos::Recupera(long n, Registro &reg) { clear(); seekg(n*sizeof(Registro), ios::beg); read(reinterpret_cast<char *> (&reg), sizeof(Registro)); return gcount() > 0;}// Marca el registro como borrado:void Datos::Borrar(long n) { char marca; clear(); marca = 'N'; seekg(n*sizeof(Registro), ios::beg); write(&marca, 1);}// Elimina los registros marcados como borradosvoid Datos::Empaquetar() { ofstream ftemp("aleatorio.tmp", ios::out); Registro reg; clear(); seekg(0, ios::beg); do { read(reinterpret_cast<char *> (&reg), sizeof(Registro)); cout << reg.Nombre() << endl; if(gcount() > 0 && reg.Valido()) ftemp.write(reinterpret_cast<char *> (&reg), sizeof(Registro)); } while (gcount() > 0); ftemp.close(); close(); remove("aleatorio.bak"); rename("aleatorio.dat", "aleatorio.bak");

Page 91: Analisis de Sistemas Computarizados (Libro)

rename("aleatorio.tmp", "aleatorio.dat"); open("aleatorio.dat", ios::in | ios::out | ios::binary);}

int main(){ Registro reg; Datos datos; int opcion; long numero;

do { opcion = Menu(); switch(opcion) { case '1': // Añadir registro reg.Leer(); datos.Guardar(reg); break; case '2': // Mostrar registro system("cls"); cout << "Coloca registro a Mostrar: "; numero = LeeNumero(); if(datos.Recupera(numero, reg)) reg.Mostrar(); break; case '3': // Eliminar registro system("cls"); cout << "Coloca registro a Eliminar: "; numero = LeeNumero(); datos.Borrar(numero); break; case '4': // Mostrar todo numero = 0; system("cls"); cout << "Nombre Datos" << endl; while(datos.Recupera(numero, reg)) reg.Listar(numero++); cout << "\n\n Presiona <<enter>>"; cin.get(); break; } } while(opcion != '0');}// Muestra un menú con las opciones disponibles y captura una opción del usuarioint Menu(){ char resp[20]; do { system("cls"); cout << "MENU PRINCIPAL" << endl; cout << "--------------" << endl << endl; cout << "1- Insertar registro" << endl; cout << "2- Mostrar registro" << endl; cout << "3- Eliminar registro" << endl; cout << "4- Mostrar todo" << endl; cout << "0- Salir" << endl; cout<<"\n Realizado por: David Ortiz\n"; cout<<"\n\n Ingrese opcion: "; cin.getline(resp, 20); } while(resp[0] < '0' && resp[0] > '4'); return resp[0];}// Lee un número suministrado por el usuariolong LeeNumero(){ char numero[6];

fgets(numero, 6, stdin); return atoi(numero);}

Page 92: Analisis de Sistemas Computarizados (Libro)

BIBLIOGRAFÍA

1. Fundamentos de Informática y Programación.2. Estructuras de datos básicas, Xavier Sáez Pous.3. Estructuras no lineales de datos: arboles, Luis Rodríguez Baena.4. http://www.monografias.com/trabajos14/estrucdinamicas/estrucdinamicas.shtml#ixzz40DHQcOgS 5. https://hhmosquera.wordpress.com/listasenlazadas/ 6. https://dmmolina.wordpress.com/listas-enlazadas-y-ejemplos/ 7. https://es.wikibooks.org/wiki/Programaci%C3%B3n_en_C%2B%2B/Estructuras_II 8. http://www.monografias.com/trabajos25/colas/colas.shtml#ixzz41n9704LH 9. http://wwwtemarioestructuradedatos.blogspot.mx/p/estructura-de-datos-dinamicas-y.html 10. http://programavideojuegos.blogspot.com/2013/05/curso-basico-de-c-710-procedimientos.html 11. https://es.wikibooks.org/wiki/Programaci%C3%B3n_en_C/Algoritmos_y_Estructuras_de_Datos 12. http://c.conclase.net/