U1. Estructuras de Datos y Análisis de Algoritmos

March 31, 2019 | Author: SalvattoreSan | Category: Algorithms, Programming Language, Bit, Python (Programming Language), Complex Number
Share Embed Donate


Short Description

Estructuras de Datos y Análisis de Algoritmos...

Description

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

Universidad Abierta y a Distancia de México

Licenciatura en Matemáticas

9° cuatrimestre

Computación II

Unidad 1 Estructuras de datos y análisis de algoritmos

Clave: 050930936

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Índice Unidad 1. Estructuras de datos y análisis de algoritmos .................................................... 4 Presentación Presentación de la Unidad ................................................................................................. 4 Propósitos de la unidad...................................................................................................... 4 Competencia Competencia específica específica ..................................................................................................... 5 1.1. Introducción ................................................................................................................ 5 1.1.1.

Información y datos ............................................................................................ 7

1.1.2.

El lenguaje de programación programación Python .................................................................. 8

 Actividad 1. Números aleatorios con Phyton Phyton .................................................................... 21 1.2. Tipos de datos .......................................................................................................... 22 1.2.1. Tipos primitivos..................................................................................................... 22 1.2.2. Tipos simples y compuestos compuestos ................................................................................. 23 1.2.3. Tipos abstractos abstractos ................................................................................................... 25 1.3. Análisis de algoritmos algoritmos ............................................................................................... 29  Actividad 2. Reflexión Reflexión sobre algoritmos algoritmos ........................................................................... 31 1.3.1. Notación asintótica ............................................................................................... 31 1.3.2. Algoritmos iterativos y recursivos.......................................................................... 39 1.3.3. Diseño de algoritmos algoritmos ............................................................................................ 43 1.3.4. Complejidad en ordenamientos ordenamientos y búsquedas búsquedas ....................................................... 46  Actividad 3. Análisis de de complejidad complejidad ................................................................................ 49 1.4. Estructuras de datos ................................................................................................. 49 1.4.1. Arreglos y listas .................................................................................................... 49

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos 1.4.2. Pilas ..................................................................................................................... 54 1.4.3. Heaps................................................................................................................... 55 1.4.4. Árboles ................................................................................................................. 56 1.4.5. Funciones y tablas de hash .................................................................................. 58  Actividad 4. Estructuras Estructuras de datos datos ..................................................................................... 60  Autoevaluación  Autoevaluación ................................................................................................................ 60 Evidencia de aprendizaje. Diseño de algoritmo........................................................... algoritmo ........................................................... 60  Autorreflexiones  Autorreflexiones ............................................................................................................... 61 Cierre de la Unidad .......................................................................................................... 61 61 Para saber más ............................................................................................................... 61 Referencias Referencias Bibliográficas Bibliográficas................................................................................................ 62

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Unidad 1. Estructuras de datos y análisis de algoritmos Presentación de la Unidad Las computadoras son popularmente vistas como aparatos electrónicos capaces de efectuar cálculos inmediatos, pero este concepto está incompleto, ya que esas son únicamente las computadoras digitales. Entonces, es importante aclarar el concepto de computadora con el que vamos a trabajar. Una computadora es un dispositivo capaz de mapear valores entre un par de conjuntos a través de una serie de pasos finitos que se denominan algoritmos. El modelo de Turing (Turing, 1936) es el modelo matemático que fundamenta la descripción de las computadoras digitales con las que trabajas, y que fue posteriormente implementado en una computadora electrónica construida por von Neumann, a partir de quien se popularizó el concepto de programa almacenado. Estas computadoras, como todos saben, corren programas para calcular valores. Sin embargo, un par de características del modelo de Turing es hacer uso de una banda infinita donde leer o escribir la información procesada, así como un conjunto de instrucciones que le dicen a la cabeza lectora si debe moverse a la derecha o izquierda. En la práctica este particular aspecto guarda una diferencia abismal con la computadora construida por von Neumann, ya que la posibilidad de recursos infinitos está fuera de nuestro alcance, en tanto que respecto al conjunto de instrucciones, es fácil observar que pueden agregarse instrucciones innecesarias para realizar algún cómputo deseado, o bien, el caso complementario: es fácil suponer que existen algoritmos que hacen un uso innecesario de instrucciones o espacio para poder realizar su cómputo. En esta unidad aprenderás a clasificar este conjunto de instrucciones, a la postre denominados algoritmos, de acuerdo con su eficiencia. Aprenderás a medir ciertos elementos que los hacen más eficientes, además de conocer distintas estructuras de datos que son fundamentales para hacer un uso óptimo de los recursos de la máquina, dependiendo del problema específico que tengas a la mano. Todo esto lo harás mediante un lenguaje de programación actualmente muy popular y sencillo de usar, llamado Python

Propósitos de la unidad    

Identificarás los diferentes tipos de datos existentes. Aplicarás las diferentes estructuras de datos según convenga al problema en cuestión. Clasificarás algoritmos mediante el uso de la notación asintótica. Diseñarás algoritmos sujetos a una clase de complejidad específica.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Competencia específica Diseñar  algoritmos para que hagan uso de los recursos computacionales de forma óptima mediante las herramientas descritas en la teoría de la complejidad computacional y el análisis de algoritmos.

1.1. Introducción En la presentación de la unidad se mencionan un par de modelos teóricos (uno de ellos implementado) sobre las computadoras que actualmente usas; entre ellos está el modelo de Turing que puedes ver en la figura 1. Otro ejemplo de computadora es el cerebro humano, ya que tiene la capacidad de hacer cálculos usualmente relacionados con valores alimentados a través de los sentidos, y cuyos cálculos se pueden observar a través de las decisiones o acciones que tomas. Al igual que las computadoras digitales, es fácil ver que el cerebro humano tiene una cantidad finita de recursos, como el espacio físico donde se alberga, o bien, la cantidad de conexiones sinápticas que son usadas y mantenidas comúnmente a lo largo de la vida de un individuo. En estos ejemplos se puede vislumbrar un compromiso entre rapidez y exactitud, es decir, el uso eficiente de los recursos dependiendo del problema en cuestión.

Figura 1. Esquema de la máquina de Turing (Wikipedia)

El uso de una computadora para resolver problemas suele ser transparente a las necesidades del usuario final, pero no debe ser así para el programador; es aquí donde los aspectos de eficiencia se hacen patentes y éste es el punto que tendrás en mente todo el tiempo a lo largo de esta unidad: el hacer uso de los recursos de la computadora de forma eficiente, o por lo menos, no los perderás de vista, y es que el que una máquina sea muy rápida para resolver algoritmos con una entrada pequeña (por pequeña puede suponerse algo menor a una decena de datos), no implica que esta relación se conserva para una entrada con muchos más datos para el mismo algoritmo. Por ejemplo, no es lo mismo encontrar una ruta óptima en una gráfica con 5 nodos que con 200, de hecho, ese particular problema es un problema NP-Completo, lo cual significa que para este tipo de problemas no se ha encontrado un algoritmo que pueda resolverlo en tiempo polinomial, pero esto se definirá con más precisión más adelante.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

a

B

Figura 2. Grafica con pesos en las cuales una es intrínsecamente más sencilla de examinar que la otra por lo tanto cualquier algoritmo para recorrerlas tardará más en la segunda gráfica.

En la figura 2 puedes ver un ejemplo del problema particular antes mencionado, problema denominado usualmente como el Problema del Agente Viajero (TSP por sus siglas en inglés). Este problema consiste en tener que visitar todos los nodos una sola vez, minimizando el esfuerzo (o distancia) recorrida, representado en las gráficas por los pesos en las aristas. Una forma ingenua de resolverlo podría ser la siguiente: 1. Crear una lista vacía L 2. Escoger un nodo al azar y ponerlo al inicio de L 3. Viajar a cualquiera de sus vecinos y agregarlo a la lista L 4. Repetir el paso 3 hasta que todos los nodos estén visitados. Una vez hecho esto, la lista L indicará el orden en el que hay que visitar esos particulares nodos para completar un recorrido en la gráfica que pase por todos los nodos. 5. Repetir desde el paso 1 hasta que todas las posibles rutas estén construidas

Algoritmo 1. Primer intento de algoritmo para el TSP

Pero es claro ver que este algoritmo, si bien es factible para la gráfica a, es impensable para la gráfica b, y esta última no es ni siquiera comparable a la configuración de una ciudad promedio. En este punto es importante aclarar que este tipo de problemas (NP-Completos) no tienen soluciones que ocupen un tiempo acotado de resolución, y lo que se ha desarrollado hasta ahora no son más que aproximaciones heurísticas, y no se ha tenido que conformar con alguna

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos aproximación. Así que, dicho esto, podría incluso hacerse otra propuesta de algoritmo para resolver el TSP. Crear un conjunto V vacío Tomar y Crear una lista vacía L Tomar un nodo al azar y denominarlo p y agregarlo a L Viajar el nodo conectado con p cuya arista tenga el peso más pequeño y no esté visitado, denominarlo q Renombrar q a p  Agregar p a L Regresar a 4 hasta que L tenga N elementos  Agregar L a V Regresar a 3 M-1 veces Ordenar V dependiendo del costo de cada elemento.

     √ 

Algoritmo 2. Segundo intento de solución al TSP

En este último algoritmo se sabe que vas encontrar la mejor solución posible que permita buscar sobre la raíz de N recorridos en la gráfica; tal vez no sea la óptima, pero, por lo menos, es una solución factible de implementar en nuestras computadoras y completar en un tiempo razonable. Este tipo de compromiso es algo que se puede ponderar y optimizar, siempre y cuando tengas claro alguna medida sobre algún algoritmo en particular.

1.1.1. Información y datos En una época donde las computadoras digitales no sólo son ubicuas, sino que también dictan muchos de los aspectos de cómo vivir, desarrollarte y convivir, es importante conocer aspectos básicos de su funcionamiento. Lo que usualmente esperas de una computadora es que procese datos para convertirlos en información. Este procesamiento recibe el nombre técnico de cómputo, y lo que hace es aplicar una serie de pasos finitos y bien determinados, serie llamada algoritmo, a un elemento de un conjunto para mapearlo en otro conjunto. Entonces, se puede establecer la definición de algoritmo que usarás de aquí en adelante Definición: un algoritmo es un conjunto de pasos finitos y ordenados, cada uno de ellos ejecutable en un tiempo finito, que llevan a cabo un mapeo entre un par de conjuntos.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

(1)

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos El título de esta subsección hace una distinción implícita entre lo que son los datos y lo que es la información. Esta distinción no es difícil de entender, y es que no es lo mismo tener una serie de valores desligados, que agrupados bajo alguna estructura que infiera alguna secuencia lógica de los mismos. No es lo mismo tener todos los nombres de los habitantes de la ciudad de México, así como sus respectivos teléfonos de manera desordenada, que estructurados en una guía telefónica ordenada alfabéticamente. Entonces, algo importante que puede decirse sobre lo que es la información, es que está estructurada con algún patrón discernible. Más aún, ese patrón puede ser computable mediante un algoritmo. Para poder practicar las diferentes estructuras y algoritmos que verás en esta unidad, tienes que usar una computadora y, particularmente, un lenguaje de programación.

