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 random4A. 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.
ruta_datos = "./archivos/p-032_ordenado.json"
ruta_consultas = [
f"./archivos/consultas-{i}-listas-posteo.json"
for i in range(1, 5)
]
with open(ruta_datos, 'r') as f:
datos = json.load(f)
datos_ordenados = sorted(datos.keys())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."""
comparaciones = 0
sp, ep = 0, len(A) - 1
while sp < ep:
comparaciones += 1
mid = (sp + ep) // 2
if x <= A[mid]:
ep = mid
else:
sp = mid + 1
comparaciones += 1
return (sp if x <= A[sp] else sp + 1), comparacionesBUSQUEDA 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."""
comparaciones = 0
for i, elemento in enumerate(A):
comparaciones += 1
if x <= elemento:
return i, comparaciones
return len(A), comparacionesBUSQUEDA 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)
"""
comparaciones = 0
n = len(A)
if n == 0:
return 0, 0
# Fase 1: Determinación del rango con doblado
i = 1
p = 0
while p + i < n and A[p + i] < x:
comparaciones += 1
p += i
i *= 2
# Fase 2: Búsqueda binaria en el rango encontrado
sp = p
ep = min(p + i, n - 1)
while sp < ep:
comparaciones += 1
mid = (sp + ep) // 2
if x <= A[mid]:
ep = mid
else:
sp = mid + 1
comparaciones += 1
return (sp if x <= A[sp] else sp + 1), comparacionesBUSQUEDA 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)
"""
comparaciones = 0
n = len(A)
if n == 0:
return 0, 0
# Fase 1: Determinación del rango con doblado doble
i = 1
j = 1
p = 0
while p + i < n and A[p + i] < x:
comparaciones += 1
p += i
i = j * j
j += 1
# Fase 2: Aplicar B1 en el rango encontrado
sp = p
ep = min(p + i, n - 1)
# Implementación de B1 dentro del rango
k = 1
q = sp
while q + k < ep and A[q + k] < x:
comparaciones += 1
q += k
k *= 2
# Fase 3: Búsqueda binaria en el subrango final
left = q
right = min(q + k, ep)
while left < right:
comparaciones += 1
mid = (left + right) // 2
if x <= A[mid]:
right = mid
else:
left = mid + 1
comparaciones += 1
return (left if x <= A[left] else left + 1), comparacionesSKIP 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
current_level = 0
while random.random() < self.p:
current_level += 1
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):
comparaciones = 0
# Comenzar desde el nivel más alto
level = len(self.levels) - 1
pos = -1
while level >= 0:
current_list = self.levels[level]
idx = bisect_left(current_list, clave)
if idx < len(current_list) and current_list[idx] == clave:
comparaciones += 1
return idx, comparaciones
comparaciones += 1
if idx > 0 and level > 0:
# Buscar en el rango correspondiente en el nivel inferior
lower_val = current_list[idx-1]
lower_idx = bisect_left(self.levels[level-1], lower_val)
pos = lower_idx
elif level == 0:
pos = idx
level -= 1
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:
consultas = json.load(f)
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():
total_comparaciones = 0
tiempo_inicio = time.time()
for consulta in consultas:
clave = str(consulta)
if nombre == "skiplist":
_, comparaciones = metodo(clave)
else:
_, comparaciones = metodo(datos_ordenados, clave)
total_comparaciones += comparaciones
tiempo_total = time.time() - tiempo_inicio
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:
resultados_totales[consulta_file] = ejecutar_pruebas(consulta_file)
# Mostrar resultados
def mostrar_resultados():
dfs = []
for i, (consulta_file, datos) in enumerate(resultados_totales.items(), 1):
df = pd.DataFrame(datos).T
df['Archivo'] = f'Consulta {i}'
dfs.append(df)
# Concatenar todos los DataFrames
df_final = pd.concat(dfs)
# Reorganizar los datos para mejor visualización
# Primero resetear el índice para tener los nombres de métodos como columna
df_final = df_final.reset_index().rename(columns={'index': 'Metodo'})
# Usar melt para tener una estructura más adecuada
df_melted = pd.melt(df_final, id_vars=['Archivo', 'Metodo'],
value_vars=['comparaciones', 'tiempo', 'comparaciones_promedio'],
var_name='Metrica', value_name='Valor')
# Pivotar para tener métodos como columnas
df_pivoted = df_melted.pivot_table(index=['Archivo', 'Metrica'],
columns='Metodo', values='Valor')
return df_pivoted.reset_index()
df_consultas = mostrar_resultados()
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")
grouped = df_consultas.groupby('Archivo')
df_consulta1 = grouped.get_group('Consulta 1').copy()LISTA DE CONSULTAS 1
print(f"LISTA DE CONSULTAS 2")
grouped = df_consultas.groupby('Archivo')
df_consulta2 = grouped.get_group('Consulta 2').copy()LISTA DE CONSULTAS 2
print(f"LISTA DE CONSULTAS 3")
grouped = df_consultas.groupby('Archivo')
df_consulta3 = grouped.get_group('Consulta 3').copy()LISTA DE CONSULTAS 3
print(f"LISTA DE CONSULTAS 4")
grouped = df_consultas.groupby('Archivo')
df_consulta4 = grouped.get_group('Consulta 4').copy()LISTA DE CONSULTAS 4
plt.style.use('ggplot')
plt.figure(figsize=(15, 10))
metricas = df_consultas['Metrica'].unique()
metodos = [col for col in df_consultas.columns if col not in ['Archivo', 'Metrica']]
consultas = df_consultas['Archivo'].unique()
# Crear subgráficos para cada métrica
fig, axes = plt.subplots(len(metricas), 1, figsize=(14, 12))
# Colores para cada método
colors = plt.cm.get_cmap('tab10', len(metodos))
for i, metrica in enumerate(metricas):
ax = axes[i] if len(metricas) > 1 else axes
df_metrica = df_consultas[df_consultas['Metrica'] == metrica]
# Ancho de las barras
width = 0.15
x = np.arange(len(consultas))
for j, metodo in enumerate(metodos):
valores = df_metrica[metodo].values
ax.bar(x + j*width, valores, width, label=metodo, color=colors(j))
ax.set_title(f'Métrica: {metrica.capitalize()}', fontsize=12)
ax.set_xticks(x + width*(len(metodos)-1)/2)
ax.set_xticklabels(consultas)
ax.set_ylabel(metrica.capitalize())
ax.grid(True, linestyle='--', alpha=0.6)
if i == len(metricas)-1:
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.suptitle('Comparación de Métodos de Búsqueda por Consulta', y=1.02, fontsize=14)
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.