Загрузка данных


"""
Бинарный и интерполяционный поиск
  • по массиву чисел
  • по массиву строк (без преобразования строк в числа — только ord символа)

Интерполяция для строк работает через первый различающийся символ
граничных элементов a[lo] и a[hi]. Это даёт относительную позицию
target прямо по алфавиту — минимальное вмешательство в данные.
"""

import os
import random
import string
import time
import urllib.request


# =====================================================================
#  1. ПОДГОТОВКА ДАННЫХ
# =====================================================================

WORDS_URL = ("https://raw.githubusercontent.com/dwyl/"
             "english-words/master/words_alpha.txt")
WORDS_FILE = "words_alpha.txt"


def load_word_dictionary(min_size=50_000):
    """
    Пытается скачать словарь, иначе берёт системный,
    иначе генерирует локально. Результат всегда отсортирован.
    """
    words = []

    # 1) попытка скачать
    try:
        if not os.path.exists(WORDS_FILE):
            print(f"Скачиваем словарь: {WORDS_URL}")
            urllib.request.urlretrieve(WORDS_URL, WORDS_FILE)
        with open(WORDS_FILE, "r", encoding="utf-8") as f:
            words = [w.strip() for w in f if w.strip()]
        print(f"Загружено слов из скачанного словаря: {len(words)}")
    except Exception as e:
        print(f"Скачать не удалось ({e}).")

    # 2) системный словарь
    if len(words) < min_size:
        for path in ("/usr/share/dict/words",
                     "/usr/share/dict/american-english",
                     "/usr/dict/words"):
            if os.path.exists(path):
                try:
                    with open(path, "r", encoding="utf-8", errors="ignore") as f:
                        words = [w.strip() for w in f if w.strip()]
                    print(f"Взят системный словарь {path}: {len(words)} слов")
                    break
                except Exception:
                    pass

    # 3) локальная генерация
    if len(words) < min_size:
        print(f"Генерируем словарь из {min_size} слов локально.")
        pool = set()
        while len(pool) < min_size:
            pool.add(''.join(random.choices(string.ascii_lowercase,
                                            k=random.randint(3, 9))))
        words = list(pool)

    return sorted(words)


def gen_numbers(n=100_000):
    return sorted(random.sample(range(1, n * 10), n))


# =====================================================================
#  2. БИНАРНЫЙ ПОИСК (универсальный — работает и для чисел, и для строк)
# =====================================================================

def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    comps = 0
    while lo <= hi:
        comps += 1
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid, comps
        if arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1, comps


# =====================================================================
#  3. ИНТЕРПОЛЯЦИОННЫЙ ПОИСК ЧИСЕЛ
# =====================================================================

def interpolation_search_numbers(arr, target):
    lo, hi = 0, len(arr) - 1
    comps = 0
    while lo <= hi and arr[lo] <= target <= arr[hi]:
        comps += 1
        if arr[hi] == arr[lo]:
            return (lo if arr[lo] == target else -1), comps

        pos = lo + (target - arr[lo]) * (hi - lo) // (arr[hi] - arr[lo])

        if arr[pos] == target:
            return pos, comps
        if arr[pos] < target:
            lo = pos + 1
        else:
            hi = pos - 1
    return -1, comps


# =====================================================================
#  4. ИНТЕРПОЛЯЦИОННЫЙ ПОИСК СТРОК — БЕЗ ПРЕОБРАЗОВАНИЙ
#
#  Для интерполяции нужно уметь оценить, "где между a[lo] и a[hi]"
#  находится target. Для строк делаем это так:
#    • находим первый индекс k, где a[lo][k] != a[hi][k];
#    • берём ord() символов на этой позиции как числовую ось;
#    • считаем относительную позицию target на этой оси.
#
#  ord() — встроенный код символа (не преобразование строки!).
#  Ни хешей, ни base-N ключей, ни предварительной подготовки.
# =====================================================================

def string_interp_ratio(s_lo, s_hi, target):
    """
    Оценка позиции target между s_lo и s_hi (s_lo <= target <= s_hi)
    в виде вещественного числа из [0, 1].
    Работает прямо по символам строк.
    """
    n = min(len(s_lo), len(s_hi))
    k = 0
    while k < n and s_lo[k] == s_hi[k]:
        k += 1

    if k == n:
        # одна строка — префикс другой; считаем позицию средней
        return 0.5

    lo_code = ord(s_lo[k])
    hi_code = ord(s_hi[k])
    if hi_code == lo_code:
        return 0.5

    # символ target на позиции k (0 — если строка кончилась раньше)
    t_code = ord(target[k]) if k < len(target) else 0

    r = (t_code - lo_code) / (hi_code - lo_code)
    if r < 0.0:
        r = 0.0
    if r > 1.0:
        r = 1.0
    return r