1.1.2. El lenguaje de programación Python El lenguaje de programación del que harás uso es Python 2.7 (no se cubrirá el uso de Python 3.x), que puedes bajar de la siguiente dirección http://www.python.org/getit/windows/

Figura 3. Página de Python

En este sitio puedes ver las opciones para bajar Python en distintas distribuciones. Si sigues la primer liga (http://www.python.org/download/releases/ ), entrarás a la siguiente página

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

Figura 4. Versiones de Python, de las cuales usaremos las versiones 2.7.x

Pero si lo deseas, y de hecho, como recomendación, puedes usar la versión de Enthought Python Distribution (http://www.enthought.com/products/epd.php ), la cual tiene una distribución gratuita (https://www.enthought.com/products/epd/free/ ) y que resultará particularmente útil, ya que tiene integradas bibliotecas como  Numpy  o Matplotlib.

Una vez instalado, podrás ver las siguientes opciones en el menú de inicio:

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

a

B

Figura 5. Python instalado. Usaremos principalmente el “Canopy command prompt ”, pero, si deseas, puedes familiarizarte con la interfaz IDLE mostrada en la figura b.

Si eres usuario de Linux es muy probable que ya lo tengas instalado, y si no, es muy sencillo instalarlo; depende de tu distribución para lograr esto. Python es un lenguaje de programación interactivo, orientado a objetos, y de tipado dinámico, así como con algunas características de un lenguaje funcional. Su principal motivación es la de crear código entendible y restringir más el laconismo que caracteriza a C o Java. Para hacer esto, en Python la identificación de los programas no es despreciable; esto significa que el margen izquierdo que caracteriza en el editor de scripts tiene un significado: el alcance de las variables definidas en ese nivel, o bien, la definición de un ámbito concreto. Esto significa que todas las variables que defina en cierto nivel pueden ser leídas en ese nivel o en aquellos donde la identación es mayor. Identar  un texto se refiere a alinearlo con un margen izquierdo mayor que el nivel anterior. El nivel de identación original es el margen 0. Si identaras el texto un nivel, por ejemplo, al margen izquierdo 4, entonces todo el texto que estuviera alineado al margen 4 estaría a un nivel de identación. En el caso concreto de Python, los distintos niveles de identación determinan el estilo de codificación que seguirás, ya que es la manera en que este lenguaje decide qué líneas pertenecen o no a un mismo bloque de código.  A continuación puedes ver un par de ejemplos, uno de ellos correcto y el otro incorrecto. Ejemplo >>> i = 4 >>> for j in range(5): ... print i,j ... 4 0

>>> i = 4 >>> for j in range(5): ... print i,j File “”, line 2 print i,j

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos 4 1 4 2 4 3 4 4 >>>

IndentationError: expected an indented block >>> A

B

En este par de ejemplos puedes ver, en el panel A, un uso correcto de la identación en Python. La línea que contiene el print es correctamente ejecutada dentro del ciclo del for en la línea anterior, que es como está definida la sintaxis en Python. Por otro lado, en el panel B puedes ver el uso incorrecto de esto, ya que el uso del for es idéntico al del panel B, con el único cambio en que la línea con print está pegada al lado izquierdo, y no correctamente indentada. Por cierto, lo que el for está diseñado a ejecutar en ambos ejemplos es imprimir el valor de la variable i, que está fijo en la primera línea, y el de j, que va variando conforme se llevan a cabo iteraciones del for. Es decir, está diseñado para imprimir la pareja ordenada (i,j) como puedes ver en el panel A. En esta sección verás un tutorial extremadamente condensado sobre el uso y sintaxis de Python, lo cual es necesario para poder comprender el resto de la unidad, pero se dejarán una serie de ligas y libros para que puedas profundizar en el uso de este lenguaje. Uso Básico Los scripts de Python son archivos de texto, es decir, tienes que editarlo con el notepad, notepad++, o cualquier editor que deje los archivos en texto claro, y al final le vas a anexar, por convención, la extensión de archivo py, sin espacios o caracteres especiales en los nombres. Los siguientes son ejemplos válidos de nombres de script en Python: - tree.py - Hola_mundo.py - basico1.py Y los siguientes son ejemplos de nombres inválidos - lo malo.py - Niño.py - Término.py - Script Para poder correr el siguiente ejemplo vas crear un directorio llamado computación2 en el directorio C:\ C:\computacion2

y dentro de él vas a copiar el siguiente contenido, tal cual: -*- coding:utf8 -*for i in range(2,5): print i, “Hola Mundo”

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos - en un archivo que se llamará hola_mundo.py, para después ejecutarlo con Python como se muestra a continuación: C:\computacion2> python hola_mundo.py

Para ejecutarlo tienes que abrir el intérprete de comandos de Windows y cambiarte al directorio especificado arriba. La ejecución debe verse como a continuación se presenta: (Canopy 32bit) C:\computacion2>python hola_mundo.py 2 Hola Mundo 3 Hola Mundo 4 Hola Mundo

Sugerencia: cambia el contenido del script que acabas de crear para que te salude a ti. Sintaxis Básica Como todo lenguaje de programación, Python tiene definido una sintaxis de operación básica en la que está definido cómo hacer la asignación de variables, cómo usar los operadores aritméticos básicos, las operaciones lógicas básicas, la ejecución de ciclos, la definición de clases, etcétera. En esta sección repasarás algunos de estos puntos. Asignación y Cadenas Python es un lenguaje de tipado dinámico, lo que quiere decir que no tiene palabras reservadas para determinar si algún número es entero, flotante, o cualquier variante de ésas que son ampliamente usadas en otros lenguajes fuertemente tipados como Java. Los detalles sobre cómo Python puede identificar la precisión del número con el que estás trabajando son ajenos a este curso; sin embargo, de aquí en adelante usarás la siguiente regla de asignación de variables sin mayor diferencia: >>> >>> var = >>> print 45 >>> var = >>> print nombre >>> >>> var = >>> print 0.445 >>>

45 var “nombre”

var

0.445 var

Como puedes ver, sin mayor problema asignas no sólo un valor, sino un tipo completamente diferente de datos a la misma variable, y así es como funciona la asignación de variables en Python.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Como puedes ver en la salida anterior, el uso de cadenas se hace a través de envolver las mismas con un par de comillas (“) , o bien, también puede hacerse con una comilla simple („) siempre y cuando sea consistente, por ejemplo: >>> var = “cadena 1”

cadena 1 >>> var = „cadena 1‟

cadena 1

>>> Un conjunto de funciones existentes en Python que vale la pena recordar son: len, str, print, input. A continuación puedes ver un ejemplo de cada una aplicada a la variable var del ejemplo anterior >>> len(var) #logitud de var 7 >>> str(12) #convierte a cadena el argumento „12‟ >>> var=input(„Otro nombre „) Otro nombre „otromas‟ >>> print var otromas >>>

En los ejemplos anteriores es importante notar que len calcula la longitud del argumento que hayas pasado, str convierte a cadena el argumento; es por esto que aparece envuelto en comillas simples, y var recibe una cadena, ya que de otra forma fallaría. Listas Otra característica en la operación de Python es su orientación al manejo de listas, las cuales se conforman de la siguiente manera. Si deseas hacer una lista vacía, debes: >>> L = []

O bien, >>> L = list()

Si deseas hacer una lista con una cantidad ya conocida de elementos, lo que tienes que hacer es lo siguiente: >>> L = [1,2,3,6,8,‟a‟,‟algo‟,8, 3.2]

>>> Desayuno = [“fruta”, “huevos”, “frijoles”, “tocino”, “cafe”]

Para que puedas obtener (indexar) algún elemento de una lista previamente hecha, lo que tienes que hacer es usar los paréntesis cuadrados y el índice del elemento requerido: >>> L[4] 8

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos >>> Desayuno[0] „fruta‟

>>>

Observa que, al igual que en muchos lenguajes de programación, el indexado de elementos empieza en 0 y no en 1 como usualmente se hace en matemáticas (o incluso en Matlab u Octave). Pero con Python se han esforzado mucho en hacer un uso eficiente y cómodo de las listas y otro tipo de secuencias, por lo que la siguiente notación, aunque no es estándar, te ayudará mucho a poder manipularlas con comodidad. Si deseas obtener todos los elementos de una lista cuyos índices sean, por ejemplo, el 3, 4 y 5, lo que tienes que hacer es pasar entre corchetes los índices 3 y 6, separados por dos puntos de la siguiente forma: >>> L[3:6] [6, 8, „a‟] >>>

Es decir, entre corchetes pasas el índice donde quieras empezar a tomar la subsecuencia (slice) y  el índice siguiente de donde quieras que termine; o sea, si tu subsecuencia va del índice i al índice j, entonces, entre corchetes, pasas desde i hasta j+1. Si quieres todos los elementos de una lista a partir del elemento 2 (o bien, lo puedes generalizar del n-ésimo elemento en adelante), lo que tienes que haces es: >>> Desayuno[2:] [‟frijoles‟, „tocino‟, „cafe‟] >>>

Puedes pasar índices negativos. Por ejemplo, si pasas Desayuno[-1], lo que obtienes es: >>> Desayuno[-1] „café‟

>>>

O bien, puedes pedir toda la subsecuencia de los últimos dos elementos de la lista Desayuno de la siguiente manera: >>> Desayuno[-2:] „café‟

>>>

Esta instrucción puede ser interpretada en lenguaje natural como: dame los elementos de la lista Desayuno, empezando dos antes de terminar.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Pero quieres los elementos de la lista Desayuno, dejando fuera los últimos dos elementos, entonces >>> Desayuno[:-2] [‟fruta‟, „huevos‟, „frijoles‟] >>>

Si lo que quieres es obtener una copia de los valores de una lista, lo que tienes que hacer es ocupar el operador dos puntos ( :) dentro de los corchetes sin acompañarlo de ningún número, o sea: >>> >>> [1, >>> [1, >>>

P = L[:] P 2, 3, 6, 8, „a‟, „algo‟, 8, 3.2] L 2, 3, 6, 8, „a‟, „algo‟, 8, 3.2]

Es muy importante que recuerdes que así se copian los valores de una lista, ya que al hacer una simple asignación, como Q=L, lo que estarías haciendo sería referenciar la lista L con el nombre Q. >>> >>> >>> [1, >>>

L = [1, 2, 3, 6, 8, „a‟, „algo‟, 8, 3.2] Q = L Q 2, 3, 6, 8, „a‟, „algo‟, 8, 3.2]

Lo que significa que si haces un cambio en la lista Q, se verá reflejado también en L sin que sea necesariamente lo que necesitas, y esto puede inducir a errores, ya que Python no lanza una excepción en este caso, pues así está diseñado. >>> 8 >>> >>> [1, >>>

Q[4] Q[4] = „cambio‟ L 2, 3, 6, „cambio‟, „a‟, „algo‟, 8, 3.2]

Si lo que quieres es unir dos listas, puedes usar el operador + >>> >>> >>> >>> [1,

L = [1, 2, 3] M = [4, 5, 6] N = L+M N 2, 3, 4, 5, 6]

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos >>>

O bien, puedes concatenar varias veces el valor de alguna lista, por ejemplo: >>> O = L*4 >>> O [1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3] >>>

En Python las cadenas de caracteres también son consideradas como listas, lo que significa que el primer carácter de alguna cadena lleva el índice 0, 1 el segundo, y así hasta el último carácter, el cual se puede saber haciendo uso de la función len, que dirá la longitud total de la cadena y, por lo tanto, el último carácter tiene como índice un número menor que lo que indique esta función. Al ser tratadas como listas, podrás indexarlas de la misma manera que estas últimas. >>> c = „Hola Mundo‟ >>> len© 10 >>> c[0] „H‟ >>> c[2:6] „la M‟ >>> c[-4:] „undo‟ >>> c[:-2] „Hola Mun‟ >>>

Existen otras estructuras extremadamente útiles que se mencionarán en su momento en caso de que las ocupes, pero sería bueno que investigaras su función y las conocieras; se trata de las tuplas y los diccionarios. Operadores Aritméticos Los operadores matemáticos son un tema que prácticamente ya conoces porque son muy similares al de otros lenguajes, excepto por algunas particularidades referentes a la conversión de tipos en los números. Los operadores de suma, multiplicación, división, potenciación, negación, etcétera, los puedes encontrar en la siguiente tabla:

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Operación Suma ## Resta Multiplicación ## División Potenciación (a a la potencia b) Módulo (a módulo b) Negación Valor absoluto

Operador a + b ## a- b a * b ## a / b a**b a % b -a abs(a)

Tabla 1. Operaciones aritméticas básicas en Python.

Una observación importante es que si divides un entero entre otro entero, el resultado por default es entero: >>> a = 5/4 >>> a 1 >>>

Pero si lo que quieres es la expansión decimal, entonces tienes que hacer un truco como se muestra a continuación: >>> a = float(5)/4 >>> a 1.25 >>> a = 5.0 / 4 >>> a 1.25 >>>

Observa el uso de la función float. Lo que hace está función es convertir en flotante el argumento que le pases. La jerarquía para aplicar estos operadores está determinada por la siguiente tabla. Primero se ejecuta lo que haya entre paréntesis, luego se evalúan los exponentes, luego la multiplicación y división, y hasta el final, la suma y la resta. 1 2 3 4

Paréntesis Exponentes Multiplicación y División Suma y Resta

Tabla 2. Jerarquía de operaciones

Los ejemplos para esta sección son de tu conocimiento, ya que son prácticamente triviales.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Operadores lógicos Los valores lógicos en Python son el True y el False. Los operadores lógicos son el and, or y not. La forma de checar la igualdad entre dos valores es igual que en muchos lenguajes, con el doble igual ( ==) Ejemplos >>> a= 1 >>> b= 2 >>> c= 1 >>> a==b False >>> a==c True >>> not a False >>> not True False >>> d = 0 >>> not d True >>>

Para comprobar la relación entre dos valores puedes usar los siguientes operadores, == (igualdad), > (mayor que), = (mayor o igual), b / a < b a >= b / a >> for i in [1, 2, 3]: ... print “Linea “, i ... Linea 1 Linea 2 Linea 3

 Aquí puedes ver cómo el cuerpo del for que consta de una sola línea se ejecuta tantas veces como valores toma la variable i dentro de la lista especificada. El siguiente ejemplo es un poco más complicado, no sólo porque el for usa dos líneas, sino porque también se introducen dos funciones nuevas que son str y range, así como la concatenación de cadenas. >>> for i in range(5): ... print “Esta es la linea “, i ... str(i) + “ El doble de “ + str(i) + “ es: “ +str(i*2) ... Esta es la linea 0 „0 El doble de 0 es: 0 ‟ Esta es la linea 1 „1 El doble de 1 es: 2 ‟ Esta es la linea 2 „2 El doble de 2 es: 4 ‟ Esta es la linea 3 „3 El doble de 3 es: 6 ‟ Esta es la linea 4 „4 El doble de 4 es: 8 ‟ >>>

La función range entrega una lista que va desde el valor inicial indicado en el parámetro hasta el final, también indicado en el parámetro; cuando sólo se indica un número, toma por defecto que la lista comienza en 0 y acaba en ese número. Opcionalmente, también puedes indicarle cuántos pasos debe dejar intermedios. >>> [0, >>> [2, >>> [2,

range(5) 1, 2, 3, 4] range(2,5) 3, 4] range(2,5,2) 4]

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos >>>

La función str convierte su argumento en una cadena con todas las propiedades de la misma, particularmente con la propiedad de poderse concatenar a otras cadenas. En el ejemplo se concatena el número i con la cadena “El doble de”, luego con el número i otra vez, luego con la cadena “ es: “ para concatenarlo, finalmente, con el número que corresponde al doble de i str(i) + “ El doble de “ + str(i) + “ es: “ +str(i*2)

Observa cómo en este último ejemplo ambas líneas tenían la misma indentación y fueron éstas las que se ejecutaron mientras la variable i tomaba los valores especificados con range, a saber 0, 1, 2, 3 y 4. La función while es muy similar, y la diferencia radica en que el cuerpo del while se ejecuta mientras la condición lógica especificada se siga cumpliendo. La sintaxis es como sigue: while condición: cuerpo del while

De igual forma, no debes perder de vista las partes resaltadas en rojo, así como el tabulador en el cuerpo del while, ya que es forzoso ponerlas. En el siguiente ejemplo puedes ver su funcionamiento: >>> while x < valor: ... print str(x) + “ es menor “ + str(valor) ... x += 1 ... 0 es menor 5 1 es menor 5 2 es menor 5 3 es menor 5 4 es menor 5 >>>

Observa cómo se ejecutaron las dos líneas en el cuerpo del while hasta que la condición dejó de cumplirse. En la segunda línea del cuerpo puedes notar que hay una instrucción atípica que es ...

x += 1



Lo que en Python significa que la variable  va a aumentar en 1 su valor. Esta instrucción tiene la misma función que en Java o en C tiene la instrucción x++

Con la diferencia de que en Python, en vez de poner 1, puedes poner el valor que desees.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Imports En Python, como en cualquier otro lenguaje, existen funciones preprogramadas o módulos que se qiueran incorporar al código, lo cual se logra con la palabra reservada import. Por ejemplo >>> import math

- incorpora todo un conjunto de funciones que puede intuirse que son de naturaleza matemática, y a las cuales vas a acceder a través del operador “.” (punto), por ejemplo, la función sqrt, que calcula la raíz cuadrada, pertenece a este módulo; entonces, para invocar a la raíz cuadrada, lo que tienes que hacer es lo siguiente: >>> math.sqrt(25) 5.0 >>>

Otras formas equivalentes de importar las funciones de un módulo son: >>> import math as m

Lo que hace que tengas un alias para el módulo math llamado m >>> from math import sin

Lo que hace que importes la pura función sin, que dependa del módulo math. Aunque esta particular forma de importar funciones no es la mejor práctica de programación, ya que puede suceder que tengas redundancia de funciones en el mismo ámbito de programación. Con esto tienes una introducción muy básica pero bastante robusta para entender cualquier programa en Python. Para saber más http://en.wikibooks.org/wiki/Python_Programming/Sequences http://docs.python.org/2/tutorial/datastructures.html http://docs.python.org/2/library/stdtypes.html

Actividad 1. Números aleatorios con Phyton En esta actividad vas a practicar el uso de Python operando con números aleatorios. Instrucciones 1. Descarga el archivo Act1. Números aleatorios con Python . 2. Lee el contenido, atendiendo a las instrucciones y sugerencias que se hacen. 3. Crea un script de Python donde guardes la función y ejemplos que se te piden. 4. Guarda tu documento con la siguiente nomenclatura: MCOM2_U1_A1*_XXYZ.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos espera su retroalimentación. 5. Envía el documento a tu Facilitador(a) y espera su Recuerda consultar la  Escala de evaluación  de la actividad para saber qué aspectos se * Recuerda consultar tomarán en cuenta para su revisión.

1.2. Tipos de datos En la sección anterior empezaste a vislumbrar la relación que existe entre datos e información, siendo los datos la estructura más básica que puedes encontrar, y que, usualmente, será la entrada de algún proceso de cómputo. El aspecto más importante que caracteriza a los datos es que son la entrada de tus programas, particularmente la entrada de tu algoritmo, o, desde un punto matemático equivalente, es que son el argumento de la función que va a mapear un conjunto en otro. Y ése es el concepto que vas a representar con el término “datos”,  el dominio de una función que, a su vez, intenta modelar lo mejor posible algún fenómeno. Los diferentes tipos de datos siguen una jerarquía que va desde lo más sencillo, como son los tipos primitivos, hasta unas construcciones mucho más complejas, como los tipos de datos abstractos, aunque todos con la misma filosofía: los tipos de datos van a funcionar como el domino para el algoritmo que estés implementando. En esta unidad revisarás la jerarquía y filosofía detrás de lo que se considera un dato, que va desde los valores más básicos hasta conceptos como el de clase y objeto. Y aun cuando Python no trabaja explícitamente con una gran gama de datos primitivos distintos, no quiere decir que de forma subyacente no lo haga, y también está el hecho de que Python es un lenguaje orientado a objetos, que es uno de los tipos que desarrollarás.

1.2.1. Tipos primitivos Estos son los tipos de datos más básicos que pueden existir, después del binario, por su puesto. Otros lenguajes de programación toman categorías dependiendo de la cantidad de bits usados. Booleanos Byte Char

Únicamente requieren 1 bit para expresar todos los valores. No están signados, lo cual significa que no toman valores negativos. Usan 4 bits únicamente (1 byte). Están signados y toman valores desde el -128 al 127. Este tipo de datos hace uso de 16 bits y guarda una tabla de 65,535

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Short Int o Integer Long

Float Double

símbolos distintos, según la especificación Unicode. Tipo de datos signado que tiene un tamaño de 16 bits, toma valores que van desde el -32,768 hasta el 32,767. Tipo de datos signado que hace uso de 32 bits. Toma valores desde el -2,147,483,648 hasta el 2,147,483,647. Son variables que tienen un tamaño de 64 bits, pero siguen siendo enteros y van desde -9,223,372,036,854,775,808 hasta el 9,223,372,036,854,775,807. Pueden representar números con expansión decimal según el estándar IEEE 754, con un tamaño de 32 bits. Representan números con expansión decimal, pero hacen uso de 64 bits según el estándar IEEE 754.

Este tipo de datos son usados para los cálculos más básicos y son comúnmente asociados al modelo de tipado estático, es decir, se usan en aquellos lenguajes donde se debe declarar a qué tipo pertenece cada variable. Bajo el modelo de tipado dinámico de Python esta asignación es transparente, excepto por las ocasiones en las que explícitamente necesites mapear una variable a un tipo entero o flotante. Para saber más http://docs.oracle.com/javase/tutorial/java/nutsandbolts/datatypes.html

1.2.2. Tipos simples y compuestos Los tipos de datos simples son la primera abstracción útil usada en la modelación a través de la computadora. Los tipos de datos que corresponden a los tipos simples tipos de datos que no constituyen ninguna estructura más allá que el escalar que representan. Como puedes advertir bien, todos los tipos primitivos están considerados aquí, aunque la distinción no es clara. Dentro de los datos compuestos puedes encontrar las estructuras denominadas struct en C, o los tipos enum. Un dato del tipo struct reserva un área en memoria para valores no homogéneos, es decir, son el tipo de datos que contemplan problemas donde un dato está compuesto por valores de distinto dominio. Un ejemplo puede ser la entrada en una agenda. struct entrada{ int edad; char [100] nombre; char [10] telefono; };

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Figura 6. Struct que ilustra la implementación de la entrada en una agenda telefónica.

¿Qué aporte tiene un tipo de dato struct sobre definir las variables por separado? La ventaja de definir tipos compuestos de compuestos de esta forma es la de proveer consistencia en el manejo de variables dentro del programa y dejar que el lenguaje de programación se encargue de asignar los valores necesarios en memoria. Esto no puede parecer una gran ventaja basada en ciencia y análisis, y parece recaer únicamente en una implementación basada en la heurística y diseñada para fomentar la comodidad del programador. Esto no es enteramente falso, pero no es la única razón. Al incorporar a tu modelo tipos compuestos, también facilitas la tarea de incorporar consistentemente operaciones propias del dominio al que corresponda el tipo  struct en cuestión. El ejemplo más sencillo del que se puede valer es el de los números complejos, aquellos que se componen de una parte real y una imaginaria, mas ambos valores son números reales, al menos en el modelo matemático. La forma en la que puedes incorporar esto en un struct struct es la siguiente: struct complejo { float parte_real; float parte_compleja; };

Figura 7. Struct que implementa un número complejo.

 Al tener un un tipo de datos como como el mostrado mostrado en la figura 6 puedes puedes implementar implementar los axiomas que que operan sobre los números complejos con más facilidad. Por ejemplo, la suma de dos complejos, como sabes, se realiza sumando las partes reales y las partes imaginarias correspondientes. El resto de los axiomas sobre la operación sobre números complejos como el conjugado de un número, la multiplicación, etcétera, preservan esta simplificación incorporada en el momento en que encapsulas todo un concepto en un espacio en memoria. Otro tipo de dato compuesto es el enum, y la finalidad en su diseño es la de especificar variables en las que hay una secuencia implícita en un conjunto de valores. El ejemplo más sencillo son de tipo temporal, por ejemplo, los días de la semana o los meses del año. enum semana { dom, lun, mar, mie, jue, vie, sab } semanaX;

Figura 8. Ejemplo de un tipo enum.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Entonces, lo que hay que resaltar en esta transición desde los tipos primitivos hasta los compuestos, es el concepto de encapsulación de los axiomas y propiedades concernientes al dominio modelado con estos tipos de datos más complejos. Para saber más Data Structures: Abstract Data Types . En línea http://www.youtube.com/watch?v=HcxqzYsiJ3k

1.2.3. Tipos abstractos Con este recorrido entre los diferentes tipos de datos que caracterizan a los lenguajes fuertemente tipados como C o Java se puede dar paso a la explicación del paradigma de programación dominante actualmente: el modelo de programación orientada a objetos. Hasta este momento los tipos de datos han ido evolucionando sobre los componentes estructurales de los elementos del dominio requeridos en algún problema particular, desde el uso de los números flotantes para los cuales las operaciones están completamente incorporadas en la máquina que los opera, pasando por la cardinalidad impuesta en los tipos enum, hasta los números complejos que están compuestos por dos flotantes, uno de los cuales representa la parte real, y otro, la parte compleja. Con esta abstracción estructural sobre los elementos que componen el dominio plasmado en las variables que usas en el programa, se ha ido facilitando la tarea del programador al encapsular todos los datos que requiere para modelar algún problema en un cómodo paquete. Pero con un dominio con cierta estructura también viene un conjunto de propiedades y de funciones válidas que hagan cosas interesantes. En este momento se tiene que hablar un poco sobre esa tarea tan intangible que es modelar en un párrafo. Todos los que han programado saben que tienen que hacer un compromiso entre los aspectos característicos que toman del problema que están resolviendo y los detalles innecesarios que descartan porque no incorporan nada a la resolución del mismo. Esta tarea, tan común prácticamente a cualquier científico, es ineludible a cualquier programador; así que tienes que abstraer los elementos más importantes y significativos de un problema para poder resolverlo lo mejor posible. Esto es lo que se entiende por modelar . Cuando haces un modelo identificas qué partes componen en esencia el fenómeno observado y cuáles son parte del panorama y pueden ser descartadas. Más importante aún, una vez que identificas estos componentes o actores fundamentales, no lo haces únicamente estableciendo etiquetas, sino identificando comportamientos y relaciones entre ellos. Como matemático debes tener algo de práctica haciendo estos resúmenes de la realidad que percibes a través de los sentidos. Tal vez esto te deje un amargo sabor de boca al leer que no existe una técnica depurada y transversalmente aceptada, y finalmente correcta, detrás de la modelación, pero ese es el estado actual de las cosas: el hacer un modelo es una de las tareas más difíciles por

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos lograr, ya que cada persona percibe su entorno de manera distinta. Si te hace sentir mejor, no debes olvidar el refrán “  

Todos los modelos son malos, pero algunos sirven mejor que otros”.

Todo esto viene a cuento porque las computadoras son las herramientas de modelación por excelencia, y las computadoras digitales son las más usadas actualmente. Esto los lograrás haciendo uso de lenguajes de programación, que es donde vas a plasmar los modelos que hagas en papel para poder obtener algún resultado medible. Los tipos de datos abstractos (TDA) o tipos abstractos ( Abstract Data Type o ADT en inglés) son la abstracción más general que se puede hacer en un tipo de dato, y es que, a diferencia de los structs, quienes incorporan únicamente los distintos componentes de un nuevo tipo de dato o los tipos primitivos que se derivan básicamente de la estructura del procesado, en los TDA se hace una descripción axiomática de los elementos de un dominio incorporando las propiedades, características y operaciones que actúan sobre cada elemento del dominio. Es decir, en la definición del TDA se incorpora qué es lo que este nuevo tipo de dato tiene permitido hacer, así como los elementos que lo componen. Ejemplo: Puedes retomar el ejemplo de los números complejos como un TDA. Como ya se dijo y sabes, los números complejos constan de dos números reales que modelarás con un par de entradas flotantes, pero también constan de un conjunto de operaciones específicas de los números complejos como: 1. 2. 3. 4. 5.

El conjugado de un número La suma de complejos El producto de complejos Una norma específica La igualdad entre dos números complejos Pero también puedes distinguir un par de constantes concretos de los Complejos,

    

1. Neutro aditivo , 2. El número complejo  tal que

Cada una de estas funciones se opera de una forma muy específica y no es la misma que la multiplicación de reales o de matrices; son funciones específicas de los complejos, entonces, si defines un objeto que modele a los números complejos, vas a querer que incorpore estas funciones específicas concernientes a los complejos. Estás listo, pues, para la definición de objeto complejo. TDA Complejo Constantes:

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos  

0: Complejo i: Complejo

Atributos:  real: Parte real  imaginaria: Parte imaginaria

    

Operaciones:   Igualdad:   Suma:   Producto:   Escalar:   Norma:

Operación Igualdad Recibe: c1 y c2 números complejos Regresa: z complejo

Regresa z

   

Operación Norma Recibe: c complejo Regresa: z real

   

Regresa z Operación Escalar Recibe: c complejo y r real Regresa: z complejo

    

Operación suma Recibe: c1 y c2 complejos Regresa: z complejo

   

Operación multiplicación Recibe: c1 y c2 complejos Regresa: z complejo

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

         Figura 9. Definición del TDA para números complejos y un par de prototipos de funciones para la suma y la norma.

Lo importante de los TDA como el que se acaba de especificar es que en un tipo de dato de computadora se han capturado todos los elementos, axiomas y propiedades, que describen al campo de los números Complejos correctamente bien encapsulado dentro de un objeto. Los TDA pueden extenderse a objetos con una definición formal más laxa, pero la definición depende de tu habilidad como programador para identificar los elementos que componen alguna problemática específica. Por ejemplo, podemos definir TDA sobre objetos que pertenezcan al dominio de la biología, los cuales tienen una serie de características, así como un conjunto de funciones más o menos conocidas. Clase Hasta ahora el concepto de TDA es una descripción teórica que describe a los elementos de un dominio junto con sus operaciones. Una clase es la implementación de un TDA en un lenguaje de programación, considerando los atributos (elementos característicos) y las funciones u operaciones que actúan sobre ellos. En el siguiente ejemplo puedes ver cómo se podría implementar el TDA de número complejo presentado anteriormente en Python. Ejemplo. Clase de número complejo implementada en Python from math import sqrt class Complejo(object): “””Ejemplo de clase que implementa numeros complejos en Python””” def __init__(self, v=(0,0)): “””Constructor””” if v==(0,0): self.real = 0.0 self.imag = 0.0 else: self.real = v[0] self.imag = v[1] def __str__(self): “””Regresa la representacion en cadena de este número con formato a+bi””” if self.imag == 0.0: return str(self.real) elif self.real == 0.0:

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos return str(self.imag) + “i” else: return str(self.real) + “ + “ + str(self.imag) + “i” def suma(self, num2): “””Suma el numero complejo actual con num2””” self.real += num2.real self.imag += num2.imag def multiplicacion(self, num2): “””Multiplica el numero complejo actual con num2””” self.real = self.real * num2.real - self.imag * num2.imag self.imag = self.real * num2.imag + self.imag * num2.real def por_escalar(self, r): “””Multiplica el número actual por el escalar r””” self.real *= r self.imag *= r def absoluto(self): “””Calcula el valor abosluto de un número””” return sqrt(self.real**2 + self.imag**2)

El ejemplo anterior resulta un poco redundante, ya que los números complejos ya están definidos nativamente en Python junto con todas sus definiciones, pero en él puedes ver la finalidad de definir un TDA y, posteriormente, convertirlo en una clase en algún lenguaje de programación. Como puedes ver en el código anterior, están definidas algunas operaciones con complejos, así como la estructura que todo complejo posee, una parte real y una imaginaria; aparte de esto, puedes ver que hay dos funciones requeridas por Python, definidas __init__ y __str__. La primera es el constructor del objeto y la segunda es una función para convertir un objeto en cadena para así poder imprimirlo en pantalla Sugerencia Define, tomando como modelo las funciones definidas, la función conjugado que determine el conjugado del número complejo actual.

1.3. Análisis de algoritmos Se ha hablado con insistencia sobre el definir algoritmos más o menos eficientes, y es parte de este curso y de quienquiera que se dedique a programar, el tener la habilidad de diseñarlos o, por lo menos, identificar en qué puntos un algoritmo se está tardando, y si se puede eficientar, o el problema es en esencia demasiado complicado para acotar su solución. Eso es lo que harás en esta sección: aprenderás algunas técnicas para determinar qué tan lento o rápido puede ser

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos un algoritmo, independientemente de la computadora o lenguaje de programación que se esté usando. Ya se ha hablado y referenciado el modelo de Turing como modelo de cómputo. El siguiente ejemplo no es exactamente el modelo de Turing, pero en algo se asemeja. Antes de que existieran las hojas de cálculo, los contadores solían usar unas hojas tabuladas para escribir sus datos. Este modelo de cómputo va a ser una hoja de cálculo donde las instrucciones las irás definiendo conforme cada problema vaya apareciendo. La mención sobre las hojas tabuladas es porque un modelo de cómputo usando una computadora puede no ser el más adecuado, pero si esto te llega a molestar, puedes pensar que estás trabajando con las limitaciones que una hoja tabular impone. La motivación para ocupar una hoja de cálculo como modelo de cómputo es que la memoria de una computadora es básicamente un arreglo de celdas direccionadas donde tus datos existen y son manipulados. Ejemplo: Considera los siguientes datos en una hoja de cálculo

1 2 3 4 5

12 6 9 21 6

Donde la primer columna indica el índice del elemento, y la segunda índica el valor que ese índice representa. En todos los aspectos de computación, encontrar algún valor particular de un arreglo es particularmente requerido. A continuación puedes ver un par de algoritmos diseñados para obtener el máximo de ese arreglo. Están escritos en pseudocódigo, el cual seguramente ya conoces, pero lo se volverá a describir, ya que no es propiamente un lenguaje de programación, pero sí es lo suficientemente genérico para poder ser implementado en uno, salvo algunos aspectos técnicos dependientes de cada lenguaje. Algoritmo maximo1 Recibe: Arreglo K Entrega: m, K[m] tal que 1>> >>> >>> >>>

a = n = L = for

M[0] len(M) range(n) i in L: if (M[i] > 5): M[i] += a

>>> Figura 10. Segmento de código

El segmento de código anterior lleva a cabo una operación al inicio, cuando hace la asignación a = M[0] El conteo de operaciones va en 2, ya que se hizo una asignación y una lectura de un arreglo. Después se hace una asignación a n = len(M), lo que aumenta el contador a tres instrucciones ejecutadas. Después se tiene una instrucción que asigna a L un arreglo con n enteros, lo que significa aumentar el contador en 1, con lo que se llega a 4, pero después la instrucción range(n) crea un arreglo con n elementos enteros, lo que va a tomar n pasos. Entonces, el contador va en .

 

Después se ejecuta una operación un tanto invisible, pero que se sabe que sucede, y es la asignación a i del primer elemento del rango de valores en L; el contador va ahora en . En este punto es donde la tarea de contar instrucciones empieza a complicarse un poco y esto es por el uso del for, que son muy comunes en cualquier programa, y como puedes advertir, van a aumentar el número de instrucciones ejecutadas tantas veces como entres en el for. En

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos



este particular caso no vas a considerar la última instrucción contada, y la vas a cambiar por n asignaciones a la variable i, con lo que tendrás al contador en . Si no entras ninguna vez al cuerpo del for, obtenedrás el siguiente conteo de instrucciones:

 

Tiempo de ejecución máximo  Ahora, el contar las instrucciones que están contenidas en el cuerpo del for, como puedes ver, no es trivial, ya que las condiciones pueden darse para que entre una sola vez al if, o bien, tantas veces como elementos tenga el arreglo. ¿Qué debes considerar entonces? Lo que usualmente se hace es tomar el peor de los casos, es decir, vas a considerar que entras tantas veces como elementos tiene el arreglo. Entonces, si entras a la evaluación del if tantas veces como elementos, tienes que contar dos instrucciones más, una por la recuperación del elemento del arreglo M y otra por la comparación, por cada vez que entras al ciclo; en otras palabras, tienes que agregar  pasos al contador



 

Pero, como estás considerando el tiempo máximo, también debes considerar que la evaluación del if es verdadera, con lo que debes considerar que entre al cuerpo del if, lo que arroja otras tres instrucciones más por cada vez que entres al for: una por la operación aritmética de sumar 4, otra por recuperar el valor de M[i], y la última por la asignación; entonces, el conteo de instrucciones final queda como

 

 A esto se le conoce como tiempo máximo de ejecución, y se define de la siguiente manera

     || 

Definición El tiempo máximo de ejecución  es el máximo número de pasos necesarios para ejecutar un algoritmo  con una entrada  de tamaño  en una máquina de Turing





(2)

Este es el tipo de análisis que vas a hacer con el código; lo irás haciendo más eficiente conforme vayas avanzando en la unidad. En este caso, obtuviste el tiempo de cómputo para el peor de los casos, aunque también se puede obtener el promedio de tiempo de cómputo, pero este suele ser muy engorroso y, por lo pronto, no lo vas a considerar. Comportamiento asintótico Una pregunta que vale la pena hacerse es, ¿qué tan importante es la expresión recién encontrada para el segmento de código presentado si para todo fin práctico sucede instantáneamente a nuestros ojos? La pregunta es válida, y la respuesta es que es irrelevante para arreglos pequeños, pero no así para arreglos cada vez más grandes. Suponiendo este

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos último caso, en el que cada vez el arreglo es más grande, puedes notar que el término 4 de la expresión anterior es cada vez menos significativo que el término 7n.



Precisamente porque piensas usar los algoritmos con entradas grandes, es decir,  los términos independientes no hacen diferencia alguna; de hecho, únicamente vas a considerar el término dominante del tiempo de cómputo, lo que vas a representar mediante la notación , denotada como la asíntota de . La asíntota del ejemplo revisado es

( )

 

Esto recibe una definición específica y se le denomina Cota Superior Asintótica un término más informal y popular, “O grande”.

( )

, o bien,

 

Definición Se dice que una función  está acotada superiormente, o dominada, por la función  si para algún  y para una constante  se cumple que

 

y se denota como

   ( ) ) (    

 es de orden

Usualmente, en lugar de asintóticamente.

(3)

, o bien,.

 vas a usar a

 como la función que quieres acotar

    

Ejemplo. Para el caso que se ha expuesto, si tomas que cumpla

, entonces tienes que encontrar



lo que implica que puedes escoger  como:

Se usa el menor o igual porque, aunque basta con que lo cumpla, también interesan las constantes que hacen que hagan a la asíntota mayor que la función que quieres acotar.

    

   

Ejemplo: La función  es de orden . Para poder ver esto tienes escoger  Entonces podrás verificar fácilmente que El significado gráfico de esto lo puedes ver a continuación

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

 

 y

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

 

Figura 11. Dominio en el crecimiento de la función en verde que es



   ./



  sobre la roja que representa a

  

 

Ejemplo: La función  tal que

 NO es de orden

Esto significa que que no es posible.

, pero esta constante crece conforme aumenta el valor de , lo

( )

. Si así lo fuera, entonces existen

 y

Otra definición que conviene introducir es la , que funciona como complemento al de , es decir, la función que acota inferiormente a

( )

    (    ) ( )  ( )



Definición Se dice que una función  está acotada inferiormente por la función algún  y para una constante  se cumple que



y se denota como la omega de

 es

 

 si para (4)

, o bien,



Se usa principalmente la definición de

 por las siguientes razones que caracterizan a

Cuando se dice que equivalentes

, significa alguna de las siguientes proposiciones

 es de orden

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

.

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

   

 

 no crece más rápido que o bien  crece a lo más tan rápido que

Entonces, se puede empezar a trabajar con esta definición para construir algoritmos más complejos y determinar el orden por el que están dominados. Es decir, surge la pregunta, ¿cuál es el orden de un algoritmo que consta de ejecutar un algoritmo seguido de otro?



        ( ) ()  .( )()/

Teorema Si tienes dos algoritmos  y , cada uno con un tiempo máximo de cómputo  y  y de orden  y  respectivamente, entonces el orden del algoritmo , que consta de ejecutar secuencialmente cada uno de los dos algoritmos, es de . Demostración

La demostración es muy sencilla. El tiempo máximo de cómputo de Sabes que

    es

 ( )  ()          *+     *   *      +  * +    * +   * +   * +  

Esto quiere decir que existen

 y

(1)

.

 tales que

Escoge entonces a  para garantizar que tienes un punto en el que no se viole dicha definición. Sumando estas expresiones obtienes

Debido a que escogiste a

 puedes replantear la expresión como:

 Al construir así a  y  has demostrado que al concatenar  dos algoritmos, su complejidad queda acotada por la cota más grande de los algoritmos que la componen, i.e.

 

Otra propiedad fundamental es la de ejecutar un algoritmo  dentro del algoritmo , un ejemplo de lo cual sería ejecutar un for dentro de otro  for. ¿Qué complejidad tiene asociada esta composición de algoritmos?

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

      ( )  ()



Teorema Si tienes dos algoritmos  y , cada uno con un tiempo máximo de cómputo  y  y de orden  y  respectivamente, entonces el orden del algoritmo , que consta de ejecutar  como una instrucción de , tiene una complejidad de .

   ( )

 



           ( )  ()          *+     ( )  ( ) 

(2)

Demostración

El tiempo máximo de cómputo del algoritmo  es de ; esto se sigue de que dentro de la ejecución de  vas a ejecutar  como una de las instrucciones que lo componen. Se sabe que

Esto quiere decir que existen

 y

 tales que

Si escoges , entonces puedes garantizar que tienes un punto en el que no se viola esa definición, y por lo tanto, puedes multiplicar estas dos expresiones.

 Así, pues, has construido

 y

 para los que se cumple que

Las funciones que usualmente se ocupan para establecer un marco de referencia son las siguientes:

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

Figura 12. Funciones paradigmáticas para clasificar ordenes de complejidad.

 Aunque la función en rojo es el 5, es en realidad la función constante. En términos de O grande estas funciones son

      Orden

Nombre Constante Logarítmica Cuasi lineal Cuadrática Cúbica Exponencial



Donde claramente la que crece más rápido es . Pero es interesante resaltar el hecho que se aprecia en la gráfica. De hecho, ese crecimiento es exponencial según la entrada, y eso es fácil de ver, ya que si la complejidad de un algoritmo crece tan rápido, apenas doblando el tamaño de su entrada, es fácil suponer que los recursos para ejecutarlo se terminarán igual de pronto.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Todos estos tipos de resultados son el fundamento para determinar qué tanto puede tardar un algoritmo en ejecutarse en toda su entrada, y a partir de este esquema de comparación se podrá determinar si los algoritmos son más o menos eficientes.

1.3.2. Algoritmos iterativos y recursivos Este par de resultados nos pueden servir para aplicarlos en todo tipo de algoritmos, pero las secciones más complicadas y también las más representativas en una amplia gama de problemas son la recursión y las iteraciones. Iteraciones El caso más sencillo es el expuesto por las iteraciones, además que ya se ha visto un poco en el primer teorema revisado, pero se examinará particularmente un ejemplo donde se usan un par de for anidados uno en el otro. Supón que tienes un arreglo con n elementos llamado M y quieres buscar qué elementos están duplicados en él. Esto significa comparar el primer elemento con el resto de los elementos en el arreglo y así hasta llegar al final; el siguiente código de Python expresa esta idea. In [14]: import random In [15]: M = [random.randint(1,9) for x in range(10)] In [16]: M Out[16]: [1, 4, 1, 4, 8, 9, 3, 7, 7, 3] In [17]: for i in range(len(M)): ....: for j in range(len(M)): ....: if M[i]==M[j]: ....: print “duplicado “, M[i] Figura 13. Ejemplo de for anidado.

En este segmento de código puedes ver que en la línea In[17] hay un for dentro de otro. Primero analizaás el más anidado. En el peor de los casos se va a ejecutar la comparación y la línea del print, esas son dos líneas por cada elemento de M.



Y este for, el que regula el índice j, se va a ejecutar tantas veces como el que regula el índice i, por lo que la cuenta final queda



Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Es fácil encontrar



 tal que

 

De hecho, usando los teoremas antes vistos, y de aquí en adelante usarás como resultado, que cuando tengas una serie de fors anidados ( de ellos, por ejemplo), donde todos recorran un arreglo, este segmento de algoritmo tendrá una complejidad polinomial de . Usualmente , por lo tanto, en fors anidados, espera tener una complejidad de .



  

Recursividad Un algoritmo recursivo consta de dos características muy importantes. La primera es que hace una llamada a sí mismo con un argumento reducido, y la segunda es que contiene una cláusula de escape a la cual se llega cuando el argumento rebasa algún umbral predefinido.

          

El ejemplo de función recursiva arriba escrito es la forma más básica de una función recursiva en el que la cláusula de escape se alcanza si el argumento rebasa un umbral que usualmente puede pensarse como un entero, al que, en otro caso, le vas restando para poder alcanzar la cláusula de escape. Esto no tiene que ser forzosamente, ya que la función que altere el argumento no necesariamente tiene que ser una resta, sino puede ser una función de tal forma que converja a .



El primer algoritmo recursivo que puedes encontrar es el cálculo de factorial, que es básicamente el primer ejemplo de recursividad que siempre se revisa. Su definición matemática es la siguiente

   

 Aunque también se puede definir usando el operador de producto, por lo pronto esta definición te ayudará a hacer tu punto. A continuación puedes ver el código en Python: In [1]: def factorial(n): ...: return n*factorial(n-1) if n>1 else 1 Figura 14. Definición de factorial en Python con una forma alternativa del if -else



El análisis de este particular algoritmo no es difícil. En el caso más básico, si n=1 la complejidad es una constante , de no serlo, entonces, al pasar un argumento distinto de 1, la función se ejecutará tantas veces como lo indique el argumento, i.e.

 

En general

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

           . /      {. /    

Entonces es fácil ver que la complejidad de factorial es

 Ahora analizarás este ejemplo. Obtener la potencia de un número mediante la función pow

En Python este código se vería como In [21]: def pow(x,n): ....: if n==0: ....: return 1 ....: elif n==1: ....: return x ....: elif n%2==0: ....:

....: ....:

print “par”

return pow(x*x,n/2) elif n%1==0:

....:

print “impar”

....: ....:

return pow(x*x,n/2)*x

Figura 15. Código recursivo para calcular la potencia de un número en Python.

Para analizar este algoritmo vas a determinar el tiempo máximo de cómputo. Para lograr esto es fácil ver que el peor de los casos es cuando n es impar, ya que se llevan a cabo tres multiplicaciones aparte de la reducción del argumento. Lo que significa que



 ./

Donde  es el tiempo que le lleve a la computadora realizar las operaciones básicas. La expresión anterior no está describiendo el hecho de que el tiempo máximo de cómputo va a resultar en el tiempo que tarde en computarse la misma función, pero con una entrada dividida a la mitad más el tiempo que se tarda en completar la operación sobre tres números. Ese fue el caso para una iteración; al repetir el proceso varias veces obtienes lo siguiente:

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

     

 ./ 0 ./1 ./  0 ./1 ./ 0 ./1 . /  0 ./1 ./

Es decir, en cada momento lo peor que tienes que calcular es la mitad del argumento más una constante por algunas operaciones elementales involucradas en ese paso de cómputo. En la función original se sabe que si , entonces . Esto, en términos de tiempo de cómputo, significa



        

Como se desconoce la constante , puedes usar este hecho para conocerla. Para poder obtener , entonces   requieres que

 . /

Por lo tanto, la expresión original se convierte en

Es decir, la complejidad del algoritmo es logarítmica y se puede afirmar que su orden es como sigue



No es difícil comprender este hecho, ya que en cada paso del que elegiste como de mayor consumo de tiempo computacional, el tamaño del argumento se reduce a la mitad. En la siguiente sección revisarás otro ejemplo en el que se repite este hecho. En general, el análisis de algoritmos recursivos no es sencillo, y de los ejemplos que has visto hasta el momento es fácil advertir la importancia que tiene el análisis combinatorio en estos temas, particularmente el uso de funciones generadoras. Para analizar algoritmos recursivos usualmente se hace uso del llamado teorema maestro. Del que puedes ver una versión simplificada a continuación:

 

. También sean   y *+. Si tenemos la 2⁄   

Teorema Maestro Sean  y  tal que relación recurrente de la forma



 

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

(5)

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

            ( )  (    )  

Entonces, para  potencia de  se cumple alguna de estas proposiciones a) Si b) Si  c) Si , Este teorema resulta muy práctico cuando se analizan algoritmos recursivos. Cabe aclarar que la definición de  significa que la complejidad del algoritmo comparado es exactamente . La demostración de este teorema no la revisarás en este contenido, ya que se requieren más herramientas y experiencia analizando algoritmos; su demostración es usualmente parte de un curso de análisis de algoritmos. Un problema muy particular de los algoritmos recursivos es que, aunque pueden ser muy eficientes y elegantes, tienen que usar una estructura de los sistemas operativos denominada pila de llamadas. En esta estructura el sistema va almacenando la última función a la que debe contestarle; entonces, para argumentos no muy grandes de una función recursiva, esta estructura se agota, así que al programar algoritmos recursivos hay que ser muy cuidadosos con la cantidad de llamadas que en promedio vas a hacer con ella.

