miércoles, 18 de abril de 2012

METODOS DE ORDENAMIENTO

Ordenación por selección
Características.
Su tiempo de ejecución es O(N2) para el mejor, peor y caso promedio.
Es el más fácil de codificar de los mostrados en este documento.
Si el array de datos es A y su tamaño es N, lo que hace el algoritmo, para cada i de
[0..N-2] es intercambiar A[i] con el mínimo elemento del subarray [A[i+1], ...,
A[N]].
Dado que es muy simple de codificar, aunque no tiene los mejores tiempos de
ejecución, es apropiado utilizarlo para arrays de datos relativamente pequeños.
void
intercambiar (Dato * A, int i, int j)
{
Dato tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
void
ordenacion_seleccion (Dato * A, int N)
{
int i, j, k;
for (i = 0; i < N - 1; i++)
{
for (k = i, j = i + 1; j < N; j++)

Algoritmos de ordenación
if (A[j] < A[k])
k = j;
if (k != i)
intercambiar (A, i, k);
}
}
Ordenación por inserción
Características.
Es un algoritmo sencillo de entender y de codificar.
Si el tamaño de la entrada es N, entonces el orden del tiempo de ejecución, para el
peor caso es O(N
2);
Si la entrada esta "casi ordenada", el algoritmo se ejecuta mucho más rápidamente.
Esta velocidad tiende a un tiempo O(N), peor caso que se cumple cuando la entrada
está totalmente ordenada.
Es por la propiedad anterior que este algoritmo, a pesar de no ser el más rápido para
entradas grandes, suele usarse de la siguiente manera: Se semi ordena la entrada con
algún otro algoritmo más rápido y más adecuado para entradas grandes. Luego,
cuando tenemos la entrada "casi ordenada" usamos este algoritmo. La velocidad de
ejecución será muy buena por dos razones: su tiempo de ejecución tiende a O(N) con
entradas "casi ordenadas" (lo cual es un tiempo excelente), y la simpleza de su
implementación hará que se ejecute más rápido que otros algoritmos más complejos.
Esto se implementará en Section 8.
Explicación del algoritmo.
Sea A el array a ordenar con N elementos. El algoritmo
consiste en recorrer todo el array A comenzando desde la posición p=2 y terminando en
p=N. Para cada p, se trata de ubicar en el lugar correcto el elemento A[p] en entre los
elementos anteriores: A[p-1], A[p-2], ..., A[0].
Dada la posición actual p, el algoritmo se basa en que los elementos A[0], A[1], ...,
A[p-1] ya están ordenados.
void
ordenacion_insercion (Dato * A, int N)
{
int p, j;

Algoritmos de ordenación
Dato tmp;
for (p = 1; p < N; p++)
{
tmp = A[p];
j = p - 1;
while ((j >= 0) && (tmp < A[j]))
{
A[j + 1] = A[j];
j--;
}
A[j + 1] = tmp;
}
}
Establezcamos la siguiente definición para poder discutir sobre el orden del tiempo de
ejecución del algoritmo y algunas de sus características.
Inversión.
Si hablamos de orden creciente, decimos que en un array A de n datos, la
pareja
(A[i],A[j]) es una inversión si se cumple que i<j y A[i]>A[j]. (en orden
decreciente sería el caso contrario).
Es decir, una
inversión es aquella pareja (A[i],A[j]) que rompe con el orden de una
secuencia de datos. Dada una secuencia A, si pudiéramos detectar todas sus inversiones
e intercambiar los valores de las mismas obtendríamos la secuencia ordenada. Por
ejemplo, considérese la secuencia
(5 3 9 7). Las inversiones de esta secuencia son
(5,3) y (9,7). Intercambiando el 5 por el 3 y el 9 por el 7 obtenemos la secuencia
ordenada
(3 5 7 9).
El algoritmo de ordenación por inserción lo que hace es recorrer todo el array
detectando inversiones (la segunda condición del
while()) y corrigiéndolas (una por
una en cada iteracción del
while()). Dado que existe esta relación entre el algoritmo
de ordenación y el número de inversiones, calculando el número medio de inversiones
en la secuencia de datos se pueden obtener cotas precisas sobre el tiempo de ejecución
del algoritmo.
Puede demostrarse fácilmente que le número medio de inversiones en un array de n
elementos es n(n-1)/4 = O(n
2).
Podemos decir entonces que el algoritmo de ordenación por inserción (y en realidad
cualquier algoritmo de ordenación que intercambie sólo elementos adyacentes),
requiere un tiempo de hasta orden n
2 en promedio.

Ordenación de Shell (ShellSort)
1
Características.
A diferencia del algoritmo de ordenación por inserción, este algoritmo intercambia
elementos distantes. Es por esto que puede deshacer más de una inversión en cada
intercambio, hecho del cual nos aprovechamos para ganar velocidad.
La velocidad del algoritmo dependerá de una secuencia de valores (llamados
incrementos, de los cuales hablaremos más abajo) con los cuales trabaja
utilizándolos como distancias entre elementos a intercambiar. Veremos que con
algunas secuencias podremos obtener ordenes de tiempo de ejecución en el peor caso
de O(n
2), O(n^(3/2)) y O(n^(4/3)).
Se considera la ordenación de Shell como el algoritmo más adecuado para ordenar
entradas de datos moderadamente grandes (decenas de millares de elementos) ya que
su velocidad, si bien no es la mejor de todos los algoritmos, es aceptable en la
práctica y su implementación (código) es relativamente sencillo.
Antes que nada definiremos el concepto de k-ordenación.
Secuencia k-ordenada.
Decimos que una secuencia A de n elementos está k-ordenada
(siendo k un natural) si, para todo i de [0,n] se cumple que los elementos A[i + hk]
están ordenados, siendo h un entero y siempre que el índice i+hk esté en [0,n].
El algoritmo de ordenación de Shell lo que hace en realidad es tomar una secuencia de
incrementos h
1, h2, ..., hp y en la etapa k-esima realizar una hk-ordenación de los datos.
La única condición que debe respetar la secuencia de incrementos es h
p=1 de modo tal
que en la última etapa se hace una 1-ordenación (o sea una ordenación normal).
¿Cómo hacemos para k-ordenar un array A de n elementos?
Dado que la
ordenación por inserción lo que hace es una 1-ordenación, usaremos el mismo
algoritmo para k-ordenar, pero comparando sólo elementos k-distanciados. Más
detalladamente, para cada i de [k+1, n] intercambiamos (si hay que hacerlo) A[i] con
alguno de los elementos anteriores a i con distancia múltiplo de k (es decir,
intercambiamos A[i] con con alguno de los elementos A[i-k], A[i-2k], ...).
Una propiedad importante de implementar una k-ordenación de este modo es que si
k>p entonces realizar una p-ordenación luego de una k-ordenación conserva el k-orden.
El lector podría estar preguntándose por qué k-ordenar varias veces si al final
terminamos haciendo una 1-ordenación utilizando ordenación por inserción. ¿Por qué
no hacer simplemente una 1-ordenación? Recordemos que la ordenación por inserción
6
Algoritmos de ordenación
trabaja mucho más rápido si la secuencia de datos está "casi ordenada". Así, luego de
efectuar varias k-ordenaciones, en la última etapa, la 1-ordenación tiene un array "casi
ordenado", por lo que el algoritmo se ejecuta más rápidamente.
Podemos decir entonces que la ordenación de Shell, es en realidad una ordenación por
inserción pero a la cual se le pasa el array "casi ordenado". Esta "casi ordenación" se
efectúa mediante h
k-ordenaciones para algunos hk.
Como el lector podrá intuir, la velocidad del algoritmo dependerá de esta secuencia de
incrementos h
k y una buena secuencia de incrementos constará de naturales primos
relativos entre sí.
Secuencias de incrementos usadas comúnmente.
La propuesta por Shell: n/2, n/4, ..., n/(2k), ..., 1. Ventajas: es fácil de calcular.
Desventajas: no optimiza mucho la velocidad del algoritmo puesto que los
incrementos tienen factores comunes (no son primos relativos). El tiempo de
ejecución promedio con esta secuencia de incrementos será O(n
2).
La propuesta por Hibbard: 2k-1, ..., 7, 3, 1; donde k se elije de modo tal que 2k-1 < n/2
y 2
k+1-1 > n/2. Aunque no se ha podido demostrar formalmente (sólo por medio de
simulaciones), utilizar esta secuencia de incrementos hace que el algoritmo de Shell
tenga un tiempo de ejecución promedio O(n
5/4). Una cota para el peor caso (que se
puede demostrar utilizando la teoría de números y combinatoria avanzada) es O(n
3/2).
La propuesta por Sedgewick: 4k-3·2k+1, ..., 19, 5, 1; con la cual se pueden obtener
tiempos de ejecución O(n
4/3) para el peor caso y O(n7/6) para el caso promedio.
A continuación damos el algoritmo formal de la ordenación de Shell, en el cual
utilizaremos los incrementos propuestos por Shell por simplicidad en el código:
void
ordenacion_shell (Dato * A, int N)
{
int incr = N / 2, p, j;
Dato tmp;
do
{
for (p = incr + 1; p < N; p++)
{
tmp = A[p];
j = p - incr;
while ((j >= 0) && (tmp < A[j]))
{
A[j + incr] = A[j];
7
Algoritmos de ordenación
j -= incr;
}
A[j + incr] = tmp;
}
incr /= 2;
}
while (incr > 0);
}
Observar que dentro del
do { ... } while(), el algoritmo es análogo a la
ordenación por inserción, salvo que se compara
tmp con los elementos anteriores
incr
-distanciados.
Ordenación por montículos (Heapsort)
Características.
Es un algoritmo que se construye utilizando las propiedades de los montículos
binarios.
El orden de ejecución para el peor caso es O(N·log(N)), siendo N el tamaño de la
entrada.
Aunque teóricamente es más rápido que los algoritmos de ordenación vistos hasta
aquí, en la práctica es más lento que el algoritmo de ordenación de Shell utilizando
la secuencia de incrementos de Sedgewick.
Breve repaso de las propiedades de los montículos
binarios (heaps)
Recordemos que un montículo Max es un árbol binario completo cuyos elementos están
ordenados del siguiente modo: para cada subárbol se cumple que la raíz es mayor que
ambos hijos. Si el montículo fuera Min, la raíz de cada subárbol tiene que cumplir con ser
menor que sus hijos.
Recordamos que, si bien un montículo se define como un árbol, para representar éste se
utiliza un array de datos, en el que se acceden a padres e hijos utilizando las siguientes
transformaciones sobre sus índices. Si el montículo está almacenado en el array A, el padre
de A[i] es A[i/2] (truncando hacia abajo), el hijo izquierdo de A[i] es A[2*i] y el hijo
derecho de A[i] es A[2*i+1].
8
Algoritmos de ordenación
Al insertar o eliminar elementos de un montículo, hay que cuidar de no destruir la
propiedad de orden del montículo. Lo que se hace generalmente es construir rutinas de
filtrado (que pueden ser ascendentes o descendentes) que tomen un elemento del montículo
(el elemento que viola la propiedad de orden) y lo muevan vérticalmente por el árbol hasta
encontrar una posición en la cual se respete el orden entre los elementos del montículo.
Tanto la inserción como la eliminación (
eliminar_min o eliminar_max según sea un
montículo Min o Max respectivamente), de un elemento en un montículo se realizan en un
tiempo O(log(N)), peor caso (y esto se debe al orden entre sus elementos y a la
característica de árbol binario completo).
Estrategia general del algoritmo.
A grandes razgos el algoritmo de ordenación por
montículos consiste en meter todos los elementos del array de datos en un montículo
MAX, y luego realizar N veces
eliminar_max(). De este modo, la secuencia de
elementos eliminados nos será entregada en ordes decreciente.
Implementación práctica del algoritmo.
Existen dos razones por las que la estrategia
general debe ser refinada: el uso de un tad auxiliar (montículo binario) lo cual podría
implicar demasiado código para un simple algoritmo de ordenación, y la necesidad de
memoria adicional para el montículo y para una posible copia del array. Estas cosas
también implican un gasto en velocidad.
Lo que se hace es reducir el código al máximo reduciéndose lo más posible la
abstracción a un montículo. Primero que nada, dado el array de datos, este no es
copiado a un montículo (insertando cada elemento). Lo que se hace es, a cada elemento
de la mitad superior del array (posiciones 0,1,...,N/2) se le aplica un filtrado
descendente (se "baja" el elemento por el árbol binario hasta que tenga dos hijos que
cumplan con el orden del montículo. Esto bastará para hacer que el array cumpla con
ser un montíclo binario.
Notamos también que al hacer un eliminar_max() se elimina el primer elemento del
array, se libera un lugar a lo último de este. Podemos usar esto para no tener que hacer
una copia del array para meter las eliminaciónes sucesivas. Lo que hacemos es meter la
salida de eliminar_max() luego del último elemento del montículo. Esto hace que luego
de N-1 eliminar_max(), el array quede ordenado de menor a mayor.
Para ahorrar código queremos lograr insertar y eliminar_max() con una sola rutina de
filtrado descendente. Ya explicamos cómo hacer que el array de datos A preserve el
orden de los elementos de un montículo. Lo que hacemos para ordenarlo usando sólo la
rutina de filtrado descendente es un eliminar_max() intercambiando el primer elemento
del array por el último del montículo y filtrar el nuevo primer elemento hasta que se
respete el orden Max. Quizá esto quede más claro mirando el código.
9
Algoritmos de ordenación
void
filtrado_desc (Dato * A, int i, int N)
{
/* queremos que se respete el orden MAX del montículo */
Dato tmp = A[i];
int hijo = 2 * i;
if ((hijo < N) && (A[hijo + 1] > A[hijo]))
hijo++;
while ((hijo <= N) && (tmp < A[hijo]))
{
/* elijo bien el hijo */
if ((hijo < N) && (A[hijo + 1] > A[hijo]))
hijo++;
A[i] = A[hijo];
i = hijo;
hijo = 2 * i;
}
A[i] = tmp;
}
void
intercambiar (Dato * A, int i, int j)
{
Dato tmp = A[i];
A[i] = A[j];
A[j] = tmp;
}
void
heapsort (Dato * A, int N)
{
int i;
/* meto los datos en el montículo (ordeno) */
for (i = N / 2; i >= 0; i--)
filtrado_desc (A, i, N);
/* saco los datos y los meto al final para obtener el array ordenado */
for (i = N - 1; i > 0; i--)
{
10
Algoritmos de ordenación
intercambiar (A, 0, i);
filtrado_desc (A, 0, i - 1);
}
}
Ordenación por Intercalación (mergesort)
Características.
Es un algoritmo recursivo con un número de comparaciones mínimo. El tiempo de
ejecución promedio es O(N log(N)).
Su desventaja es que trabaja sobre un array auxiliar lo cual tiene dos consecuencias:
uso de memoria extra y trabajo extra consumido en las copias entre arreglos (aunque
es un trabajo de tiempo lineal).
Es una aplicación clásica de la estrategia para resolución de algoritmos "divide y
vencerás". Esta estrategia plantea el hecho de que un problema puede ser dividido en
varios subproblemas y una vez resueltos estos se puede proceder a unir las
soluciones para formar la solución del problema general. La solución de los
subproblemas más pequeños se realiza de la misma manera: es decir, se van
resolviendo problemas cada vez más pequeños (hasta encontrarnos con un caso base:
problema no divisible con solución tribial).
En cada recursión se toma un array de elementos desordenados. Se lo divide en dos
mitades, se aplica la recursión en cada una de estas y luego (dado que al finalizar estas
recursiones tenemos las dos mitades ordenadas) se
intercalan ambas para obtener el
array ordenado.
Intercalar.
Es la operación que le da el nombre a este algoritmo. La intercalación toma
dos secuencias (arrays) de elementos y a partir de estas construye una tercera secuencia
que contiene todos los elementos de estas en orden.
Implementación de la intercalación.
Sean dos arrays ordenados A y B (cuyas
longitudes pueden ser distintas). Sea el array C tal que |C|>=|A|+|B| (tiene capacidad
para almacenar todos los elementos de ambas listas A y B). Sean los contadores ap, bp
y cp con valor inicial 0 (se ponen al inicio de sus arreglos respectivos). Lo que hace
intercalar
es copiar el menor de A[ap] y B[bp] en C[cp] y avanzar los contadores
apropiados. Cuando se agota cualquier lista de entrada (A o B), los datos que quedan en
la otra lista se copian en C.
11
Algoritmos de ordenación
Esperamos que al leer el código, el lector entienda los detalles menores tanto de la
rutina recursiva del algoritmo de recursión como de la rutina
intercala().
void ord_intercalacion (Dato * A, Dato * tmp, int izq, int der);
/* Función recursiva! A es el array de datos. tmp debe ser un
array de tamaño mayor o igual a A. izq y der son los extremos
del subarreglo sobre el cual trabajaremos en esta
recursión. */
void intercalar (Dato * A, Dato * tmp, int izq, int centro, int der);
/* lo que hace esta rutina es intercalar las particiones
[A[izq], ..., A[centro-1] ] y [ A[centro], ..., A[der] ]
(que deberían estar ya ordenadas) en el subarray
[ tmp[izq], ..., tmp[der] ] y luego copiar los elementos
nuevamente a A, en el subarray [ A[izq], ..., A[der] ] */
void
mergesort (Dato * A, int N)
{
Dato *tmp = crear (N); /* creamos un array auxiliar del mismo
tamaño que A */
ord_intercalacion (A, tmp, 0, N - 1);
}
void
ord_intercalacion (Dato * A, Dato * tmp, int izq, int der)
{
if (izq < der)
/* este if comprueba el caso base que es cuando la partición
pasada no tiene elementos. */
{
/* dividimos a la mitad el subarray [A[izq],...,A[der]] */
int centro = (izq + der) / 2;
/* aplicamos la recursión en ambas mitades */
ord_intercalacion (A, tmp, izq, centro);
ord_intercalacion (A, tmp, centro + 1, der);
/* a este punto ambas mitades deberían estar ordenadas por
lo que las intercalamos para unirlas en una sola
secuencia ordenada. */
12
Algoritmos de ordenación
intercalar (A, tmp, izq, centro + 1, der);
}
}
void
intercalar (Dato * A, Dato * tmp, int izq, int centro, int der)
{
/* mis particiones serán [izq,...,centro-1] y
[centro,...,der] */
/* contadores para la primera mitad, la segunda y para la
intercalacion respectivamente. */
int ap = izq, bp = centro, cp = izq;
while ((ap < centro) && (bp <= der))
{
if (A[ap] <= A[bp])
{
tmp[cp] = A[ap];
ap++;
}
else
{
tmp[cp] = A[bp];
bp++;
}
cp++;
}
/* terminamos de intercalar, ahora metemos los elementos
restantes de la lista que aún no ha terminado de ser
procesada. */
while (ap < centro)
{
tmp[cp] = A[ap];
cp++;
ap++;
}
while (bp <= der)
{
tmp[cp] = A[bp];
cp++;
bp++;
13
Algoritmos de ordenación
}
/* ahora que tenemos la intercalación finalizada en tmp, la
pasamos a A */
for (ap = izq; ap <= der; ap++)
A[ap] = tmp[ap];
}
Observar como la función principal
mergesort() solamente es un manejador de la
función
ord_intercalacion() en la cual se realiza todo el trabajo recursivamente.
Si bien el algoritmo puede parecer largo, es más fácil (desde nuestro punto de vista)
entenderlo que otros algoritmos más cortos pero más complejos como por ejemplo la
ordenación de shell. La única parte difícil es entender cómo funciona la recursión ya
que el algoritmo intercalar es bastante fácil.
Ordenación rápida (quicksort)
Características.
Es el algoritmo de ordenación más rápido (en la práctica) conocido. Su tiempo de
ejecución promedio es O(N log(N)).
Para el peor caso tiene un tiempo O(N2), pero si se codifica correctamente las
situaciones en las que sucede el peor caso pueden hacerse altamente improbables.
EN la práctica, el hecho de que sea más rápido que los demás algoritmos de
ordenación con el mismo tiempo promedio O(N log(N)) está dado por un ciclo
interno muy ajustado (pocas operaciones).
Al igual que el algoritmo de ordenación por intercalación, el algoritmo de
ordenación rápida es fruto de la técnica de resolución de algoritmos "divide y
vencerás". Como ya se explicó, la técnica de divide y vencerás se basa en, en cada
recursión, dividir el problema en subproblemas más pequeños, resolverlos cada uno
por separado (aplicando la misma técnica) y unir las soluciones.
Se explica a continuación, a grandes razgos, qué se hace en cada recursión:
1. Si el array de datos es vacío o tiene un sólo elemento no hacemos nada y salimos
de la recursión.
14
Algoritmos de ordenación
2. Elegimos un elemento
v (llamado pivote) del array de datos.
3. Particionamos el array de datos A en tres arrays:
A1={todos los elementos de A - {v} que sean menores o iguales que v}
A2={v}
Utilizando múltiples algoritmos

En Section 3 prometimos usar el algoritmo de ordenación por inserción para terminar
de ordenar un array de datos "casi ordenado". En esta sección examinaremos este caso
realizando lo siguiente: Modificamos ligeramente el algoritmo de ordenación rápida
para que ordene particiones de hasta CORTE elementos, donde CORTE es un entero
previamente determinado en función del tamaño de la entrada. Mostramos la
implementación esperando que se entienda el cometido de las ligeras modificaciones:
#include <math.h>
static int CORTE;
void ord_rapida (Dato * A, int izq, int der);
void
quicksort (Dato * A, int N)
{
CORTE=log(N)*log(N);
ord_rapida (A, 0, N - 1);
if(CORTE>1)
ordenacion_insercion(A, N);
}
void
ord_rapida (Dato * A, int izq, int der)
/* se trabaja en el subarray [A[izq],...,A[der]] */
{
if (der - izq > CORTE) /* caso base de la recursión */
{
{ /* elegimos el pivote y lo ponemos en A[der-1] */
int centro = div2 (izq + der);
if (A[izq] > A[centro])
intercambiar (A, izq, centro);
if (A[izq] > A[der])
intercambiar (A, izq, der);
if (A[centro] > A[der])
intercambiar (A, centro, der);
intercambiar (A, centro, der - 1);
}
{ /* "particionamos" */
int i = izq, j = der - 1;
Dato pivote = A[der - 1];
do
{
do
i++;
while (A[i] < pivote);
do
j--;
while (A[j] > pivote);
intercambiar (A, i, j);
}
while (j > i);
/* deshacemos el último intercambio el cual se efectuó
sin cumplirse i < j */
intercambiar (A, i, j);
/* ponemos el pivote en el medio de ambas particiones */
intercambiar (A, i, der - 1);
/* aplicamos la recursión en las particiones halladas */
ord_rapida (A, izq, i - 1);
ord_rapida (A, i + 1, der);
}
}
}
Como puede verse sólo se ha modificado una línea de la función
ord_rapida() pero
se ha modificado sustancialmente la función
quicksort(). En esta se determina el
valor correcto de la variable CORTE y se aplica, luego de ord_rapida() la función de
ordenación por inserción. Esta última se ejecutará muy rápido cuanto "mejor ordenado"
halla quedado el array de datos.
Hemos propuesto CORTE=log(N)
2 basados sólo en observaciones experimentales. Sin
embargo, hemos observado un incremento de velocidad bastante bueno con este corte.
A3={todos los elementos de A - {v} que sean mayores o iguales que v}
4. Aplicamos la recursión sobre A
1 y A3
2
5. Realizamos el último paso de "divide y vencerás" que es unir todas las soluciones
para que formen el array A ordenado. Dado que a este punto A
1 y A3 están ya
ordenados, concatenamos A
1, A2 y A3.
Si bien este algoritmo puede parecer sencillo, hay que implementar 2 y 3 de modo de
favorecer al máximo la velocidad del algoritmo. Es por esto que discutiremos a parte
cómo realizar estas tareas.



METODOS DE BUSQUEDA

Técnica Secuencial Ordenada/Desordenada.
Descripción de la técnica

La búsqueda es el proceso de localizar un registro (elemento) con un valor de llave particular. La búsqueda termina exitosamente cuando se localiza el registro que contenga la llave buscada, o termina sin éxito, cuando se determina que no aparece ningún registro con esa llave.

Normalmente un archivo secuencial se almacena en bloques, en un orden secuencial simple de los registros. La organización física del archivo en una cinta o disco se corresponde exactamente con la ubicación lógica del archivo. En este caso, el procedimiento para ubicar los nuevos registros en un archivo de pila separado, llamado archivo de registro (log file) o archivo de transacciones. Periódicamente, se realiza una actualización por lotes que mezcla el archivo de registro con el archivo maestro para producir un nuevo archivo en secuencia correcta de claves.
Búsqueda secuencial, también se le conoce como búsqueda lineal. Supongamos una colección de registros organizados como una lista lineal. El algoritmo básico de búsqueda secuencial consiste en empezar al inicio de la lista e ir a través de cada registro hasta encontrar la llave indicada (k), o hasta al final de la lista.

 La situación óptima es que el registro buscado sea el primero en ser examinado. El peor caso es cuando las llaves de todos los n registros son comparados con k (lo que se busca). El caso promedio es n/2 comparaciones.

Este método de búsqueda es muy lento, pero si los datos no están en orden es el único método que puede emplearse para hacer las búsquedas. Si los valores de la llave no son únicos, para encontrar todos los registros con una llave particular, se requiere buscar en toda la lista.

Ventajas de la técnica.
Es el algoritmo más simple de búsqueda y no requiere ningún proceso previo de la tabla, ni ningún conocimiento sobre la distribución de las llaves. La búsqueda secuencial es el área del problema donde previamente existían mejores algoritmos.
Es el mejor metodo de busqueda para registros desordenados y revisa nodo por nodo sin brincar ninguno ( es muy seguro)

Desventajas de la técnica
Este método de búsqueda es muy lento, pero si los datos no están en orden es el único método que puede emplearse para hacer las búsquedas. Si los valores de la llave no son únicos, para encontrar todos los registros con una llave particular, se requiere buscar en toda la lista.
Si los registros a los que se accede con frecuencia no estan al principio del archivo, la cantidad promedio de comparaciones aumenta notablemente dado que se requiere mas tiempo para recuperar dichos regisros.

Para las aplicaciones interactivas que incluyen peticione s o actualizaciones de registros individuales, los archivos secuenciales ofrecen un rendimiento pobre.

Definitivamente, la busqueda secuencial es el metodo menos eficiente; porque se basa en comparar el valor que se desea buscar con cada uno de los valores del archivo.


Principales Aplicaciones

Los archivos secuenciales son típicamente utilizados en aplicaciones de proceso de lotes Y son óptimos para dichas aplicaciones si se procesan todos los registros. La organización secuencias de archivos es la única que es fácil de usar tanto en disco como en cinta.
Un ejemplo claro para utilizar esta tecnica de busqueda es cuando se tiene una base de datos no muy grande en un negocio pequeño donde los registros mas usados son llamados con frecuencia , es aquí donde esta tecnica es fuerte, ya que se aplica a un patron de busqueda pequeño, sencillo y manejable; es decir como si fuera una descripcion, es uno tras otro.
Ejemplo: numero de cliente nombre apellido direccion curp

Codificación.



void sequential_search(int x[100], int search_num)

{

int index = 0;
while((index < 100) && (search_num != x[index]))

{ // Loop while the number is not found and while more elements remain.
if(x[index] != search_num)

{ // If current element is not the one for which we are
index++; // searching, increment subscript index.

}
}
return(index);
}


Técnica de Búsqueda Secuencial Indexada

Descripción de la técnica



Un método popular para superar las desventajas de los archivos secuenciales es el del archivo secuencias indexado; pero implica un aumento en la cantidad de espacio requerida.
Funciona de la siguiente manera:

Se reserva una taba auxiliar llamada indice ademas del archivo ordenado mismo. Cada elemento en el indice consta de una llave kindex y un apuntador al registro en el archivo que corresponde a kindex. Los elementos en el indice al igual que los elementos en el archivo, deben estar ordenados en la llave. Si el indice es de un octavo del tamaño del archivo, se representa en el indice cada octavo registra el archivo.

'Algoritmos de búsqueda'

Si el indice comienza a crecer tanto que se vuelve ineficaz se puede usar un indice secundario que funciona casi de la misma forma que el indice principal, solo que apunta a este, no a la tabla principal la busqueda empieza con una exploracion por el indice secundario; esto nos lleva a un subarreglo en el indice principal; despues el procesamiento continua normalmente. Un ejemplo de lo anterior es la siguiente figura.
'Algoritmos de búsqueda'
Ventajas de la técnica
Permite procesar el archivo secuencialmente por orden lógico y también procesarlo al azar.
La ventaja real del método secuencial indexado es que los elementos en la tabla pueden ser examinados en forma secuencial si todos los registros en el archivo deben ser accesados, pero sin embargo, el tiempo de búsqueda para algún elemento en particular se reduce considerablemente. La búsqueda secuencial se realiza en la tabla de índices que es más pequeña en lugar de la tabla más grande. Una vez que se ha encontrado un índice correcto, se hace una segunda búsqueda secuencial únicamente en la parte reducida de la tabla que contiene los registros.
La organización secuencial indexado es conveniente para archivos con mediana volatilidad, actividad variable y tamaño relativamente estable.
Las eliminaciones de una tabla secuencial indexada se pueden hacer fácilmente mediante la asignación de banderas a las entradas que son eliminadas. Durante la búsqueda secuencial a través de la tabla, se ignoran las entradas que han sido eliminadas.

Desventajas de la técnica

Pero implica un aumento en la cantidad de espacio requerida, porque se ocupa un indice y “se pone a un lado además del fichero clasificado a sí mismo”.
La inserción en una tabla secuencial indexada es un poco más difícil debido a que puede qe no exista espacio entre dos entradas en la tabla, siendo necesario mover un gran número de elementos en la tabla.
El uso de una lista ligada (indice) da una gran sobrecarga de espacio y tiempo para los apuntadores que se utilizan en la busqueda de registros.
Los registros deben ser de longitud fija. El archivo debe estar soportado por una memoria de masa tal como el disco; no se utiliza en cinta magnética. A veces todo el archivo debe estar presente en línea.

Principales Aplicaciones

Un uso en la cual esta busqueda se aplica, es donde se presenta el ingreso de datos (registros) sin ningun tipo de orden especifico; pero en cada determinado momento su campo llave es almacenado en un indice, en el cual esas llaves estan ordenadas de menor a mayor o de mayor a menor dependiendo el uso que se le de. De esta manera, para agilizar la busqueda de un registro en particular se accesa a ese registro por medio de su campo llave alacenado en el indice.

Un ejemplo de nuestra vida diaria y donde se aplica esta busqueda es en un negocio mediano (negocio de carnes frias , refaccionaria), ya que aquí se necesita una busqueda eficiente con una sola clave de acceso y otorgandonos la informacion requerida.

Codificación

Algoritmo de Búsqueda Secuencial Indexada:

PROCEDURE B_S_INDEXADA(llave:integer; var pocis:integer);

var

i,n:integer;

f:boolean;

begin

f:=false;

i:=1;

while (i<llave) then

f:= true;

else

inc(i);

if i=1 then

linf:=1

else

linf:=pindex[i-1];

if f then

lsup:=pindex[i]-1

else

lsup:=n;

j:=linf; f:=false;



while(j<=lsup) ands (not f) do

if k[j]="llave" then f:="true"

else

inc(j);

if f then posic:="j"

else

posic:="0;"

end;

KINDEX. Arreglo de llaves en la tabla índice. PINDEX. Arreglo de punteros a los registros actuales del archivo. N. Tamaño del archivo. TAMINDEX. Tamaño de la tabla índice. Primero busca el rango de llaves en el que puede encontrar posiblemente la llave indicada, después busca secuencialmente en cada una de ellas hasta encontrarla o no, en caso de que no exista.

Técnica de Búsqueda Binaria.
Descripción de la técnica

Si los datos que se buscan están clasificados en un determinado orden, el método citado anteriormente se denomina búsqueda binaria.
La búsqueda binaria utiliza un método de `divide y vencerás'para localizar el valor deseado. Con este método se examina primero el elemento central de la lista; si éste es el elemento buscado, entonces la búsqueda ha terminado.

En caso contrario, se determinar si el elemento buscado será en la primera o la segunda mitad de la lista y a continuación se repite este porceso, utilizando el elemento central de esa sublista.
Se puede aplicar tanto a datos en listas lineales como en árboles binarios de búsqueda. Los prerrequisitos principales para la búsqueda binaria son:
  • La lista debe estar ordenada en un orden específico de acuerdo al valor de la llave.
  • Debe conocerse el número de registros.

Algoritmo:

  • Se compara la llave buscada con la llave localizada al centro del arreglo.
  • Si la llave analizada corresponde a la buscada fin de búsqueda si no.
  • Si la llave buscada es menor que la analizada repetir proceso en mitad superior, sino en la mitad inferior.
  • El proceso de partir por la mitad el arreglo se repite hasta encontrar el registro o hasta que el tamaño de la lista restante sea cero , lo cual implica que el valor de la llave buscada no esta en la lista.
  • El esfuerzo máximo para este algoritmo es de log2n. El mínimo de 1 y en promedio ½ log2 n.

 Ventajas de la técnica.
La búsqueda binaria es un método eficiente siempre que el vector esté ordenado. En la práctica, esto suele suceder, pero no siempre. Por esta razón la búsqueda binaria exige una ordenación previa del archivo.
La búsqueda binaria proporciona un medio para reducir el tiempo requerido para buscar en una lista. Este método, sin embargo, exige que los datos estén ordenados.
Es mas rapido por su recursividad, su mayor ventaja es con los archivos extensos.
El codigo del procedimiento de esta busqueda es corto en comparacion con las demas tecnicas de busqueda.
En esencia, con una sola comparacion eliminamos la mitad de la tabla; este es el metodo mas eficiente de buscar en una lista ordenada sin emplear tablas o indices adicionales.
Desventajas de la técnica.
La búsqueda binaria tiene, sin embargo, inconvenientes a resaltar:

El archivo debe estar ordenado y el almacenamietno de un archivo ordenado suele plantear problemas en las inserciones y eliminaciones de elementos.
No revisa todos los elementos del archivo, requiere que todos los elementos esten ordenados
Esta busqueda mas de uno o dos accesos si el archivo es enorme;y mantener ese archivo ordenado es muy costoso.

Principales Aplicaciones.

Ejemplo: Árbol Binario de Búsqueda:

Se distinguen 2 casos triviales con solución directa: árbol vacío (elemento no encontrado) o raíz del árbol.

Solución:

Cuando el elemento no se encuentra en la raíz, dividimos el árbol en 2 subarboles (izquierda y derecha) y aplicamos ABB, sobre aquel que nos interese (al estar ordenado solo debemos ir por una de las ramas).

La combinación de resultados es trivial: la solución del subproblema es también la del problema global.

Supongamos la lista:

1231

1473

1545

1834

1892

1898 elemento central

1983

2005

2446

2685

3200
Si está buscando el elemento 1983, se examina el número central 1898 en la sexta posición. Ya que 1983 es mayor que 1898, se desprecia la primera sublista y nos centramos en la segunda
1983

2005

2446 elemento central

2685

3200
El número central de esta sublista es 2446 y el elemento buscado es 1983 menos que 2446; eliminamos la segunda sublista y nos queda
1983

2005
Como no hay término centralm elegimos el término inmediatamente anterior al término central, 1983, que es el buscado.

Se han necesitado tres comparaciones, mientras que la búsqueda secuencial hubiese necesitado siete.


Codificación.


void binary_search(int x[100], int lowerbound, int upperbound, int search_num)

{

int search_pos;

int compare_count = 1; // Variable used to count the comparisons.



// Calculate initial search position.

search_pos = (lowerbound + upperbound) / 2;

while((x[search_pos] != search_num) && (lowerbound <= upperbound))

{

compare_count++;

if(x[search_pos] > search_num) // If the value in the search

{ // position is greater than the number

upperbound = search_pos - 1; // for which we are searching, change

} // upperbound to the search position

else // minus one.

{ // Else, change lowerbound to search

lowerbound = search_pos + 1; // position plus one.

}
search_pos = (lowerbound + upperbound) / 2;

}
if(lowerbound <= upperbound)

{
cout << "A binary search found the number in "

<< compare_count << " comparisons.\n";

}
else

{
cout << "Number not found by binary search after "

<< compare_count << " comparisons.\n";

}

}
Técnica de Búsqueda por Interpolación.
Descripción de la técnica.

Este método se puede aplicar solamente a tablas o archivos ordenados. Como su nombre lo indica se trata de llegar al elemento buscado por medio de la interpolación lineal. El procedimiento es recursivo; como en el caso de la búsqueda binaria, en cada paso se van modificando los límites, disminuyendo el intervalo, hasta llegar al elemento buscado.

Si se determina que la clave buscada XX se encuentra dentro del intervalo INTABLA de la tabla, y que la variación en ese intervalo de la clave es INCLAVE, la siguiente posición a probar es:

PX = PI + ENTERO ((XX-XI) * (INTABLA / INCLAVE))

El algoritmo es similar al de búsqueda binaria, la diferencia está en que en vez de dividir el área en mitades, se delimita por medio de los valores resultantes de la interpolación.

En búsqueda binaria el espacio se corta siempre adentro a medias, las garantías de lo que desea el funcionamiento logarítmico. Sin embargo, durante la búsqueda encontramos un valor que esté muy cerca del número z de la búsqueda, parece más razonable continuar la búsqueda en esa area envez de ocultor e ir a la media punta siguiente.

En detalle, si z es muy pequeño, debemos comenzar la búsqueda en alguna parte en el principio de la secuencia en vez de la punta intermedia Considere la manera que abrimos un libro cuando estamos buscando para cierta página. Diga que la página es 200 y el libro parece de 800 paginas. La paginación 200 es así alrededor de la marca de un cuarto, y nosotros utilizamos este conocimiento como indicación de donde abrir el libro. No golpearemos probablemente la paginación 200 en el primer intento; suponga que conseguimos la paginación 250 en lugar de otro. Ahora cortamos la búsqueda a un rango de 250 paginas, y la paginación deseada está en alrededor la marca de 80 por ciento entre la paginación 1 y 250. Ahora intentamos ir detrás de un quinto de la manera corta. Podemos continuar este proceso hasta que conseguimos bastante cercanos a la paginación 200, de que que podemos mover de un tirón una pagina al mismo tiempo. Ésta es exactamente la idea detrás de la búsqueda de la interpolación. En vez de cortar el espacio de la búsqueda por una mitad fija, la cortamos por una cantidad que se parezca la más probable tener éxito.

El funcionamiento de la búsqueda de la interpolación depende no solamente de la talla de la secuencia, pero también de la entrada de información misma. Hay entradas de información para los chequeos de la búsqueda de interpolación del deseado en cada número en la secuencia. Sin embargo, la búsqueda de la interpolación es muy eficiente para las entradas de información que consisten en elementos relativamente uniformemente distribuidos (las paginaciones de un libro, por supuesto, se distribuyen uniformemente). Puede ser mostrado que el número medio de comparaciones se realizó por la búsqueda de interpolación, donde el promedio asume el control todas las secuencias posibles, es 0 (logn del registro). Aunque éste se parece ser un orden de la mejora magnitud concluída de el funcionamiento de la búsqueda binaria ( debido al logaritmo adicional).

Ventajas de la técnica

La búsqueda de interpolación, es una búsqueda mucho mejor que la binaria en la práctica porque, a menos que no sea muy grande, el valor de log2n es bastante pequeño que el logaritmo de él no es mucho más pequeño.

Incluso a pesar de qe el calclo es de algun modo mas complejo, una busqueda con interpolacion puede proporcionar una mejoria importante a nuestra busqueda binaria en grandes conjuntos de datos con claves distribuidas de modo uniforme.

Desventajas de la técnica

La búsqueda de la interpolación requiere una aritmética más elaborada, a parte que los calculos que se necesitan para esta busqueda son muy lentos.

Para lograr esta busqueda se requieren llaves, multiplicaciones y divisiones complejas, es decir, calculos de nivel alto.

Principales Aplicaciones

En aplicaciones matematicas donde se busquen approximaciones de alguna ecuacion, se utiliza este metodo pero sin su recursividad solo hace su primera para conseguir las approx.
Tambien tiene las mismas aplicaciones que la busqueda binaria ya que son casi iguales.

 Codificación.

#define MEDIO (x) ( ((x) + 1)/2 )

#define NO_ ENCONTRADO -1
int bus_bin (int datos [], int tamaño, int clave)
{

int delta,mitad;

delta =tamaño / 2;

while (clave := datos[mitad] ) {

if (delta ==0)

return (NO_ENCONTRADO);

else if(clave>datos[mitad]);

mitad + = medio (delta);

else

mitad -= MEDIO (delta);

delta=delta/2;

}

return (mitad);

}