def interpolation_search_strings(arr, target):
    lo, hi = 0, len(arr) - 1
    comps = 0
    while lo <= hi:
        # target вне текущего диапазона — искать нечего
        if arr[lo] > target or arr[hi] < target:
            return -1, comps
        comps += 1

        if lo == hi:
            return (lo if arr[lo] == target else -1), comps

        r = string_interp_ratio(arr[lo], arr[hi], target)
        pos = lo + int(r * (hi - lo))
        if pos < lo:
            pos = lo
        if pos > hi:
            pos = hi

        if arr[pos] == target:
            return pos, comps
        if arr[pos] < target:
            lo = pos + 1
        else:
            hi = pos - 1
    return -1, comps


# =====================================================================
#  5. ЗАМЕРЫ И ВЫВОД
# =====================================================================

def measure(fn, arr, target):
    t0 = time.perf_counter_ns()
    idx, comps = fn(arr, target)
    dt = time.perf_counter_ns() - t0
    return idx, comps, dt


def show_block(title, arr, cases, interp_fn):
    print()
    print("=" * 92)
    print(title)
    print("=" * 92)
    print(f"{'Случай':<16}{'Поиск':<24}{'Индекс':<10}"
          f"{'Сравнений':<12}{'Время, мкс':<12}")
    print("-" * 92)
    for name, tgt in cases:
        ib, cb, tb = measure(binary_search, arr, tgt)
        ii, ci, ti = measure(interp_fn, arr, tgt)
        print(f"{name:<16}{'бинарный':<24}{('—' if ib<0 else ib):<10}"
              f"{cb:<12}{tb/1000:<12.2f}")
        print(f"{'':<16}{'интерполяционный':<24}"
              f"{('—' if ii<0 else ii):<10}{ci:<12}{ti/1000:<12.2f}")
        print("-" * 92)


def main():
    # ---------- ЧИСЛА ----------
    nums = gen_numbers(100_000)
    print(f"Числовой массив: {len(nums)} элементов, "
          f"[{nums[0]}..{nums[-1]}]")
    num_cases = [
        ("первый",       nums[0]),
        ("середина",     nums[len(nums) // 2]),
        ("последний",    nums[-1]),
        ("отсутствует",  -1),
    ]
    show_block("ЧИСЛА: бинарный vs интерполяционный",
               nums, num_cases, interpolation_search_numbers)

    # ---------- СТРОКИ ----------
    words = load_word_dictionary(50_000)
    print(f"\nСтроковый массив: {len(words)} элементов, "
          f"'{words[0]}'..'{words[-1]}'")
    str_cases = [
        ("первый",       words[0]),
        ("середина",     words[len(words) // 2]),
        ("последний",    words[-1]),
        ("отсутствует",  "zzzzzzzzzzz"),
    ]
    show_block("СТРОКИ: бинарный vs интерполяционный",
               words, str_cases, interpolation_search_strings)

    # ---------- ИТОГИ ----------
    print()
    print("=" * 92)
    print("ИТОГ: сравнение эффективности")
    print("=" * 92)
    print("""
  ЧИСЛА (равномерные):
      • бинарный            ~17 сравнений
      • интерполяционный    1–3 сравнения
      → ускорение в разы на каждом поиске.

  СТРОКИ (словарь):
      • бинарный            ~log2(N) ≈ 16 сравнений
      • интерполяционный    обычно 1–5 сравнений, но зависит от
                            распределения слов по алфавиту.
      → выигрыш есть, но меньше, чем на числах, потому что слова
        распределены неравномерно ('a','b','c' — густо,
        'x','y','z' — пусто).

  СЛОЖНОСТЬ:
      • бинарный              O(log n) в любом случае
      • интерполяционный      O(log log n) при равномерных данных,
                              O(n) — в худшем случае.

  ПРИМЕЧАНИЕ про строки:
      Интерполяция сделана БЕЗ каких-либо преобразований строк —
      только по первому различающемуся символу через ord().
      Никаких хешей, base-N ключей и предвычислений.
""")


if __name__ == "__main__":
    main()