1.3.3. Diseño de algoritmos Como podrás ver, el análisis de algoritmos no es una tarea lineal, pero tampoco lo es su diseño. Conjuntando los elementos que acabas de ver, podrás incorporarlos en una serie de consejos que puedes seguir para diseñar algoritmos eficientemente, tomando en cuenta siempre el crecimiento del tiempo máximo de cómputo. 1. Planificación: Haz un esquema del inicio de tu programa y los estados por los que debe transitar. Una buena opción es seguir la notación del diseño de autómatas. 2. Encapsulamiento: Procura crear TDA en los que encapsules todas las funciones que vayas a usar con ellos, pues hacen un programa más fácil de usar y de depurar. 3. Acoplamiento: Evita la interdependencia entre módulos. Esto se logra un poco al implementar TDA, pero de todas maneras es fácil caer en el hábito de hacer llamadas directas entre módulos que deberían ser independientes. 4. Reusa: Usa el código que ya exista, así como las bibliotecas de funciones ya programadas. No es necesario reinventar el hilo negro cuando tantas funciones ya están programadas. 5. Itera: Procura ocupar iteraciones sobre recursividad, los algoritmos de optimización de los compiladores están muy bien diseñados para las iteraciones, mas no así para el for.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Eliminación de la recursión de cola



