Задачи модуля "Поиск": от бинарного поиска до Бойера-Мура
Задача 1. Бинарный поиск в массиве
Условие: Дан отсортированный по возрастанию массив целых чисел A длины N и искомое число key. Напишите функцию, которая находит индекс элемента key в массиве за время O(log N). Если элемент не найден, функция должна вернуть -1.
Сигнатура: int binary_search(int A[], int N, int key);
Подсказка
Используйте два указателя (индекса) на начало и конец текущего отрезка массива. На каждом шаге вычисляйте индекс середины и сравнивайте элемент в середине с искомым ключом.
Разбор
Бинарный поиск основан на том, что массив отсортирован. Мы сравниваем искомый элемент с элементом посередине массива. Если он меньше, то продолжаем поиск только в левой половине, если больше - в правой. На каждой итерации размер области поиска уменьшается вдвое, что и дает логарифмическую сложность.
Код на C
int binary_search(int A[], int N, int key) {
int lo = 0;
int hi = N - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; // Защита от переполнения
if (A[mid] == key) {
return mid; // Элемент найден
}
if (A[mid] < key) {
lo = mid + 1; // Ищем в правой половине
} else {
hi = mid - 1; // Ищем в левой половине
}
}
return -1; // Элемент не найден
}
Задача 2. Поиск в двоичном дереве (BST)
Условие: Дано двоичное дерево поиска (BST), узлы которого содержат целые числа. Напишите рекурсивную функцию, которая ищет узел с заданным значением val. Если узел найден, верните указатель на него. Если нет - верните NULL.
Подсказка
Воспользуйтесь основным свойством BST: если искомое значение меньше значения в корне, оно может быть только в левом поддереве. Если больше - только в правом.
Разбор
Рекурсивный поиск в BST очень похож на бинарный поиск в массиве. Базовый случай - если мы дошли до пустого узла (возвращаем NULL) или нашли узел с нужным значением. Иначе мы рекурсивно вызываем функцию либо для левого, либо для правого сына.
Код на C
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
};
struct TreeNode* searchBST(struct TreeNode* root, int val) {
// Базовый случай: корень пуст или ключ найден
if (root == NULL || root->val == val) {
return root;
}
// Ключ больше, чем ключ корня → ищем в правом поддереве
if (root->val < val) {
return searchBST(root->right, val);
}
// Ключ меньше → ищем в левом поддереве
return searchBST(root->left, val);
}
Задача 3. Поиск подстроки (Бойер-Мур-Хорспул)
Условие: Напишите функцию, которая использует эвристику "плохого символа" (алгоритм Бойера-Мура-Хорспула) для поиска первого вхождения образца pat в строке txt. Функция должна возвращать индекс первого вхождения или -1, если образец не найден.
Подсказка 1
Для реализации эвристики "плохого символа" вам потребуется заранее составить таблицу сдвигов для всех возможных символов алфавита. По умолчанию сдвиг равен длине образца.
Подсказка 2
Если алфавит состоит из 256 символов (расширенный ASCII), создайте массив badChar[256]. Пройдитесь по всем символам образца, кроме последнего, и запишите для каждого из них сдвиг: badChar[(unsigned char)pat[i]] = m - 1 - i.
Подсказка 3
Основной цикл: сравнивайте строку и образец справа налево. Если найдено несовпадение, сдвигайте шаблон вправо на значение из badChar для символа текста, который оказался напротив последнего символа образца.
Разбор
Алгоритм Бойера-Мура-Хорспула работает быстрее наивного поиска за счет того, что при несовпадении символов он может "перепрыгнуть" сразу несколько символов в тексте.
Таблица сдвигов указывает, на сколько позиций можно безопасно сдвинуть образец вправо, если произошла ошибка. Величину сдвига задает не тот символ, на котором сорвалось сравнение, а последний символ окна - в коде это txt[shift + m - 1]. Именно на этом месте решение чаще всего и разъезжается. Возьмем текст ABRACADABRA и образец CADA: буквы B и R в образец не входят и дают прыжок на всю его длину, а C, A и D стоят в таблице со своими расстояниями до конца.
CADA в тексте ABRACADABRA. В окне 0 сравнение срывается на позиции 2, но в таблицу идет символ T[3] = A, и прыжок получается на 2, а не на 1.Код на C
#include <string.h>
#define ALPHABET_SIZE 256
int search_bmh(const char *txt, const char *pat) {
int m = strlen(pat);
int n = strlen(txt);
if (m == 0) return 0;
if (m > n) return -1;
int badChar[ALPHABET_SIZE];
// 1. Заполняем таблицу сдвигов максимальным значением (длиной образца)
for (int i = 0; i < ALPHABET_SIZE; i++) {
badChar[i] = m;
}
// 2. Рассчитываем сдвиги для символов образца (кроме последнего)
for (int i = 0; i < m - 1; i++) {
badChar[(unsigned char)pat[i]] = m - 1 - i;
}
// 3. Поиск
int shift = 0;
while (shift <= (n - m)) {
int j = m - 1;
// Сравниваем символы справа налево
while (j >= 0 && pat[j] == txt[shift + j]) {
j--;
}
if (j < 0) {
return shift; // Найдено!
} else {
// Эвристика плохого символа
shift += badChar[(unsigned char)txt[shift + m - 1]];
}
}
return -1; // Не найдено
}