
Оценка порядка алгоритмов
n |
log2n |
nlog2n |
n2 |
n3 |
2n |
2 |
1 |
2 |
4 |
8 |
4 |
4 |
2 |
8 |
16 |
64 |
16 |
8 |
3 |
24 |
64 |
512 |
256 |
16 |
4 |
64 |
256 |
4096 |
65536 |
32 |
5 |
160 |
1024 |
32768 |
4294967296 |
128 |
7 |
896 |
16384 |
2097152 |
3.41038 |
1024 |
10 |
10240 |
1048576 |
1073741824 |
1.810308 |
65536 |
16 |
1048576 |
4294967296 |
2.81014 |
Избегайте! |
4.4. Последовательный и бинарный поиск
Теперь познакомимся с последовательным поиском в целях нахождения некоторого значения в списке. Предположим, что мы ищем пределы списка целых с использованием этого алгоритма. В действительности, мы можем выполнять поиск в массиве любого типа, для которого определен оператор = =. Необходимо модифицировать последовательный поиск для ссылки на параметризованный тип DataType, который является псевдонимом фактического типа. Мы создаем этот псевдоним, используя ключевое слово typedef. Например:
typedef int DataType; //DataType это int
или
typedef double DataType: //DataType это double
Если предположить, что программист имеет определенный тип DataType, то код для общего алгоритма последовательного поиска следующий:
// поиск в массиве а из n элементов для нахождения соответствия с ключем использовать
//последовательный поиск, возвращать индекс соответствующего элемента массива или
// –1, если нет соответствия
int SeqSearch(DataType list[ ], int n, DataType key)
{
for (int i=0; i < n; i++)
if (list[i] == key)
return i; //возвращать индекс соответствующего элемента
return -1; //поиск неудачный, возвращать -1
}
При определении порядка алгоритма последовательного поиска различают поведение наилучшего и наихудшего случаев. Наилучшему случаю соответствует нахождение ключа в первом элементе списка. Время выполнения алгоритма при этом составляет O(1). Наихудший случай имеет место, когда этот ключ не находится в списке или обнаруживается в конце списка. Он требует проверки всех п элементов и имеет порядок O(п). Средний случай требует небольшого количества вероятностных рассуждений. Для случайного списка совпадение с ключом может с одинаковой вероятностью появиться в любой позиции списка. После выполнения проверок большого количества элементов средняя позиция совпадения – это срединный элемент (midpoint) п/2. Эта промежуточная точка анализируется после n/2 сравнений, что определяет ожидаемую стоимость поиска. По этой причине мы говорим, что средняя эффективность последовательного поиска составляет O(п).
Бинарный поиск
Последовательный поиск применим для любого списка. Если список является упорядоченным, алгоритм, называемый бинарный поиск (binary search), предоставляет улучшенный метод поиска. Ваш опыт по нахождению номера в большом телефонном справочнике – это модель такого алгоритма. Зная нужные имя и фамилию, вы открываете справочник ближе к началу, середине или концу, в зависимости от первой буквы фамилии. Вам может повезти, и вы сразу попадете на нужную страницу. В противном случае вы переходите к более ранней или более поздней странице в справочнике в зависимости от относительного местоположения имени человека по алфавиту. Например, если имя человека начинается с буквы R, а вы находитесь на странице с именами на букву Т, вы переходите на более раннюю страницу. Процесс продолжается до тех пор, пока вы не найдете соответствие или не обнаружите, что этого имени нет в справочнике. Соответствующая идея применима к поиску в упорядоченном списке. Мы идем к середине списка и ищем быстрое соответствие ключа значению срединного элемента. Если нам не удается найти соответствия, мы смотрим на относительный размер ключа и значение срединного элемента в затем перемещаемся в нижнюю или верхнюю половину списка. В общем, если мы знаем, как упорядочены данные, мы можем использовать эту информацию» чтобы сократить время поиска.
Следующие шаги описывают алгоритм. Предположим, что список упорядочен, как массив. Индексами в концах списка являются: low = 0 и high = п – 1, где n – это количество элементов в массиве.
1. Сравнить индекс срединного элемента массива:
mid = (low+high)/2.
2. Сравнить значение в срединном элементе с key (ключ).
Если совпадение найдено, возвращать индекс mid для нахождения ключа.
if (A[mid] = = key)
return(mid);
Если A[mid] < key, совпадение должно происходить в диапазоне индексов mid+1 ... high, в правой половине рассматриваемого списка. Это верно, потому что список упорядочен. Новыми границами являются low = mid+1 и high.
Е
А
Пример 4.2
Рассмотрим массив целых А. Этот пример дает выборку алгоритма для заданного ключа 33.
Заметьте, что этот алгоритм требует трех (3) сравнений. При линейном поиске в списке требуется восемь (8) сравнений.
Реализация бинарного поиска
Функция использует параметризованное имя DataType, которое должно поддерживать оба оператора: равенства (= =) и меньше чем (). Первоначально low равно 0, a high – (n–1), где n – число элементов в этом массиве. Функция возвращает номер удовлетворяющего условию элемента массива или -1, если такой элемент не найден (low>high).
// dsearch.h
// просмотреть сортированный массив на предмет совпадения с ключом, используя
// бинарный поиск, возвращать индекс совпадающего элемента массива или -1, если
// совпадение не происходит
int BinSearch(DataType list[], int low, int high, DataType key)
{
int mid;
DataType midvalue;
while (low <= high)
{
mid = (low+high)/2; // raid-индекс в подсписке
midvalue = list(mid); // значение при mid-индексе
if (key == midvalue)
return mid; // совпадение имеется, возвращаем его положение в массиве
else if (key < midvalue)
high = mid-1; // перейти в нижний подсписок
else
low = mid+1; // перейти в верхний подсписок
}
return -1; // элемент не найден
}
Реализация последовательного и бинарного поиска включена в файл dsearch.h. Так как эта функция зависит от класса DataType, определение DataType должно предшествовать включению этого файла.
Программа 4.1. Сравнение последовательного и бинарного поиска
Программа сравнивает время вычисления последовательного и бинарного поиска. Массив A заполняется 1000 случайными целыми числами в диапазоне 0 ... 1999 и затем сортируется. Второму массиву B присваиваются 500 случайных целых чисел в том же диапазоне. Элементы в массиве B используются как ключи для алгоритмов поиска. Временная функция TickCount определяется в файле ticks.h и возвращает количество 1/60-х секунд со времени запуска системы. Мы измеряем время, которое занимает выполнение 500 поисков, используя каждый алгоритм. Выходная информация включает время в секундах и количество соответствий.
#include <iostream.h>
typedef int DataType; // данные типа integer
#include "dsearch.h"
#include "random.h"
#include "ticks.h"
// сортировать целый массив из n элементов в возрастающем порядке
void ExchangeSort(int a[], int n)
{
int i, j, temp;
for (i=0;i< n-1; i++)
// поместить минимум элементов a[i]...a[n-1] в a[i]
for (j -i+1; j <n; j++)
// если a[j] < a[i], выполнить их замену
if (a[j] <a[i])
{
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
void main (void)
{
//А содержит список для поиска, В содержит ключи
int A[1000], В[500];
int i, matchCount;
// используется для данных времени
long tcount;
RandomNumber rnd;
// создать массив А из 1000 случайных чисел со значениями в диапазоне 0.. 1999
for (i=0; i < 1000; i++)
A[i] = rnd.Random(2000);
ExchangeSort(A,1000) ;
// генерировать 500 случайных ключей из того же диапазона
for (i=0; i < 500; i++)
B[i] = rnd.Random(2000);
cout << "Время последовательного поиска" << endl;
tcount = TickCount (); // время начала
matchCount = 0;
for (i = 0; i < 500; i++)
if (SeqSearch(A,1000, B[i]) != -1)
matchCount++;
tcount = TickCount() - tcount;
cout << "Последовательный поиск занимает " << tcount/60.0
<< " секунд для “ << matchCount << " совпадений. " << endl;
cout << "Время бинарного поиска"<< endl;
tcount = TickCount() ;
matchCount = 0;
for (i-0; i < 500; i++)
if (BinSearch(A,0,999,B[i]) !--1)
matchCount++;
tcount - TickCount() - tcount;
cout << "Бинарный поиск занимает " << tcount/60.0
<< " секунд для " << matchCount << " совпадений. " << endl;
}
/* <Выполнение программы 4.1>
Время последовательного поиска
Последовательный поиск занимает 0.816667 секунд для 181 совпадений.
Время бинарного поиска
Бинарный поиск занимает 0.016667 секунд для 181 совпадений.
*/
Неформальный анализ для бинарного поиска. Наилучший случай имеет место, когда совпадающий с ключом элемент находится в середине списка. При этом порядок алгоритма составляет O(1), так как требуется только одно тестирующее сравнение равенства. При наихудшем случае, когда элемент не находится в списке или определяется в последнем сравнении, имеем порядок O(log2n). Мы можем интуитивно вывести этот порядок. Наихудший случай возникает, когда мы должны уменьшать подсписок до длины 1. Каждая итерация, которая не находит соответствие, уменьшает длину подсписка на множитель 2. Размеры подсписков следующие:
n n/2 n/4 n/8 ... 1
Разделение на подсписки требует m итераций, где m – это приблизительно log2n (см. подробный анализ). Для наихудшего случая мы имеем начальное сравнение в середине списка и затем – ряд итераций log2n. Каждая итерация требует одну операцию сравнения:
Total Comparisons = 1 + log2n
В результате наихудшим случаем для бинарного поиска является 0(log2n). Этот результат проверяется эмпирически программой 4.1. Отношение времени выполнения последовательного поиска ко времени выполнения бинарного поиска равно 49,0. Теоретическое отношение ожидаемого времени приблизительно составляет 500/log21000) = 50,2.
Формальный анализ бинарного поиска. Первая итерация цикла имеет дело со всем списком. Каждая последующая итерация делит пополам размер подсписка. Так, размерами списка для алгоритма являются
n n/21 n/22 n/23 n/24 ... n/2m
В конце концов будет такое целое т, что
n/2m<2 или n<2m+1
Так как m – это первое целое, для которого n/2m < 2, то должно быть верно
n/2m–12 или 2m m
Из этого следует, что
2m n <2m+1
Возьмите логарифм каждой части неравенства и получите log2n = х действительному числу:
m log2n = x < m+1
Значение m – это наибольшее целое, которое х и задается int(x). Например, если n=50, log250=5,644. Следовательно,
m = int(5,644) = 5
Можно показать, что средний случай также составляет O(log2n).