Una forma para convertir un algoritmo recursivo y convertirlo en iterativo es lo que se hace en la eliminación de la recursión de cola. Cuando se tiene un procedimiento  con una llamada a sí mismo, se puede eliminar ese llamado pasando otra variable que sirva de contador y el argumento modificado. Considera el siguiente código de Fibonacci en Python In [1]: def fib(n): ...: if n==0: ...: return 1 ...: elif n==1: ...: return 1 ...: else: ...: return fib(n-1)+fib(n-2) ...: Figura 16. Código de Fibonacci(n) recursivo estándar.

Se puede eliminar la llamada a fib dentro del mismo código, como puedes ver en el siguiente ejemplo de Fibonacci iterativo. In [6]: def fib2(n,v1=1,v2=1): ...: L = [v1,v2] ...: if n>=0 and n=2: ...: for i in range(n-2): L.append(L[-1]+L[-2]) ...: return L ...: Figura 17. Código de Fibonacci(n) iterativo.

Se pueden evitar las llamadas recursivas incorporando una variable que guarde los resultados de llamadas internas que serían regresadas por las llamadas de fib(n). En este caso se llama L, y como puedes ver, vas guardando ahí los subsecuentes resultados que de otra forma se calcularían recursivamente. Esto permite calcular valores mucho más grandes de Fibonacci que de otra forma se acabarían la pila de llamadas a funciones de Python (por ejemplo, intenta calcular fib(40) y fib2(40)). Divide y vencerás Otra técnica muy común para resolver problemas es la de divide y vencerás, en la que dividimos el problema a la mano en subproblemas más básicos. Por ejemplo, multiplicar dos enteros muy grandes, realmente grandes en realidad.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

 ⁄     ⁄     ⁄  

