import json
import time
import numpy as np
import math
import pandas as pd
import matplotlib.pyplot as plt
from bisect import bisect_left
from sortedcontainers import SortedList
import random
4A. Reporte escrito. Experimentos y análisis de algoritmos de búsqueda por comparación.
Docente: Dr. Eric Sadit Téllez Avila
Alumno: Arif Narvaez de la O
1. Introducción
Este documento describe la implementación y evaluación de diversos algoritmos de búsqueda aplicados a un conjunto de datos ordenado. Se han considerado cinco métodos de búsqueda distintos:
Búsqueda binaria acotada
Búsqueda secuencial (B0)
Búsqueda no acotada B1
Búsqueda no acotada B2
Búsqueda utilizando la estructura SkipList
El objetivo es medir la eficiencia de cada algoritmo en términos de número de comparaciones y tiempo de ejecución utilizando varios archivos de consulta. Los resultados permiten analizar cuál de estas estrategias es más óptima en el contexto de datos ordenados.
De acuerdo con Cormen et al. (2009), la búsqueda binaria es óptima en listas ordenadas, mientras que Knuth (1998) destaca que la búsqueda secuencial es ineficiente en grandes volúmenes de datos. La estructura SkipList, introducida por Pugh (1990), se considera una alternativa eficiente que permite búsquedas rápidas mediante listas enlazadas con múltiples niveles.
2. Librerias a utilizar
3.Carga de datos
Esta Secc¿cion de codigo abre y carga los datos desde un archivo JSON, extrayendo las claves y ordenándolas para facilitar las búsquedas.
= "./archivos/p-032_ordenado.json"
ruta_datos = [
ruta_consultas f"./archivos/consultas-{i}-listas-posteo.json"
for i in range(1, 5)
]
with open(ruta_datos, 'r') as f:
= json.load(f)
datos
= sorted(datos.keys()) datos_ordenados
Variables y su función * ruta_datos: Almacena la ruta del archivo JSON que contiene la lista ordenada de claves principales.
ruta_consultas: Lista de rutas a los archivos JSON que contienen las consultas a realizar.
datos: Diccionario cargado desde el archivo JSON, con las claves como índices.
datos_ordenados: Lista de claves extraídas y ordenadas para su uso en los algoritmos de búsqueda.
4. Implementacion de los algoritmos de busqueda
BUSQUEDA BINARIA
Este algoritmo tiene una complejidad de O(log n) y es eficiente para listas ordenadas (Cormen et al., 2009).
Función: busqueda_binaria(lista, clave)
Variables clave:
izquierda y derecha: Definen los límites del área de búsqueda.
medio: Calcula el punto intermedio para dividir la lista.
comparaciones: Cuenta el número de comparaciones realizadas.
def busqueda_binaria(A, x):
"""Búsqueda binaria clásica para encontrar posición de inserción."""
= 0
comparaciones = 0, len(A) - 1
sp, ep
while sp < ep:
+= 1
comparaciones = (sp + ep) // 2
mid if x <= A[mid]:
= mid
ep else:
= mid + 1
sp
+= 1
comparaciones return (sp if x <= A[sp] else sp + 1), comparaciones
BUSQUEDA SECUENCIAL
Este método tiene una complejidad de O(n), lo que lo hace ineficiente en grandes volúmenes de datos (Knuth, 1998).
Ademas, revisa cada elemento de la lista, de izquierda a derecha, hasta encontrar el valor buscado o llegar al final.
Función: busqueda_secuencial(lista, clave)
Variables clave:
- comparaciones: Cuenta cuántas veces se compara la clave con elementos de la list
def busqueda_secuencial(A, x):
"""Búsqueda secuencial para encontrar posición de inserción."""
= 0
comparaciones for i, elemento in enumerate(A):
+= 1
comparaciones if x <= elemento:
return i, comparaciones
return len(A), comparaciones
BUSQUEDA NO ACOTADA B1
Este método selecciona posiciones aleatorias dentro de la lista y compara el valor en dichas posiciones con la clave buscada. Si después de un número fijo de intentos no encuentra el elemento, la búsqueda se considera fallida. Es menos eficiente en listas ordenadas, ya que no aprovecha la estructura de los datos. Su eficiencia depende de la distribución de los datos y la suerte en los intentos aleatorios.
- Función: busqueda_no_acotada_b1(lista, clave)
Características:
Se eligen 10 posiciones aleatorias para buscar la clave.
def busqueda_no_acotada_B1(A, x):
"""
Algoritmo B1: Búsqueda no acotada con doblado simple (doubling search/galloping)
Implementación según Bentley y Yao (1976)
"""
= 0
comparaciones = len(A)
n
if n == 0:
return 0, 0
# Fase 1: Determinación del rango con doblado
= 1
i = 0
p while p + i < n and A[p + i] < x:
+= 1
comparaciones += i
p *= 2
i
# Fase 2: Búsqueda binaria en el rango encontrado
= p
sp = min(p + i, n - 1)
ep
while sp < ep:
+= 1
comparaciones = (sp + ep) // 2
mid if x <= A[mid]:
= mid
ep else:
= mid + 1
sp
+= 1
comparaciones return (sp if x <= A[sp] else sp + 1), comparaciones
BUSQUEDA NO ACOTADA B2
Esta variante combina la búsqueda secuencial con saltos aleatorios en la lista, lo que introduce cierta aleatoriedad en la exploración. Si bien intenta mejorar la búsqueda secuencial, no logra un rendimiento estable debido a la naturaleza aleatoria de los saltos.
- Función: busqueda_no_acotada_b2(lista, clave)
Características:
Mezcla búsqueda secuencial con saltos aleatorios.
def busqueda_no_acotada_B2(A, x):
"""
Algoritmo B2: Búsqueda no acotada con doblado doble (doubling-doubling search)
Implementación según Bentley y Yao (1976)
"""
= 0
comparaciones = len(A)
n
if n == 0:
return 0, 0
# Fase 1: Determinación del rango con doblado doble
= 1
i = 1
j = 0
p while p + i < n and A[p + i] < x:
+= 1
comparaciones += i
p = j * j
i += 1
j
# Fase 2: Aplicar B1 en el rango encontrado
= p
sp = min(p + i, n - 1)
ep
# Implementación de B1 dentro del rango
= 1
k = sp
q while q + k < ep and A[q + k] < x:
+= 1
comparaciones += k
q *= 2
k
# Fase 3: Búsqueda binaria en el subrango final
= q
left = min(q + k, ep)
right
while left < right:
+= 1
comparaciones = (left + right) // 2
mid if x <= A[mid]:
= mid
right else:
= mid + 1
left
+= 1
comparaciones return (left if x <= A[left] else left + 1), comparaciones
SKIP LIST
SkipList es una estructura de datos que permite búsquedas eficientes mediante listas enlazadas con múltiples niveles de acceso. Su rendimiento es cercano al de la búsqueda binaria, con una complejidad promedio de O(logn) (Pugh, 1990).
Función: SkipList.buscar(clave)
Estructura: Utiliza SortedList para optimizar la búsqueda.
class SkipList:
"""Implementación mejorada de SkipList para búsqueda eficiente."""
def __init__(self, p=0.5):
self.p = p
self.levels = []
self.levels.append([]) # Nivel 0 contiene todos los elementos
def insertar(self, valor):
# Insertar en todos los niveles necesarios
self.levels[0].append(valor)
self.levels[0].sort() # Mantener ordenado
# Decidir si promocionar el elemento a niveles superiores
= 0
current_level while random.random() < self.p:
+= 1
current_level if current_level >= len(self.levels):
self.levels.append([])
# Insertar en el nivel superior
self.levels[current_level].append(valor)
self.levels[current_level].sort()
def buscar(self, clave):
= 0
comparaciones # Comenzar desde el nivel más alto
= len(self.levels) - 1
level = -1
pos
while level >= 0:
= self.levels[level]
current_list = bisect_left(current_list, clave)
idx
if idx < len(current_list) and current_list[idx] == clave:
+= 1
comparaciones return idx, comparaciones
+= 1
comparaciones if idx > 0 and level > 0:
# Buscar en el rango correspondiente en el nivel inferior
= current_list[idx-1]
lower_val = bisect_left(self.levels[level-1], lower_val)
lower_idx = lower_idx
pos elif level == 0:
= idx
pos
-= 1
level
return pos, comparaciones
# Preparar estructuras de datos
= SkipList()
skiplist for elemento in datos_ordenados:
skiplist.insertar(elemento)
5. Resultados
Esta función, mostrar_resultados(), tiene como objetivo convertir un diccionario de resultados (resultados) en un diccionario de DataFrames de pandas, donde cada DataFrame corresponde a una consulta específica.
df_consultas = {}:
Se inicializa un diccionario vacío que almacenará los DataFrames generados.
Las claves serán strings como “df_consulta1”, “df_consulta2”, etc.
Los valores serán DataFrames de pandas.
# Función para ejecutar pruebas
def ejecutar_pruebas(consulta_file):
with open(consulta_file, 'r') as f:
= json.load(f)
consultas
= {
metodos "binaria": busqueda_binaria,
"secuencial": busqueda_secuencial,
"B1_doubling": busqueda_no_acotada_B1,
"B2_doubling-doubling": busqueda_no_acotada_B2,
"skiplist": skiplist.buscar
}
= {}
resultados
for nombre, metodo in metodos.items():
= 0
total_comparaciones = time.time()
tiempo_inicio
for consulta in consultas:
= str(consulta)
clave if nombre == "skiplist":
= metodo(clave)
_, comparaciones else:
= metodo(datos_ordenados, clave)
_, comparaciones += comparaciones
total_comparaciones
= time.time() - tiempo_inicio
tiempo_total = {
resultados[nombre] "comparaciones": total_comparaciones,
"tiempo": tiempo_total,
"comparaciones_promedio": total_comparaciones / len(consultas)
}
return resultados
# Ejecutar pruebas para cada archivo de consultas
= {}
resultados_totales for consulta_file in ruta_consultas:
= ejecutar_pruebas(consulta_file)
resultados_totales[consulta_file]
# Mostrar resultados
def mostrar_resultados():
= []
dfs for i, (consulta_file, datos) in enumerate(resultados_totales.items(), 1):
= pd.DataFrame(datos).T
df 'Archivo'] = f'Consulta {i}'
df[
dfs.append(df)
# Concatenar todos los DataFrames
= pd.concat(dfs)
df_final
# Reorganizar los datos para mejor visualización
# Primero resetear el índice para tener los nombres de métodos como columna
= df_final.reset_index().rename(columns={'index': 'Metodo'})
df_final
# Usar melt para tener una estructura más adecuada
= pd.melt(df_final, id_vars=['Archivo', 'Metodo'],
df_melted =['comparaciones', 'tiempo', 'comparaciones_promedio'],
value_vars='Metrica', value_name='Valor')
var_name
# Pivotar para tener métodos como columnas
= df_melted.pivot_table(index=['Archivo', 'Metrica'],
df_pivoted ='Metodo', values='Valor')
columns
return df_pivoted.reset_index()
= mostrar_resultados()
df_consultas df_consultas
Metodo | Archivo | Metrica | B1_doubling | B2_doubling-doubling | binaria | secuencial | skiplist |
---|---|---|---|---|---|---|---|
0 | Consulta 1 | comparaciones | 50000.000000 | 60000.000000 | 70000.000000 | 70000.000000 | 120000.000000 |
1 | Consulta 1 | comparaciones_promedio | 5.000000 | 6.000000 | 7.000000 | 7.000000 | 12.000000 |
2 | Consulta 1 | tiempo | 0.024972 | 0.036585 | 0.019634 | 0.013380 | 0.078532 |
3 | Consulta 2 | comparaciones | 50000.000000 | 60000.000000 | 70000.000000 | 70000.000000 | 120000.000000 |
4 | Consulta 2 | comparaciones_promedio | 5.000000 | 6.000000 | 7.000000 | 7.000000 | 12.000000 |
5 | Consulta 2 | tiempo | 0.021208 | 0.031001 | 0.021719 | 0.018851 | 0.090308 |
6 | Consulta 3 | comparaciones | 50000.000000 | 60000.000000 | 70000.000000 | 70000.000000 | 120000.000000 |
7 | Consulta 3 | comparaciones_promedio | 5.000000 | 6.000000 | 7.000000 | 7.000000 | 12.000000 |
8 | Consulta 3 | tiempo | 0.023023 | 0.030506 | 0.018978 | 0.019227 | 0.069717 |
9 | Consulta 4 | comparaciones | 50000.000000 | 60000.000000 | 70000.000000 | 70000.000000 | 120000.000000 |
10 | Consulta 4 | comparaciones_promedio | 5.000000 | 6.000000 | 7.000000 | 7.000000 | 12.000000 |
11 | Consulta 4 | tiempo | 0.023000 | 0.035561 | 0.023235 | 0.014868 | 0.067240 |
resultados.items():
- Asumimos que resultados es un diccionario externo con una estructura como:
- .items() devuelve pares (clave, valor) del diccionario.
enumerate(…, start=1):
Numera las consultas comenzando desde 1 (en lugar de 0).
i toma valores 1, 2, 3, … para generar nombres como df_consulta1.
consulta_file es la clave del diccionario (ej: “consulta1”).
datos es el valor asociado (ej: {“método1”: {“comparaciones”: 10, “tiempo”: 0.5}, …}).
df_nombre = f”df_consulta{i}“:
- Genera un nombre dinámico para el DataFrame (ej: “df_consulta1”).
pd.DataFrame(datos).T:
datos es un diccionario anidado (ej: {“método1”: {“comparaciones”: 10, “tiempo”: 0.5}}).
pd.DataFrame(datos) lo convierte en un DataFrame donde:
Filas: Claves del diccionario interno (“comparaciones”, “tiempo”).
Columnas: Métodos de búsqueda (“método1”, “método2”, etc.).
.T transpone el DataFrame para que:
Filas: Métodos de búsqueda.
Columnas: Métricas (“comparaciones”, “tiempo”).
print(f"LISTA DE CONSULTAS 1")
= df_consultas.groupby('Archivo')
grouped = grouped.get_group('Consulta 1').copy() df_consulta1
LISTA DE CONSULTAS 1
print(f"LISTA DE CONSULTAS 2")
= df_consultas.groupby('Archivo')
grouped = grouped.get_group('Consulta 2').copy() df_consulta2
LISTA DE CONSULTAS 2
print(f"LISTA DE CONSULTAS 3")
= df_consultas.groupby('Archivo')
grouped = grouped.get_group('Consulta 3').copy() df_consulta3
LISTA DE CONSULTAS 3
print(f"LISTA DE CONSULTAS 4")
= df_consultas.groupby('Archivo')
grouped = grouped.get_group('Consulta 4').copy() df_consulta4
LISTA DE CONSULTAS 4
'ggplot')
plt.style.use(=(15, 10))
plt.figure(figsize
= df_consultas['Metrica'].unique()
metricas = [col for col in df_consultas.columns if col not in ['Archivo', 'Metrica']]
metodos = df_consultas['Archivo'].unique()
consultas
# Crear subgráficos para cada métrica
= plt.subplots(len(metricas), 1, figsize=(14, 12))
fig, axes
# Colores para cada método
= plt.cm.get_cmap('tab10', len(metodos))
colors
for i, metrica in enumerate(metricas):
= axes[i] if len(metricas) > 1 else axes
ax = df_consultas[df_consultas['Metrica'] == metrica]
df_metrica
# Ancho de las barras
= 0.15
width = np.arange(len(consultas))
x
for j, metodo in enumerate(metodos):
= df_metrica[metodo].values
valores + j*width, valores, width, label=metodo, color=colors(j))
ax.bar(x
f'Métrica: {metrica.capitalize()}', fontsize=12)
ax.set_title(+ width*(len(metodos)-1)/2)
ax.set_xticks(x
ax.set_xticklabels(consultas)
ax.set_ylabel(metrica.capitalize())True, linestyle='--', alpha=0.6)
ax.grid(
if i == len(metricas)-1:
=(1.05, 1), loc='upper left')
ax.legend(bbox_to_anchor
plt.tight_layout()'Comparación de Métodos de Búsqueda por Consulta', y=1.02, fontsize=14)
plt.suptitle( plt.show()
C:\Users\ThinkPad\AppData\Local\Temp\ipykernel_6100\607428116.py:12: MatplotlibDeprecationWarning: The get_cmap function was deprecated in Matplotlib 3.7 and will be removed in 3.11. Use ``matplotlib.colormaps[name]`` or ``matplotlib.colormaps.get_cmap()`` or ``pyplot.get_cmap()`` instead.
colors = plt.cm.get_cmap('tab10', len(metodos))
<Figure size 1440x960 with 0 Axes>
6. Conclusiones
Los resultados muestran que la búsqueda binaria y la estructura de datos SkipList son significativamente más eficientes en términos de número de comparaciones y tiempo de ejecución.
La búsqueda secuencial y las variantes no acotadas presentan un alto número de comparaciones y tiempos de respuesta más largos, lo que las hace menos recomendables para listas ordenadas. En particular, la búsqueda no acotada B2, que combina exploración secuencial con saltos aleatorios, mostró el peor desempeño en comparación con los demás algoritmos.
Por otro lado, la implementación de estructuras avanzadas como SkipList puede optimizar la búsqueda en grandes volúmenes de datos. Su rendimiento, en algunos casos, es incluso mejor que la búsqueda binaria debido a su estructura de múltiples niveles de acceso.
7. Referencias
Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms. MIT Press.
Knuth, D. E. (1998). The Art of Computer Programming, Volume 3: Sorting and Searching. Addison-Wesley.