En informática, la complejidad de los algoritmos se puede estudiar estimando la dependencia en- tre el tiempo que necesitan para procesar su entrada y el tamaño de ésta. El mejor rendimiento se obtiene si ambos son independientes: el tiempo es constante. En orden decreciente de eficiencia, otros algoritmos presentan dependencia logarítmica, polinómica (lineal, cuadrática, etc.) o expo- nencial. Para clasificar un algoritmo de esta forma, se elige una de sus instrucciones y se estima el tiempo empleado en ejecutarla mediante el número de veces que el algoritmo tiene que ejecu- tar dicha instrucción, se calcula el tiempo en función del tamaño de la entrada y se estudia su or-
den cuando la entrada se hace arbitrariamente grande.
A lo largo de este capítulo se usará n para representar el tamaño de la entrada. Para la esti- mación de los órdenes de eficiencia, los valores concretos de las constantes que aparecen en las funciones no son relevantes, por lo que la dependencia constante se representa como 1, la loga- rítmica como log(n), la lineal como n, la cuadrática como n2 y la exponencial como en.
Resulta útil considerar el peor tiempo posible (el tiempo utilizado en tratar la entrada que más dificultades plantea) y el tiempo medio (calculado sobre todas las entradas o sobre una muestra suficientemente representativa de ellas).
Buscar un dato en una estructura implica compararlo con alguno de los datos contenidos en ella. La instrucción seleccionada para medir el rendimiento de los algoritmos de búsqueda suele ser esta comparación, que recibe el nombre de comparación de claves.
Una explicación más detallada de esta materia queda fuera del objetivo de este libro. El lec- tor interesado puede consultar [1, 2].
2.1.1.
Búsqueda lineal
La búsqueda lineal supone que los datos entre los que puede estar el buscado se guardan en una lista o vector, no necesariamente ordenados.
Este algoritmo (véase la Figura 2.1) recorre la estructura desde la primera posición, com- parando (comparación de clave) cada uno de los elementos que encuentra con el buscado. Termina por una de las dos situaciones siguientes: o se llega al final de la estructura o se en- cuentra el dato buscado. En la primera situación, el dato no se ha encontrado y el algoritmo no termina con éxito.
Figura 2.1. Pseudocódigo del algoritmo de búsqueda lineal del elemento k en la tabla no ne-
cesariamente ordenada T, entre las posiciones P y U.
ind BúsquedaLineal (tabla T, ind P, ind U, clave k) Para i de P a U:
Si T [i] == k: devolver i; devolver “error”;
La situación más costosa es la que obliga a recorrer la estructura de datos completa. Esto pue- de ocurrir si la búsqueda termina sin éxito o, en caso contrario, si el elemento buscado es el último de la estructura. En estos casos (tiempo peor) el orden coincide con el tamaño de la entrada (n). La dependencia es lineal.
2.1.2.
Búsqueda binaria
La búsqueda binaria supone que los datos, entre los que puede estar el buscado, se guardan en una lista o vector ordenados.
Este algoritmo (véase la Figura 2.2) aprovecha el orden propio de la estructura para descartar, en cada iteración, la mitad de la tabla donde, con seguridad, no se encuentra el dato buscado. En la si- guiente iteración sólo queda por estudiar la otra mitad, en la que sí puede encontrarse. Para ello se com- para el elemento buscado con el que ocupa la posición central de la estructura (comparación de clave). Si éste es posterior (anterior) al buscado, puede descartarse la segunda (primera) mitad de la estructu- ra. El algoritmo termina por una de las dos razones siguientes: alguna de las comparaciones encuen- tra el elemento buscado o la última tabla analizada tiene sólo un elemento, que no es el buscado. La última circunstancia significa que el dato no ha sido encontrado y la búsqueda termina sin éxito.
Figura 2.2. Pseudocódigo del algoritmo de búsqueda binaria del elemento k en la tabla
ordenada T entre las posiciones P y U.
ind BusquedaBinaria
(tabla T, ind P, ind U, clave K)
mientras P≤U M= (P+U) /2 Si T[M] < K P=M+1; else Si T[M]>K U=M-1; else devolver M; devolver Error;
Como mucho, este algoritmo realiza las iteraciones necesarias para reducir la búsqueda a una tabla con un solo elemento. Se puede comprobar que el tamaño de la tabla pendiente en la itera- ción i-ésima es igual a n/2i. Al despejar de esta ecuación el número de iteraciones, se obtendrá
una función de log2(n), que es la expresión que determina, tanto el tiempo peor, como el tiem-
po medio del algoritmo.
2.1.3.
Búsqueda con árboles binarios ordenados
Los árboles binarios ordenados se pueden definir de la siguiente manera:
Si se llama T al árbol, clave(T) al elemento contenido en su raíz, izquierdo(T) y de- recho(T)a sus hijos izquierdo y derecho, respectivamente, y nodos(T) al conjunto de sus no- dos, T es un árbol binario ordenado si y sólo si cumple que:
∀ T’∈ nodos(T) clave(izquierdo(T’)) ≤ clave(T’) ≤ clave(derecho(T’))
El algoritmo de búsqueda (véase la Figura 2.3) consulta la raíz del árbol para decidir si la búsqueda ha terminado con éxito (en el caso en el que el elemento buscado coincida con la cla- ve del árbol) o, en caso contrario, en qué subárbol debe seguir buscando: si la clave del árbol es posterior (anterior) al elemento buscado, la búsqueda continúa por el árbol izquierdo(T) (derecho(T)). Si en cualquier momento se encuentra un subárbol vacío, la búsqueda termina sin éxito. Se suelen utilizar distintas variantes de este algoritmo para que el valor devuelto resul- te de la máxima utilidad: en ocasiones basta con el dato buscado, o con una indicación de que se ha terminado sin éxito; en otras, el retorno de la función apunta al subárbol donde está el ele- mento buscado o donde debería estar.
La Figura 2.3 resalta la comparación de claves. En el peor de los casos (que la búsqueda ter- mine con fracaso, tras haber recorrido los subárboles más profundos, o que el elemento buscado esté precisamente en el nivel más profundo del árbol), el número de comparaciones de clave coin- cidirá con la profundidad del árbol. Es decir, los tiempos peor y medio dependen de la profundi- dad del árbol. Se escribirá prof(T) para representar la profundidad del árbol T.
a) b) 1 0 2 3 4 1 4 0 2 3
Figura 2.4. Dos árboles binarios distintos para el conjunto de datos {0,1,2,3,4}:
a) con profundidad 3, b) con profundidad 2.
Figura 2.3. Pseudocódigo recursivo del algoritmo de búsqueda del elemento k en el árbol
binario ordenado T.
ArbolBin Buscar(clave K, ArbolBin T) Si vacío(T) «devolver árbol_vacío;» Si k == clave(T) «devolver T»
Si k < clave(T)
«devolver(Buscar(k,izquierdo(T));» Si k > clave(T)
«devolver(Buscar(k,derecho(T));»
Es interesante observar que este razonamiento no expresa una dependencia directa de n, sino de un parámetro del árbol binario que depende tanto de n como de la manera en la que se creó el árbol binario en el que se busca. La Figura 2.4 muestra dos posibles árboles binarios ordena- dos y correctos formados con el conjunto de datos {0,1,2,3,4}
Posteriormente se profundizará más en esta reflexión, para comprender cómo depende
prof(T)de n.
2.1.4.
Búsqueda con árboles AVL
Los árboles de Adelson-Velskii y Landis (AVL) son un subconjunto de los árboles binarios orde- nados. Un árbol binario ordenado es un árbol AVL si y sólo si las profundidades de los hijos de cualquier nodo no difieren en más de una unidad. Por tanto, aunque pueda haber muchos árboles AVL para el mismo conjunto de datos, se tiene la garantía de que la profundidad es siempre más
o menos la del árbol binario menos profundo posible.
El árbol binario menos profundo que se puede formar con n nodos es el que tiene todos sus niveles completos, es decir, cada nodo que no sea una hoja tiene exactamente 2 hijos. Es fácil comprobar que el número de nodos que hay en el nivel i-ésimo de un árbol con estas caracterís- ticas es igual a 2i, y también que el total de nodos en un árbol de este tipo, de profundidad k,
es igual a 2k+1-1. Si se despeja la profundidad k necesaria para que el número de nodos sea igual
a n, quedará en función de log2(n).
Se ha dicho que la profundidad es más o menos la del árbol binario menos profundo posible, porque se permite una diferencia en la profundidad de como mucho una unidad, que se puede despreciar para valores grandes de n. Por tanto, los árboles AVL garantizan que la profundidad es del orden de log(n), mientras que en la sección anterior se vio que el tiempo peor y medio de la búsqueda en un árbol binario T es del orden de prof(T).
2.1.5.
Resumen de rendimientos
El estudio del mejor tiempo posible para cualquier algoritmo de búsqueda que se base en la com- paración de claves se parece al razonamiento informal de las secciones anteriores respecto a los árboles binarios. Intuitivamente, se puede imaginar que el mejor tiempo posible estará asociado a la profundidad del árbol binario menos profundo que se puede formar con n nodos: log2(n).
Se puede concluir, por tanto, que éste es el rendimiento mejor posible para los algoritmos de ordenación basados en comparaciones de clave. La Tabla 2.1 muestra un resumen de los rendi- mientos observados.
Algoritmo Orden del tiempo peor Orden del tiempo medio
Búsqueda lineal n — Búsqueda binaria log(n) log(n)
Cota inferior log(n) log(n)