Si P y Q son enteros grandes de longitud  bits, se puede convertirlos en

La multiplicación queda como

Con lo que se hacen tres sumas de

 bits máximo y dos corrimientos.

Programación dinámica Este método es bastante popular. Dividir un problema en subproblemas más sencillos no siempre es posible, pero, en cambio, lo que sucede es que se tienen casos distintos, para los cuales tendrás que resolver alguno de estos casos repetidamente. Entonces, lo que conviene es crear una tabla con las soluciones para dicho problema para simplemente buscar su solución en el momento requerido y después usarla. Esto funciona bien cuando dichos casos son parte interna de algún otro caso; entonces, recuperar su información es más sencillo en vez de crear un crecimiento exponencial de llamadas a diferentes algoritmos. Para hacer una implementación usando programación dinámica, usualmente se tienen que ir guardando los resultados previos en una tabla. Backtracking Es una estrategia de programación para hacer búsquedas exhaustivas de todas las soluciones posibles a un problema, el cual está compuesto por varios pasos intermedios. Consiste en probar una solución, y si no se viola la función a optimizar, entonces se agrega otro elemento a probar. Un ejemplo clásico de esto es el problema de las nueve reinas. Este problema consiste en colocar nueve reinas en un tablero de ajedrez, considerando su capacidad de moverse dentro de él, de tal forma que no se ataquen. La estrategia es la siguiente: 1. Colocar una en el primer escaque 2. Si no se viola la función a optimizar 3. Colocar la siguiente en un escaque no visto por la reina anterior 4. Si no hay forma de poner la reina siguiente en otro escaque 5. Quitar la reina anterior y ponerla en el escaque siguiente al que fue ubicada 6. Regresar al paso 2.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos 1.3.4. Complejidad en ordenamientos y búsquedas Dos de las principales tareas que llevan a cabo cualquier algoritmo en cualquier programa en cualquier computadora son   Ordenar   Buscar

 



