Основы программирования. Борисенко
.pdf4.6 Множество |
291 |
конец алгоритма
4.6. Множество
Множество — это структура данных, содержащая конечный на¬ |
||
бор элементов некоторого типа. Каждый элемент содержится толь¬ |
||
ко в одном экземпляре, т.е. разные элементы множества не равны |
||
между собой. Элементы множества никак не упорядочены. В множе |
||
ство M можно добавить элемент ж, из множества M можно удалить |
||
элемент ж. Если |
при добавлении элемента ж он у ж е |
содержится в |
множестве M , то |
ничего не происходит. Аналогично, |
никакие дей¬ |
ствия не совершаются при удалении элемента ж, |
когда он |
не содер¬ |
ж и т с я в множестве M . Наконец, для заданного |
элемента |
ж можно |
определить, содержится ли он в множестве M . Множество |
— это по¬ |
тенциально неограниченная структура, оно может содержать любое конечное число элементов.
В некоторых языках программирования накладывают ограниче¬ ния на тип элементов и на максимальное количество элементов мно¬ жества. Так, иногда рассматривают множество элементов дискрет¬ ного типа, число элементов которого не может превышать некото¬ рой константы, задаваемой при создании множества. (Тип называет¬ ся дискретным, если все возможные значения данного типа можно занумеровать целыми числами.) Д л я таких множеств употребляют название Bitset ("набор битов") или просто Set. Как правило, для реализации таких множеств используется битовая реализация мно¬ жества на базе массива целых чисел. Каждое целое число рассмат¬ ривается в двоичном представлении как набор битов, содержащий 32 элемента. Биты внутри одного числа нумеруются справа налево (от младших разрядов к старшим); нумерация битов продолжается от одного числа к другому, когда мы перебираем элементы массива. К примеру, массив из десяти целых чисел содержит 320 битов, номера
которых изменяются от 0 до 319. Множество в данной |
реализации |
||
может содержать любой набор целых чисел |
в диапазоне от 0 до |
319. |
|
Число N содержится в множестве тогда и |
только тогда, |
когда |
бит |
с номером N равен единице (программисты говорят «бит установ¬ |
|||
лен»). Соответственно, если число N не содержится в множестве, то |
|||
бит с номером N равен нулю (программисты |
говорят «бит очищен»). |
292 |
|
4.6. Множество |
||
Пусть, например, множество |
содержит элементы 0, 1, 5, 34. Тогда |
|||
в |
первом |
элементе массива |
установлены биты с номерами |
0, 1, 5, |
во |
втором |
— бит с номером |
2 = 34 - 32. Соответственно, |
двоичное |
представление первого элемента массива равно 10011 (биты нумеру¬ ются справа налево), второго — 100, это числа 19 и 4 в десятичном представлении. Все остальные элементы массива нулевые.
Хотя в языке программирования Паскаль слово Set, в перево¬ де «множество», закреплено за ограниченным множеством элемен¬ тов дискретного типа, такими множествами далеко не исчерпывают¬ ся потребности программирования. Например, множество точек на
плоскости или множество |
текстовых |
строк не являются таковыми. |
Д л я множеств общего вида |
битовая |
реализация не подходит. Н и ж е |
будут рассмотрены несколько других реализаций, используемых для любых множеств.
В |
программировании довольно часто |
рассматривают |
структуру |
чуть |
более сложную, чем просто множество: нагруженное |
множе¬ |
|
ство. |
Пусть к а ж д ы й элемент множества |
содержится в нем вместе с |
дополнительной информацией, которую называют нагрузкой элемен¬ та. При добавлении элемента в множество нужно также указывать нагрузку, которую он несет. В разных языках программирования и в различных стандартных библиотеках такие структуры называют «Отображением» (Map) или «Словарем» (Dictionary). Действитель¬ но, элементы множества как бы отображаются на нагрузку, которую они несут (заметим, что в математике понятие функции или отоб¬ ражения определяется строго как множество пар; первым элементом каждой пары является конкретное значение аргумента функции, вто¬ рым — значение, на которое функция отображает аргумент). В ин¬ терпретации «Словаря» элемент множества — это иностранное слово, нагрузка элемента — это перевод слова на русский язык (разумеет¬ ся, перевод может включать несколько вариантов, но здесь перевод рассматривается как единый текст).
Подчеркнем еще раз, что все элементы содержатся в нагружен¬ ном множестве в одном экземпляре, т.е. разные элементы множества
не могут |
быть равны друг другу. В отличие от самих элементов, их |
|
нагрузки |
могут совпадать (так, различные иностранные слова могут |
|
иметь одинаковый перевод). Поэтому иногда |
элементы нагруженного |
|
множества называют ключами, их нагрузки |
— значениями ключей. |
294 4. 6. Множество
элемента, которая отвечает на вопрос, принадлежит ли элемент x множеству, и, если принадлежит, выдает индекс ячейки массива, со¬ держащей x. Та ж е процедура поиска используется и при удалении элемента из множества. При добавлении элемента он дописывается в конец (в ячейку массива с индексом «число элементов») и пере менная «число элементов» увеличивается на единицу. Д л я удаления элемента достаточно последний элемент множества переписать на место удаляемого и уменьшить переменную «число элементов» на
единицу. |
|
|
|
|
|
В «наивной» реализации |
элементы |
множества хранятся |
в масси¬ |
ве |
в произвольном порядке. Это означает, что при поиске элемента |
|||
x |
придется последовательно |
перебрать |
все элементы, пока |
мы ли¬ |
бо не найдем x, либо не убедимся, что его там нет. Пусть мно¬ жество содержит 1,000,000 элементов. Тогда, если x содержится в множестве, придется просмотреть в среднем 500,000 элементов, если нет — все элементы. Поэтому «наивная» реализация годится только для небольших множеств.
4.6.2.Бинарный поиск
Пусть можно сравнивать элементы множества друг с другом, определяя, какой из них больше. (Например, для текстовых строк применяется лексикографическое сравнение: первые буквы сравни¬ ваются по алфавиту; если они равны, то сравниваются вторые буквы и т.д.) Тогда можно существенно убыстрить поиск, применяя алго¬ ритм бинарного поиска. Д л я этого элементы множества хранятся в массиве в возрастающем порядке. Идея бинарного поиска иллюстри¬ руется следующей шуточной задачей: «Как поймать льва в пустыне? Надо разделить пустыню забором пополам, затем ту половину, в ко¬ торой находится лев, снова разделить пополам и так далее, пока лев не окажется пойманным».
В алгоритме бинарного поиска мы на каждом шагу делим отрезок массива, в котором может находиться искомый элемент x, пополам. Рассматриваем элемент у в середине отрезка. Если x меньше у, то выбираем левую половину отрезка, если больше, то правую. Таким образом, на каждом шаге размер отрезка массива, в котором может находиться элемент x, уменьшается в два раза. Поиск заканчивается, когда размер отрезка массива (т.е. расстояние между его правым и
4.6.2. Бинарный поиск |
295 |
левым концами) становится равным единице, т.е. через [log2 п]+1 ша гов, где п — размер массива. В нашем примере это произойдет после 20 шагов (т.к. log 2 1000000 < 20). Таким образом, вместо миллиона операций сравнения при последовательном поиска нужно выполнить всего лишь 20 операций при бинарном.
Запишем алгоритм бинарного поиска на псевдокоде. Д а н упоря доченный массив a вещественных чисел (вещественные числа ис¬ пользуются для определенности; бинарный поиск можно применять, если на элементах множества определен линейный порядок, т.е. дл я любых двух элементов можно проверить их равенство или опреде¬
лить, какой их них больше). Пусть текущее |
число элементов |
равно |
n. Элементы массива упорядочены по возрастанию: |
|
|
а[0] < а[1] < • • • < а[п - |
1]. |
|
М ы ищем элемент x. Требуется определить, |
содержится ли x |
в мас¬ |
сиве. Если элемент x содержится в массиве, то надо определить ин¬ декс i ячейки массива, содержащей x:
найти i : a[i] = x. |
|
|
Если ж е x не содержится в массиве, |
то надо определить |
индекс i , |
такой, что при добавлении элемента x |
в i - ю ячейку массива |
элементы |
массива останутся упорядоченными, т.е.
найти i : a[i — 1] < x < a[i]
(считается, что a[—1] = —оо, a[n] = +оо). Объединив оба случая, получим неравенство
a[i — 1] < x < a[i].
Используем схему построения цикла с помощью инварианта, см. раздел 1.5.2. В процессе выполнения хранятся два индекса b и e (от слов begin и end), такие, что
a[b] < x < a[e].
Индексы b и e ограничивают текущий отрезок массива, в котором осуществляется поиск. Приведенное неравенство является инвари антом цикла. Перед началом выполнения цикла рассматриваются
296 |
4.6. Множество |
разные исключительные |
случаи — когда массив пустой (n = 0), ко¬ |
гда x не превышает минимального элемента массива (x < a[0]) и
когда |
x |
больше максимального элемента (x > |
a[n — 1]). В |
общем |
||||
случае |
выполняется |
неравенство |
|
|
|
|||
|
|
|
|
a[0] |
< x < a[n — 1]. |
|
|
|
Полагая |
b = 0, e = |
n — 1, |
мы обеспечиваем |
выполнение |
инвари¬ |
|||
анта цикла перед началом выполнения цикла. Условие |
завершения |
|||||||
состоит |
в |
том, что длина участка массива, внутри которого |
может |
|||||
находится |
элемент x, равна |
единице: |
|
|
|
|||
|
|
|
|
|
b - e = 1. |
|
|
|
В этом |
случае |
|
|
|
|
|
||
|
|
|
|
a[e — 1] < x < a[e], |
|
|
|
т.е. искомый индекс i равен e, а элемент x содержится в массиве
тогда и только тогда, когда x = |
a[e]. В цикле мы вычисляем |
середину |
||||||
c |
отрезка [b, e] |
|
|
|
|
|
|
|
|
|
|
c = |
целая |
часть ((b + e)/2) |
|
||
и |
выбираем |
ту |
из двух |
половин [b, c] или |
[c, e], которая |
содержит |
||
x. |
Д л я этого достаточно |
сравнить x со значением a[c] в середине |
||||||
отрезка. Завершение цикла обеспечивается |
тем, что величина e — b |
|||||||
монотонно убывает после каждой итерации |
цикла. |
|
||||||
|
Приведем текст алгоритма бинарного поиска: |
|
||||||
|
лог алгоритм бинарный_поиск( |
|
|
|||||
|
вход: цел n, вещ a[n], вещ x, выход: цел i |
|
||||||
|
) |
|
число |
элементов в массиве a, |
|
|||
|
| Дано: n |
|
||||||
|
| |
a[0] < a[1] < ... < a[n-1] |
|
|
||||
|
| Надо: |
ответ := |
(x содержится в массиве), |
|
||||
|
| |
вычислить |
i , такое, что |
|
|
|||
|
| |
|
a[i-1] |
< x <= a[i] |
|
|
||
|
| |
(считая a[-1] = -беск., a[n] = +беск.) |
|
|||||
|
начало |
|
|
|
|
|
|
|
| цел b, e, c;
|
4.6.3. Реализации множества на базе |
деревьев |
297 |
|||||||||
| |
// Рассматриваем сначала исключительные случаи |
||||||||||
| |
если |
(n == |
0) |
|
|
|
|
|
|||
| |
| то |
i = |
0; |
ложь; |
|
|
|
|
|||
| |
| ответ |
:= |
a[0]) |
|
|
|
|||||
| иначе |
если |
(x <= |
|
|
|
||||||
| |
| i |
:= |
0; |
(x == |
a[0]); |
|
|
|
|||
| |
| ответ |
:= |
|
|
|
||||||
| иначе |
если |
(x > |
a[n-1]) |
|
|
|
|||||
| |
| i |
:= |
n |
:= |
ложь; |
|
|
|
|
||
| |
| ответ |
|
|
|
|
||||||
| |
иначе |
|
|
случай |
|
|
|
||||
| |
| // |
Общий |
<= |
a[n-1]; |
|||||||
| |
| утверждение: a[0] < x |
||||||||||
| |
| b |
:= |
0; e |
:= |
n-1; |
|
|
|
|||
| | |
|
|
|
|
e - b > 1 |
|
|
|
|||
| |
| цикл пока |
и |
x |
<= a[e]; |
|||||||
| |
| | инвариант: a[b] < x |
||||||||||
| |
| |
| c |
:= |
целая |
ч а с т ь ( ^ |
+ b) / 2); |
|||||
| |
| |
| если |
x |
<= a[c] |
|
|
|
||||
| |
| |
| |
| то |
e |
:= |
c; |
|
|
|
|
|
| |
| |
| иначе |
|
|
|
|
|
|
|||
| |
| |
| |
| |
|
b |
:= |
c; |
|
|
|
|
| |
| |
| конец если |
|
|
|
|
|
||||
| |
| конец |
цикла |
|
|
|
|
|
||||
| | |
|
|
|
|
|
|
|
1 |
и |
|
|
| |
| утверждение: e - b == |
|
|||||||||
| |
| |
|
|
|
|
|
a[b] < x |
<= a[e]; |
|||
| |
| i |
:= |
e; |
(x == |
a [ i ] ) ; |
|
|
|
|||
| |
| ответ |
:= |
|
|
|
||||||
| конец |
если |
|
|
|
|
|
|
||||
конец алгоритма |
|
|
|
|
|
4.6.3.Реализации множества на базе деревьев
Реализация множества с помощью бинарного поиска во всех от¬ ношениях лучше «наивной» реализации. Вместе с тем, она все ж е имеет недостатки: 1) при добавлении и удалении элементов в сере¬ дине массива приходится переписывать элементы в конце массива
298 |
4.6. Множество |
на новое место, чтобы освободить |
место для добавляемого элемента |
либо закрыть образовавшуюся лакуну при удалении элемента; 2) по¬ иск выполняется гарантированно быстро, но все-таки не мгновенно.
От первого |
из этих недостатков можно избавиться, применяя вместо |
||||||
непрерывной |
реализации на базе |
массива ссылочную |
реализацию, |
||||
при |
которой |
элементы |
множества |
содержатся |
в вершинах бинарно¬ |
||
го |
дерева. |
Элементы |
в вершинах |
упорядочены |
таким |
образом, что, |
если зафиксировать некоторую вершину V и рассмотреть два подде¬ рева, соответствующих левому и правому сыновьям вершины, то все элементы в вершинах левого поддерева должны быть меньше, чем элемент в вершине V , а все элементы в вершинах правого поддерева
должны быть больше него. |
|
|
Д л я такого |
дерева |
можно т а к ж е приме¬ |
нять алгоритм бинарного поиска. Мак¬ |
||
симальное |
число |
сравнений при поис¬ |
ке в таком дереве равняется его высоте (т.е. максимальной длине пути от корня
|
к терминальной вешине). |
||
Чтобы |
поиск выполнялся быстро, |
дерево |
должно быть сбалансиро |
ванным, |
т.е. все его ветви должны |
иметь |
почти одинаковую длину. |
Точное определение сбалансированности следующее: будем счи¬ тать, что у каждой вершины, включая терминальные, ровно два сы¬ на, при необходимости добавляя внешние, или нулевые, вершины. Например, у терминальной вершины оба сына нулевые. (Это в точ¬
ности сответствует представлению дерева в |
языке |
Си, |
где к а ж д а я |
вершина хранит два указателя на сыновей; |
если |
сына |
нет, то со¬ |
ответствующий указатель нулевой.) Обычные вершины дерева будем называть собственными. Рассмотрим путь от корня дерева к внешней
(нулевой) вершине. Длиной пути считается количество |
собственных |
вершин в нем. Дерево называется сбалансированным, |
если длины |
всех возможных путей от корня дерева к внешним вершинам разли¬ чаются не более чем на единицу. Иногда в литературе такие дере¬ вья называют почти сбалансированными, понимая под сбалансиро¬ ванностью строгое равенство длин всех путей от корня к внешним узлам; мы, однако, будем придерживаться нестрогого определения.
4.6.3. Реализации множества на базе деревьев |
299 |
Пример сбалансированного дерева представлен на рисунке.
Высота сбалансированного |
дерева |
h оценивается логарифмически |
|
в зависимости от числа вершин n: |
|
|
|
h < |
log |
2 n |
+ 1. |
Поскольку максимальное число сравнений при поиске элемента в упорядоченном бинарном дереве равняется высоте дерева, поиск в сбалансированном дереве осуществляется исключительно быстро, за время, логарифмически зависящее от числа элементов множества. (Можно доказать, что это является теоретической оценкой снизу: никакой алгоритм не может в общем случае находить элемент бы¬ стрее, чем за log 2 n операций.)
Д л я эффективной реализации множества на базе дерева проце¬ дуры добавления и удаления элементов должны сохранять свойство сбалансированности (или почти сбалансированности). Рассмотрим коротко две наиболее популярные схемы реализации.
AVL-деревья
Так называемые AVL-деревья (названные в честь их двух изоб ретателей Г.М. Адельсона-Вельского и Е.М. Ландиса) хранят до¬ полнительно в каждой вершине разность между высотами левого и правого поддеревьев, которая в сбалансированном дереве может при¬ нимать только три значения: —1, 0, 1. Строго говоря, AVL-деревья не являются сбалансированными в смысле приведенного выше опре¬
деления. |
Требуется только, чтобы для любой вершины |
AVL-дерева |
|
разность |
высот ее левого и правого поддеревьев была |
по |
абсолют¬ |
ной величине не больше единицы. При этом длины путей |
от корня |
||
к внешним вершинам могут различаться больше, чем |
на |
единицу. |
300 |
4.6. Множество |
Можно, |
тем не менее, доказать, что и в случае AVL-деревьев их |
высота оценивается сверху логарифмически в зависимости от числа вершин:
h < C log 2 n, |
|
где константа C = 1.5. Обычно константы не очень в а ж н ы в |
практи |
ческом программировании — принципиально лишь, по какому |
закону |
увеличивается время работы алгоритма при увеличении n. В данном случае зависимость логарифмическая, т.е. наилучшая из всех воз¬ можных (поскольку поиск невозможен быстрее чем за log 2 n опера¬ ций).
Новый элемент всегда добавляется в дерево в соответствии с упорядоченностью как левый или правый сын некоторой вершины, у которой данного сына до этого не было (или, как мы считаем, сын являлся внешним). Новая вершина добавляется как терминальная. После этого выполняется процедура восстановления балансировки. В ней используются следующие элементарные преобразования дерева, сохраняющие упорядоченность вершин:
1) вращение вершины x поддерева влево:
Здесь вершина x поддерева, которая является его корнем, опус кается вниз и влево. Бывший правый сын d вершины x стано¬ вится новым корнем поддерева, а x становится левым сыном d. (Вершины x и d, «начальник» и «подчиненный», как бы ме¬ няются ролями: бывший начальник становится подчиненным.) Поддерево с, которое было левым сыном вершины d, перехо¬ дит в подчинение от вершины d к вершине x и становится ее правым сыном. Отметим, что упорядоченность вершин сохраня¬ ется: a<b<c<d<e. Таким образом, для выполнения преоб-