Метод_материалы / Учебники / Программирование_С
.pdfЭто сортировка с использованием дерева, которое аналогично дереву турнира. Аналогичным образом можно построить дерево для массива, причем «победителем» в каждой паре является наибольший элемент. Таким образом,
корнем дерева станет максимальный элемент массива.
Следующий шаг: ищем второй по величине элемент, для чего максимальный элемент помещаем в некоторый выходной массив — на последнее или первое место, в зависимости от желаемого порядка результата. Затем заменяем максимальный элемент исходного массива на значение, заведомо меньшее любого его элемента — барьер (в примере это –1), и перестраиваем дерево. В итоге корнем станет второй по величине элемент. Процесс повторяем, пока все элементы исходного массива не станут барьером.
Пример. Исходный массив
25 57 48 37 12 92 86 33
Процесс построения деревьев показан на рис 3.5.
Если исходный массив имеет размер n, который не является степенью числа 2, то в лишние листья при инициализации устанавливается барьер.
Напишите программу, реализующую этот метод. 3.1.2.4.4. Задачи на другие сортировки
3.1.2.4.4.1. Объединение массивов
Рассмотрим следующий метод объединения упорядоченных массивов а и b в массив с. Выполним бинарный поиск элемента b[1] в массиве а. Если b[1] находится между a[i] и a[i + 1], то выводим элементы от а[1] до a[i] в массив с, затем запишем элемент b[1] в массив с. Далее выполним бинарный поиск элемента b[2] в подмассиве с элементами от a[i + 1] до а[iа] (где iа является числом элементов в массиве а) и повторим процесс вывода. Повторим эту процедуру для каждого элемента в массиве b.
Напишите программу, реализующую этот метод.
3.1.2.4.4.2. Бинарное слияние
Рассмотрим следующий метод объединения отсортированных массивов а и b в массив с (называемый бинарным слиянием). Пусть 1а и 1b будут числом элементов в массивах а и b соответственно, и предположим, что 1а ≥ 1b. Разделим массив а на 1b + 1 примерно равных подмассивов. Сравним b[1] с наименьшим элементом во втором подмассиве массива а. Если b[1] меньше, тогда найдем a[i] такой, что a[i] ≤ b[1] ≤ a[i + 1], при помощи бинарного поиска в первом подмассиве. Выведем в массив с все элементы первого подмассива до элемента a[i] включительно, а затем выведем в массив с элемент b[1]. Повторим этот процесс для элементов b[2], b[3], ..., b[j], где b[j] оказывается элементом большим, чем наименьший элемент во втором подмассиве. Выведем в массив с все оставшиеся элементы из первого подмассива и первый элемент из второго подмассива. Затем сравним b[j] с наименьшим элементом в третьем подмассиве массива а и т. д.
Напишите программу, реализующую бинарное слияние.
201
3.1.2.4.4.3. Рекурсивное слияние
Напишите программу, которая реализует рекурсивный алгоритм сортировки некоторого массива mas следующим способом:
а) пусть k будет индексом среднего элемента в данном массиве, б) отсортируйте элементы до элемента mas[k] и включая его, в) отсортируйте элементы после mas[k],
г) соедините эти два подмассива в один отсортированный массив.
3.1.2.4.4.4. Естественное слияние
В сортировке простым слиянием все массивы имеют одинаковый размер (за исключением, возможно, последнего). Можно, однако, использовать любой порядок, который уже может существовать среди элементов, а подмассивы определить как самые длинные подмассивы из возрастающих элементов.
Напишите программу, реализующую этот метод.
3.1.3. Поиск:
Алгоритмы поиска занимают очень важное место в общей иерархии прикладных алгоритмов. Любые данные хранятся на магнитных носителях в определенным образом упорядоченном наборе. Найти некоторую запись из этого набора (и, возможно, что — то с этой записью сделать) — вот одна из классических задач программирования, вокруг которой было сгенерировано множество идей. Представим себе некоторую базу данных как таблицу, состоящую из записей (рис. 3.6).
Рис. 3.6. Таблица записей
Первое поле каждой записи содержит некоторый ключ (им может быть, например, табельный номер сотрудника), второе поле — фамилию сотрудника, и так далее. Эта так называемая плоская таблица далее будет для нас основой при изучении всех алгоритмов поиска.
Ключом может служить любое поле записи; фамилия сотрудника в этом качестве ничуть не хуже его табельного номера. Основная задача любого алгоритма поиска: найти запись с заданным ключом.
202
Все алгоритмы поиска разбиваются на две большие группы в зависимости от того, упорядочена или же нет таблица, в которой проводятся поиск. Упорядоченность мы будем понимать как наличие хотя бы одного отсортированного поля — а именно, ключевого.
3.1.3.1. Последовательный поиск
Наиболее примитивный — а значит, наименее эффективный способ поиска — это последовательный поиск. Он применяется для неупорядоченных таблиц — начинаем с начала и продвигаемся либо до обнаружения нужного элемента, либо до конца таблицы. Ввиду простоты метода псевдокод не приводится.
3.1.3.1.1. Улучшения последовательного поиска.
Разумеется, последовательный поиск неэффективен, поскольку для нахождения элемента, расположенного в конце таблицы, придется просмотреть почти всю таблицу. Поэтому целесообразно переупорядочивать таблицу так, что записи, доступ к которым осуществляется более часто, передвигались бы к началу, а записи, доступ к которым осуществляется менее часто, передвигались бы к концу. Эта идея реализуется при помощи двух основных методов.
3.1.3.1.1.1. Метод перемещения в начало.
Этот метод является эффективным только для таблицы, организованной как список. В этом методе, когда поиск выполняется успешно (т. е. когда найдено, что искомый элемент совпадает с ключом некоторой записи в списке), извлеченная запись удаляется из ее текущей позиции в списке и помещается в голову списка. Эта операция, как нам уже известно, не приводит к перемещению данных и сводится к переброске указателей.
3.1.3.1.1.2. Метод транспозиции.
Извлеченная запись меняется местами с записью, которая ей предшествует. Этот метод эффективен как для таблицы, организованной как в виде списка, так и в виде массива, поскольку во втором случае все сводится к обмену местами двух соседних элементов массива и не требует перемещения больших объемов данных.
Оба этих метода основаны на том наблюдении, что запись, которая только что была извлечена, вероятно, будет извлечена снова. При перемещении таких записей в начало таблицы последующие поиски этих записей делаются более эффективными.
3.1.3.2. Поиск в упорядоченной таблице
Хранение данных в упорядоченной таблице значительно эффективнее, нежели в неупорядоченной, по той причине, что имеются гораздо лучше работающие методы поиска.
3.1.3.2.1.Бинарный поиск в упорядоченной таблице Бинарный поиск был ранее рассмотрен в разделе 3.1.2.1.2.
3.1.3.2.2.Поиск с накоплением группы запросов в упорядоченной таблице Идея этого поиска заключается в том, что входной поток искомых элементов
накапливается в некоторой структуре в упорядоченном виде; затем ищем
203
элементы в основной таблице, причем для каждого искомого элемента (кроме первого) поиск продолжается не с начала таблицы, а с текущего индекса.
3.1.3.2.3. Индексно — последовательный поиск в упорядоченной таблице Этот метод заключается в создании дополнительной таблицы, хранящей
некоторые «опорные» элементы основной таблицы – как правило, равноотстоящие друг от друга.
Рис. 3.7. Индексно — последовательный поиск
Поиск начинается с дополнительной таблицы — выясняем, в каком диапазоне основной таблицы следует искать элемент, после чего переходим к нужному подмассиву основной таблицы.
3.1.3.3. Хеширование таблиц и способы разрешения коллизий
3.1.3.3.1. Разрешение коллизий при хешировании методом открытой адресации
Пусть мы захотим ввести в таблицу новый номер изделия 0596397. Используя хеш-функцию исключения всех цифр, кроме трех последних, получим (0596397) =397 и что эта запись должна находиться в позиции 397 в массиве. Однако позиция 397 уже занята, поскольку там находится запись с ключом 4957397. Следовательно, запись с ключом 0596397 должна быть вставлена в таблицу в другом месте.
Самым простым методом разрешения коллизий при хешировании является помещение данной записи в следующую свободную позицию в массиве. Например, на запись с ключом 0596397 помещается в ячейку 398, которая пока свободна, поскольку 397 уже занята. Когда эта запись будет вставлена, другая запись, которая хешируется в позицию 397 (например, с таким ключом, как 8764397) или в позицию 398 (например, с таким ключом, как 2194398), вставляется в следующую свободную позицию, которая в данном случае равна
400.
Этот метод называется линейным опробованием, и он реализует некий общий подход разрешения коллизий при хешировании. Альтернативные названия —
повторное хеширование или открытая адресация. В общем случае функция
204
повторного хеширования rh воспринимает один индекс в массиве и выдает другой индекс. Если ячейка массива h(key) уже занята некоторой записью с другим ключом, то функция rh применяется к значению h(key) для того, чтобы найти другую ячейку, куда может быть помещена эта запись. Если ячейка rh(h(key)) также занята, то хеширование выполняется еще раз и проверяется ячейка rh(rh(h(key))). Этот процесс продолжается до тех пор, пока не будет найдена пустая ячейка. Таким образом, мы можем написать алгоритм поиска и вставки, используя хеширование, следующим образом. Мы предполагаем, что h является хеш-функцией, a rh — функцией повторного хеширования. Для указания пустой записи используется значение NULL, а для указания индекса вставляемой записи - переменная index.
{
i = h(key) //хешируем ключ
while ((k[i] != key) and (k[i] != NULL) )
i = rh(i); //мы должны выполнить повторное хеширование
if (k[i] == nullkey)
//вставляем запись в пустую позицию
{
k[i]=key; r[i] =rec;
}
index = i; return 0
}
Может случиться, что данный цикл будет выполняться бесконечно. Для этого существуют две возможные причины. Во-первых, таблица может быть полной, так что вставить какие-либо новые записи невозможно. Эта ситуация может быть обнаружена при помощи счетчика числа записей в таблице. Когда этот счетчик равен размеру таблицы, не надо проверять дополнительные позиции.
Возможно, однако, что этот алгоритм приведет к бесконечному зацикливанию, даже если имеются некоторые пустые позиции (или даже много таких позиций). Предположим, например, что в качестве функции повторного хеширования используется функция rh(i) =mod(i + 2, 1000). Тогда любой ключ, который хешируется в нечетное целое число, повторно хешируется в следующие за ним нечетные целые числа, а любой ключ, который хешируется в четное число, повторно хешируется в следующие за ним четные целые числа. Рассмотрим ситуацию, при которой все нечетные позиции в таблице заняты, а все четные свободны. Несмотря на тот факт что половина позиций в массиве свободна, невозможно вставить новую запись, чей ключ хешируется в нечетное число.
3.1.3.3.2. Разрешение коллизий при хешировании методом цепочек Имеется несколько причин, почему повторное хеширование может быть
неадекватным методом для обработки коллизий при хешировании. Во-первых,
205
оно предполагает фиксированный размер таблицы. Если число записей превысит этот размер, то их невозможно вставлять без выделения таблицы большего размера и повторного вычисления значений хеширования для ключей всех записей, уже находящихся в таблице. Более того, из такой таблицы трудно удалить запись. Например, предположим, что в позиции р находится запись r1. При добавлении записи г2, чей ключ k2 хешируется в р, эта запись должна быть вставлена в первую свободную позицию rh(p), rh(rh(p)), .... Предположим, что r1 затем удаляется, так что позиция р становится свободной. Поиск записи г2 начинается с позиции h(k2), что равно р. Но поскольку эта позиция уже свободна, процесс поиска может ошибочно сделать вывод, что записи г2 нет в таблице.
Другой метод разрешения коллизий при хешировании называется методом цепочек. Он представляет собой организацию связанного списка из всех записей, чьи ключи хешируются в одно и то же значение. Предположим, что хеш-функция выдает значения в диапазоне от 0 до m – 1. Тогда описывается некоторый массив bucket, имеющий размер m и состоящий из узлов заголовков. Элемент bucket[i] указывает на список всех записей, чьи ключи хешируются в i. При поиске записи осуществляется доступ к заголовку списка, который занимает позицию i в массиве узлов. Если запись не найдена, то она вставляется в конец списка.
Предположим, что имеется массив из 10 элементов и что хеш-функция равна
mod(key, 10). Ключи представлены в таком порядке: |
|
|
|
||||||||||
75 |
66 |
42 |
192 |
91 |
40 |
49 |
87 |
67 |
16 |
417 |
130 |
372 |
227 |
Можно написать алгоритм поиска и вставки, используя метод цепочек с хешфункцией h и массивом узлов bucket (узлы содержат поля — поле k для ключа, поле r для записи и поле next в качестве указателя на следующий узел в списке):
{
i = h(key); q = null;
р = bucket [i]; while (p != null) do
{
if (k(p)=key)
{
search = p; return;
}
q=p;
p = next(p); };
//ключ не найден, вставляем новую запись s=getnode;
k(s) =key; r(s)=rec; next(s)=null; if (q==null)
206
bucket [I] =s; else
next(q) =s;
search = s; return;
}
Удаление узла из таблицы, которая построена по методу цепочек, заключается просто в исключении узла из связанного списка. Удаленный узел никак не влияет на эффективность алгоритма поиска. Алгоритм будет работать так, как если бы этот узел никогда не вставлялся в таблицу. Отметим, что эти списки могут быть динамически переупорядочены для получения большей эффективности поиска.
3.1.3.5. Задачи на поиск.
3.1.3.5.1. Поиск в циклическом списке Предположим, что упорядоченная таблица хранится в виде некоторого
циклического списка с двумя внешними указателями — table и other. Указатель table всегда указывает на узел, содержащий запись с наименьшим ключом. Указатель other первоначально равен указателю table, но каждый раз, когда выполняется поиск, он переустанавливается так, чтобы указывать на запись, которая извлечена. Если поиск был неудачным, то указатель other переустанавливается так, что он указывает на table. Напишите программу, которая принимает входную информацию TABLE, OTHER и KEY, реализует этот метод, переустанавливает переменную OTHER, как было описано, и устанавливает переменную SEARCH так, чтобы указывать на извлеченную запись или на некоторый пустой указатель, если поиск был неудачным.
3.1.3.5.2. Поиск в двусвязном списке Рассмотрим некоторую упорядоченную таблицу, представленную как массив
или как список с двумя связями, так что поиск в данной таблице может быть осуществлен последовательно вперед или назад. Предположим, что некоторый указатель р указывает на последнюю, успешно извлеченную запись. Поиск всегда начинается с записи, на которую указывает р, но он может продолжаться в любом направлении. Напишите подпрограмму для извлечения записи с ключом key и соответствующей модификации р для массива и для списка с двумя связями. Сравните число сравнений ключа для случаев успешного и неудачного поиска с методами из упражнения 4, где таблица может просматриваться только в одном направлении, но процесс просмотра может начинаться в одной из двух точек.
3.1.3.5.3. Фибоначчиев поиск Следующий алгоритм поиска в отсортированном массиве известен как
фибоначчиев поиск из-за использования чисел Фибоначчи (что касается определения чисел Фибоначчи и функции fib, см. разд. 3.1.1.6)
{
j=1;
while (fib[j] < n + 1 )
207
|
j = j + 1; |
|
mid=n-fib(j-2) + 1; |
|
f1=fib(j – 2); |
|
f2=fib(j – 3); |
|
finish=FALSE; |
|
while (key != k(mid)) and (finish = FALSE) |
|
{ |
|
if (mid <= 0) || (key > k(mid)) |
|
{ |
|
if (f1 == 1) |
|
finish=TRUE; |
|
else |
|
{ |
|
mid=mid + f2; |
|
f1 = f1 –f2; |
|
f2 = f2 – f1; |
|
} |
|
} |
|
else |
|
{ |
I |
if (f2 == 0) |
|
finish=TRUE; |
|
else |
|
{ |
|
mid = mid – f2; |
|
t = f1 – f2; |
|
f1 = f2; |
|
f2 = t; |
|
} |
|
} |
`}
if (finish) search = 0;
else
search=mid;
}
Напишите программу, реализующую данный метод.
3.1.3.5.4.Разрешение коллизий хеширования методом открытой адресации Напишите программу, реализующую данный метод.
3.1.3.5.5.Разрешение коллизий хеширования методом цепочек
Напишите программу, реализующую данный метод. 3.1.3.5.6. Метод перемещения в начало для массива Напишите программу, реализующую данный метод.
208
3.1.3.5.7.Метод перемещения в начало для списка Напишите программу, реализующую данный метод.
3.1.3.5.8.Метод транспозиций для массива Напишите программу, реализующую данный метод.
3.1.3.5.9.Метод накопления группы запросов Напишите программу, реализующую данный метод.
3.1.3.5.10.Индексно – последовательный поиск Напишите программу, реализующую данный метод.
3.2.Задачи на структурах данных
3.2.1.Задачи на массивах:
3.2.1.1.Рациональные числа
Напомним, что рациональным числом называется любое число, которое может быть представлено в виде отношения двух целых чисел. Так, 1/2, 3/4, 2/3 и 2 (т. е. 2/1) являются рациональными числами, а π таким числом не является.
В ЭВМ рациональное число обычно выражается посредством десятичного приближения. Если мы потребуем от ЭВМ напечатать число 1/3, то будет напечатано число 0,333333. Хотя это и достаточно точное приближение (разница между 0,333333 и 1/3 составляет всего одну трехмиллионную), оно тем не менее не является точным. Следовательно, желательно иметь такое представление рациональных чисел, при котором можно выполнять арифметические операции без потери точности.
Каким образом мы можем представить десятичное число без потери точности? Поскольку рациональное число состоит из числителя и знаменателя, мы можем представить рациональное число с помощью массива из двух элементов, содержащих числитель и знаменатель, например ratnum[2].
Мы ссылаемся к числителю как к ratnum[1].а к знаменателю как к ratnum[2]. Может показаться, что мы уже можем определить арифметические операции для нового введенного представления рациональных чисел, однако при этом возникает следующая проблема. Предположим, что мы определили два рациональных числа и присвоили им некоторые значения. Как, однако, мы
можем проверить равенство этих двух чисел?
Если числители и знаменатели равны, то рациональные числа также равны. Однако возможна ситуация, при которой числители и знаменатели не равны, но при этом сами рациональные числа равны между собой. Например, числа 1/2 и 2/4 равны между собой, хотя их числители (1 и 2) и знаменатели (2 и 4) не равны. Следовательно, необходим другой способ проверки.
209
Для проверки рациональных чисел на равенство необходимо сначала привести их к несократимым дробям, после чего можно проверить их на равенство между собой путем сравнения их числителей и знаменателей.
Определим несократимое рациональное число как такое рациональное число, для которого не существует целого числа, большего единицы, на которое числитель и знаменатель делятся без остатка. Так, 1/2, 2/3 и 10/1 являются несократимыми, а 4/8, 12/18 и 15/6 таковыми не являются.
Для приведения любой дроби к несократимой может быть использована процедура, известная как алгоритм Евклида. Эта процедура может быть описана следующим образом.
Пусть a есть наибольшее число из двух чисел — числителя и знаменателя, а b — наименьшее.
Разделим a на b, найдем частное u и остаток r (т. е. a = q*b + r).
Пусть a= b и b = r.
Повторять шаги 2 и 3 до тех пор, пока b не станет нулем. Разделим числитель и знаменатель на последнее значение а.
В качестве примера сократим дробь 1032/1976.
Шаг 1 |
a=1976 |
b=1032 |
Шаг 2 |
а=1976 |
b=1032 |
Шаг 3 |
а=1032 |
b = 944 |
Шаги 4 и 2 |
а=1032 |
b=944 |
Шаг 3 |
а = 944 |
b = 88 |
Шаги 4 и 2 |
а = 944 |
b = 88 |
Шаг 3 |
а = 88 |
b = 64 |
Шаги 4 и 2 |
а = 88 |
b = 64 |
Шаг 3 |
а = 64 |
b = 24 |
Шаги 4 и 2 |
а = 64 |
b = 24 |
Шаг 3 |
а = 24 |
b=16 |
Шаги 4 и 2 |
а = 24 |
b=16 |
Шаг 3 |
а=16 |
b = 8 |
Шаги 4 и 2 |
а=16 |
b = 8 |
Шаг 3 |
а = 8 |
b = 0 |
Шаг 5 |
1032/8=129 |
1976/8=247 |
Следовательно, дробь 1032/1976 может быть сокращена до 129/247. Напишем функцию reduce, сокращающую рациональное число. Ее псевдокод:
{
// шаг 1 — выяснение, что больше — числитель или знаменатель
210