Puedes apreciar la importancia de contrastar estos dos métodos tomando en cuenta que la recuperación de un elemento de un arreglo tiene complejidad . De hecho, parece que intuitivamente son los algoritmos que van a permitir llevar a cabo buena parte del cómputo que hagas. Búsqueda binaria Una de las búsquedas más populares es la búsqueda binaria. Esta se hace en alguna estructura Q en la que, si tomas un elemento c, sabes que Q[a]>> for i in range(24): L.append(L[-1]+L[-2]) >>> k = L[23] >>> x = b.binaria(L, k) Arreglo completo Llamada Derecha Llamada Derecha Llamada Derecha Llamada Izquierda Llamada Resultado >>> x 23

Figura 18. Algoritmo de búsqueda binaria en Python usado en la lista de número de Fibonacci con 24 elementos.



La complejidad de este algoritmo, como puedes intuir, es . El análisis no es realmente complicado, ya que se parece mucho al del ejemplo de elevar un número a alguna potencia. El tiempo total de cómputo de este algoritmo es el que le lleve examinar la mitad restante de su entrada más una constante , donde pones todas las asignaciones y operaciones que sean de complejidad , excepto por la cláusula de escape, que es la operación en la que recuperas el elemento buscado del arreglo L.





⁄      ⁄

Entonces, para las diferentes iteraciones obtienes la siguiente progresión, considerando el peor de los casos

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos

  ,⁄-⁄     ⁄  

De la misma forma, como la última iteración es

Entonces, como era de esperarse Ordenamientos

, implica que



Otra de las tareas más socorridas en computación son los ordenamientos. La tarea de ordenar consiste en tomar un arreglo A donde a cada índice se le denomina  llave, y el contenido del arreglo es el valor sobre el cual se impondrá el orden. Uno de los ejemplos más sencillos de ordenamiento es Bubble Sort . La idea es muy sencilla. únicamente se tiene que recorrer el arreglo en reversa, desde atrás hacia adelante, para ir intercambiando los elementos que sean mayores y que, por lo tanto, correspondan al final del arreglo; y esto lo debes hacer de manera progresiva por todos los elementos dejando fijos los ya ordenados. def bubblesort(A): n =len(A) for i in range(n-1): for j in range(n-1, i, -1): if A[j-1] > A[j]: temp=A[j-1] A[j-1]=A[j] A[j] = temp

Figura 19. Código de Bubble Sort en Python.

El peor de los casos es que este algoritmo entre todas las veces a la sección de intercambio, lo que está determinado por el for interno al que se entra tantas veces como lo indique el for externo.

 ⁄ ⁄ ⁄ ∑    Esto es un orden de complejidad cuadrática. lo que significa  . Pero no es el único algoritmo de ordenamiento; existen varios otros.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Para saber más Mañas, José. Análisis de algoritmos: Complejidad . En línea http://www.lab.dit.upm.es/~lprg/material/apuntes/o/index.html#s4 Zindros, Dyonisis. A gente introduction to Algorithm Complexity Analysis . En línea http://discrete.gr/complexity/ .

Actividad 3. Análisis de complejidad En esta actividad realizarás un análisis de complejidad sobre un algoritmo. Instrucciones 1. Descarga el archivo “ Act3. Análisis de complejidad  . ”  

2. Lee el contenido atendiendo a las instrucciones y sugerencias que se hacen. 3. Crea un script de Python donde guardes la función y ejemplos que se te piden. 4. Guarda tu documento con la siguiente nomenclatura: MCOM2_U1_A3_XXYZ. 5. Espera la retroalimentación de tu Facilitador(a). * Recuerda consultar la Escala de evaluación  de la actividad para saber qué aspectos se tomarán en cuenta para su revisión.

1.4. Estructuras de datos Otro elemento que va a asistir a crear y diseñar un algoritmo más eficiente es la utilización de estructuras de datos específicas. Las estructuras de datos son arreglos específicos de tus datos. Un par de estructuras de datos que ya conoces son los vectores y las matrices en el ámbito de las matemáticas, y seguramente ya has usado arreglos en computación. Pero hay diferentes estructuras para diferentes problemas. En esta unidad repasarás las estructuras de datos más conocidas, que van desde las listas hasta los árboles.

1.4.1. Arreglos y listas Dentro de las estructuras más básicas que vas a encontrar es la de una lista ligada. La idea principal de este par de estructuras es la de dar una secuencia lógica a un grupo de registros,

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos aspecto que puede obviarse con los arreglos donde la secuencia viene dada por los índices del arreglo. En Python, el uso de listas es muy común y tiene todos los métodos que podrías implementar no tan naturalmente en otros lenguajes, pero lo harás con el fin de mostrar cómo funciona una lista ligada. Este puede ser un tema un tanto engorroso pero sencillo al final.

Figura 20. Diagrama de una lista ligada. a) Lista de registros ligada apuntando a un registro específico b) borrado de un registro c) inserción de un registro

Lo que harás en este caso será diseñar un TDA de tipo lista para que controle todas las operaciones referentes a ella; esto lo convertirás en un par de clases funcionales y operables en Python. En una lista ligada, cada elemento tiene registrado qué elemento le sigue. Entonces, para problemas donde necesitas obviar la secuencia de los elementos, viene muy bien, por ejemplo, los días de la semana. En el siguiente código puedes ver la implementación de la misma junto con la clase registro que es donde guardarás los datos. class registro(object): def __init__(self,t):

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos self.c = t self.s = None def siguiente(self,t): self.s = t def contenido(self): return self.c def sigue(self): return self.s class lista(object): def __init__(self): self.h = None #cabeza self.a = self.h #celda actual self.pos = 0 self.conteo = 0 def cuantos(self): return self.conteo def posicion(self): return self.pos def ubica(self,p): if p > 0 and p 1 and p < self.conteo: self.a = self.h self.pos = 1 while self.pos < p-1: self.siguiente() self.a.siguiente(self.a.siguiente().siguiente()) self.conteo -= 1 def imprime(self): self.a = self.h i = 1 while i < self.conteo: print self.contenido() self.siguiente() i += 1

Figura 21. Código para implementar una lista simplementa ligada en Python junto con una c lase de registro ocupada para las diferentes celdas de la lista.



Hay dos tipos de métodos en esta implementación. Aquellos de complejidad constante como son inserta (o los que reportan un valor) y los de complejidad , entre éstos están: ubica, imprime, borra. Y es que todos estos tienen que recorrer toda la lista para poder llevar a cabo su operación.  Aquí puedes ver el uso de los registros. In [49]: t = b.registro(5) In [50]: s = b.registro(3) In [51]: r = b.registro(1) In [52]: u = b.registro(7)

Figura 22. Creación de los registros

Y aquí la implementación de la lista In [53]: L = b.lista() In [54]: L.conteo Out[54]: 0

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos In [55]: L.inserta® In [56]: L.inserta(s) In [57]: L.inserta(t) In [58]: L.inserta(u) In [59]: L.h.contenido() Out[59]: 1 In [60]: L.h.sigue().contenido() Out[60]: 3 In [61]: L.h.sigue().sigue().contenido() Out[61]: 5 In [62]: L.h.sigue().sigue().sigue().contenido() Out[62]: 7

Figura 23. Creación y uso de la lista

Colas Las listas ligadas tienen un uso aplicado para muchos procesos, por ejemplo, un tipo de listas ligadas es una cola. Las colas son un tipo de lista ligada denominada FIFO, que significa First In First Out, que es básicamente el funcionamiento que todos identifican con las filas. El primero que se forma es el primero en salir. ¿Esto cómo se refleja en la implementación? ¿Cómo asignas un orden a tu lista? Hasta el momento casi todo está implementado, ya que cuando se agrega un elemento a la lista es en el orden en el que van llegando a ella; lo único que falta es un método que los extraiga de acuerdo a este principio. Una propuesta es tener un método pop que elimine el primer elemento de la lista. def pop(self): if self.conteo > 1: self.borra(1)

Figura 24. Metodo pop para agregarlo a la implementación de lista ligada.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos 1.4.2. Pilas Las pilas o stacks son otro tipo de estructura que puede ser implementado con una lista o con un arreglo. A diferencia de las colas, estas estructuras son del tipo FILO, First In Last Out . No es una fila particularmente intuitiva, al menos no en el andar cotidiano, pero sí lo es en otros aspectos. Considera el siguiente conjunto de paréntesis ((([(([])[[ ]])])[[]]))

El conjunto de paréntesis anterior está bien apareado. La estructura de datos que puede ayudar a llevar bien la cuenta de este fenómeno es precisamente una pila; esto se debe a que cada vez que cierras un paréntesis, su contraparte que lo abre debe ser el inmediato anterior, es decir, el último elemento que entró en la pila es el primero en salir (complementariamente el primero en entrar debe ser el último en salir).  A continuación puedes ver la implementación de un stack, usando una lista como estructura interna para guardar los elementos del stack. class stack: def __init__(self): self.elementos = [] def es_vacio(self): return self.elementos == [] def push(self, elem): self.elementos.append(elem) def pop(self): return self.elementos.pop() def peek(self): return self.items[len(self.items)-1] def tamano(self): return len(self.items)

Figura 25. Implementación de un stack en Python

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Los nombres de las funciones son notación estándar en el uso de pilas y colas. Un programa que podrías implementar con una pila es una calculadora con notación sufija. Por ejemplo, la instrucción 46+ Usando notación sufija, la expresión anterior significa que debes sumar el 4 y 6 obteniendo 10. La suma se puede llevar a cabo en el momento en el que detectes el símbolo +; en ese momento sacas los dos elementos superiores del stack y los sumas.

Figura 26. Calculadora con notación sufija que implementa una calculadora, la operación usualmente depende del programador, puede suceder apenas recibamos un símbolo de operación o hasta el final que se llene la pila. a) Esquema de una pila b) Inserción c) Ejemplo de pop.

1.4.3. Heaps El término heap significa montículo en inglés y refleja en buena medida lo que deseas modelar con esta estructura, un montículo donde lo más importante que requieras esté hasta el tope del montículo. Incluso existe una expresión en inglés con este significado que va: top of the heap. Los heaps son otro tipo de listas, usualmente conocidos como listas con prioridad. Por ejemplo, una lista de urgencias en un hospital, aunque todos los pacientes llegan en un orden cronológico bien determinado, no todos tienen la misma prisa por ser atendidos; hay unos casos que son mucho más urgentes.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Esto lo puedes denotar en un arreglo. Considera el siguiente ejemplo: 5 1 4 6 3 8 2 1

Si el tamaño del número en cada entrada del arreglo anterior determinara el grado de importancia de esa entrada, entonces el 8 sería el elemento más importante, mismo que puedes encontrar con tiempo  al escanear el arreglo de números. O bien, puedes crear una estructura de datos que siempre los mantenga ordenados según su prioridad.



1.4.4. Árboles Los árboles son de las estructuras más cómodas y eficientes para representar datos, ya que implícitamente representan una jerarquía en ellos, y ya no son lineales como todas las estructuras que has visto hasta ahora. De hecho, ya examinaste previamente una de las ventajas de tener la información estructurada en un árbol cuando revisaste el algoritmo de la búsqueda binaria. En este algoritmo descartabas la mitad del espacio de búsqueda que no te interesaba, y de esta manera la complejidad del algoritmo es de .



Como bien sabes, un árbol es una gráfica sin ciclos que consta de un conjunto de nodos y de aristas.



Por ejemplo, considera el siguiente árbol:

Figura 27. Ejemplo de un árbol

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos En este árbol el nodo raíz A tiene iluminado de verde toda su rama izquierda, y de naranja toda la derecha. Un problema que aparece inmediatamente es el de cómo vas a representar una estructura de dimensión 2 en Python. Una opción es hacer uso de las ventajas del manejo de listas y hacer una lista de listas. Por ejemplo, el árbol de la figura puede ser representado así: In [4]: Arbol = ['A', ...: ['B', ['F', [], [] ], []], ...: ['C', ['G', [], [] ], ['H',[],[]]] ...: ]

En esta sección revisarás la implementación de un árbol binario, aunque hay varios tipos de árboles. def ArbolBinario(r): return [r, [], []] def ramaIzquierda(raiz,rama): t = root.pop(1) if len(t) > 1: raiz.insert(1,[ramaI,t,[]]) else: raiz.insert(1,[ramaI, [], []]) return raiz def ramaDerecha(raiz,rama): t = raiz.pop(2) if len(t) > 1: raiz.insert(2,[rama,[],t]) else: raiz.insert(2,[rama,[],[]]) return raiz def pon_raiz(arbol): return arbol[0]

Figura 28. Implementación de árbol binario en Python.

Como puedes ver, la búsqueda en este árbol reduce el tiempo de cómputo en potencias de dos, despreciando todo la rama que no es de tu interés.

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos 1.4.5. Funciones y tablas de hash



Hasta ahora las estructuras que has visto han podido reducir, en el mejor de los casos, el tiempo de cómputo y búsqueda a , en buena medida por el orden que guarda el dominio que toman las llaves de tus elementos. Si las llaves fueran las entradas del arreglo de dónde quieres recuperar la información, entonces todo estaría resuelto, ya que con simplemente incorporar esa llave al hipotético arreglo que esté guardando la información, obtendrías lo que buscas en orden , que es la gran gracia de los arreglos: su rapidez para recuperar información.



Es por esto que serviría tener un arreglo de este tipo, donde pusieras la información para ser recuperada. El problema con este planteamiento es que, si haces un arreglo, por ejemplo, para una agenda telefónica con números de diez dígitos, donde el número sea la llave para recuperar la información del dueño de ese teléfono, entonces necesitarías alrededor de entradas para poder guardar esa hipotética agenda telefónica, y lo peor es que gran parte de ella estará vacía. Pero qué tal si te aferras un poco a la idea de un arreglo para tener complejidad constante para recuperar la información, pero con menos llaves. ¿Cómo puedes hacer esto posible?



Una opción, tal vez no muy buena, pero muy ilustrativa, es quedarte con los últimos dos dígitos de cada número y usar eso como llave del arreglo, por ejemplo:

               

 

y así para cada uno de los teléfonos de tu agenda. Entonces, a través de un mapeo  estás reduciendo el espacio de búsqueda de todos los posibles números telefónicos para quedarte únicamente con un elemento que funciona como la llave de un arreglo, y que los identifica con complejidad .



El problema radica en que se puede dar este caso

     

- en el que otro elemento sea mapeado a la misma entrada del arreglo y que ya esté ocupado.  A este fenómeno se le denomina colisión, en tanto que al arreglo que estás construyendo se le denomina tabla de h a s h ,  y a la función que se encarga de mapear el objeto de tu interés en una entrada de la tabla se le llama función h a s h . 

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos Función h a s h  



,-



Formalmente una función de hash ( ) se define considerando que va a recibir un argumento de entrada y producir un número de clase en el rango , donde  es el número total de clases. Esto es, requieres diseñar una función que mapee todo el dominio en un conjunto de cardinalidad mucho menor; para esto requieres que el mapeo sea, por lo menos, inyectivo, y aunque es imposible, también sería bueno que fuera suprayectivo, o por lo menos debes procurar que el número de colisiones sea mínimo. Ejemplo: El algoritmo MD5 produce un entero único dado una cierta entrada de bits siguiente algoritmo.

 

con el

1) Se extiende el mensaje hasta hacerlo congruente 448 módulo 512 concatená ndole “1” y “0”

2) Se concatena un número de 64 bits que representa la longitud del mensaje 3) Se usa un búfer de cuatro palabras (arreglos de ocho bits) para representar el mensaje 4) Se calcula el contenido de cada búfer con las funciones

                  Entonces, con estos cuatro bytes concatenados se obtiene un identificador único de cada entrada. Un mal ejemplo podría ser el siguiente. Para identificar una cadena de números, por ejemplo, los números telefónicos, podrías sumar su valor entero, pero esto produciría demasiadas colisiones, ya que con mucha facilidad podrías obtener el mismo entero a partir de una simple suma. Colisiones Existen tres formas de lidiar con las colisiones en la tabla. - Direccionamiento abierto: Se busca un espacio libre dentro de la tabla para colocar la nueva entrada. - Encadenamiento: La entrada de la tabla contiene una lista y cada nuevo elemento que llega se coloca como elemento de la misma. Para no perder eficiencia no deben ser demasiados elementos en cada lista. - Encadenamiento ligado ( coalesced chaining ): Este es un híbrido de los dos primeros métodos en el que asignas el objeto a guardar en otra llave dentro de la tabla, pero

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos guardando la dirección en la entrada original y estableciendo una liga a la nueva entrada.

Para saber más Know thy complexities. En línea http://bigocheatsheet.com/ Data structures visualizations . En línea http://www.cs.usfca.edu/~galles/visualization/ Morales Luna, G. Computabilidad y Complejidad . En línea

http://cs.cinvestav.mx/~gmorales/complex/complex.html

Actividad 4. Estructuras de datos  A través de esta actividad, utilizarás estructuras de datos para resolver problemas genéricos, dependiendo el uso que se le asigne. Para ello: 1. Descarga el archivo “ Act4. Estructuras de datos

”  

2. Lee el contenido atendiendo a las instrucciones y sugerencias que se hacen. 3. Crea un script de Python donde guardes la función y ejemplos que se te piden. 4. Guarda tu documento con la siguiente nomenclatura: MCOM2_U1_A4_XXYZ. 5. Envía el documento a tu Facilitador(a) y espera su retroalimentación. * Recuerda consultar la Escala de evaluación  de la actividad para saber qué aspectos se tomarán en cuenta para su revisión.

Autoevaluación Para reforzar los conocimientos relacionados con los temas que se abordaron en esta unidad del curso, es necesario que resuelvas la autoevaluación. Ingresa al Aula virtual para realizar tu actividad.

Evidencia de aprendizaje. Diseño de algoritmo  A través de esta actividad podrás crear un TDA y su respectiva clase en Phyton, y por medio de ella, responder  a las interrogantes que se presentan. Para ello:

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

Computación II Unidad 1. Estructuras de datos y análisis de algoritmos 1. Descarga el archivo EA. Diseño de algoritmo. 2. Resuelve inciso que se presenta en el documento descargable. 3. Escribe tus preguntas y respuestas en un documento, y si existiese alguna función, guárdala en un archivo .py 4. Guarda tu documento y archivo con la siguiente nomenclatura: MCOM2_U1_EA_XXYZ. 5. Envía tu archivo al Portafolio de evidencias  y espera la retroalimentación de tu Facilitador(a). Una vez que la tengas, atiende sus comentarios y reenvía la nueva versión de tu evidencia. Nota: No olvides consultar la Escala de evaluación  para conocer los criterios con que será evaluado tu trabajo.

Autorreflexiones Como parte de cada unidad, es importante que ingreses al foro Preguntas de autorreflexión  y leas los cuestionamientos que formuló tu Facilitador(a), ya que a partir de ellos debes elaborar tu Autorreflexión y enviarla mediante la herramienta  Autorreflexiones. No olvides que también se toman en cuenta para la calificación final .

Cierre de la Unidad En toda esta unidad acabas de ver un panorama un tanto extenso, pero básico, de la operación de una computadora y de la información que es capaz de manipular, así de cómo hacerlo eficientemente. Las estructuras de datos presentadas aquí no son ni por asomo las más sofisticadas o las únicas; de hecho, cada una de estas estructuras se puede sofisticar bastante más. Lo mismo pasa con los fundamentos de computación teórica presentados. Lo que viste en esta unidad es una base para el análisis y entendimiento de la manipulación de datos en la computadora de manera eficiente.

Para saber más Puedes consultar las siguientes ligas para extender cada tema Python Programming/Sequences . Wikibooks. En línea

http://en.wikibooks.org/wiki/Python_Programming/Sequences

Educación Abierta y a Distancia * Ciencias Exactas, Ingenierías y Tecnologías

View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF