Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы программирования. Борисенко

.pdf
Скачиваний:
1533
Добавлен:
09.04.2015
Размер:
9.31 Mб
Скачать

4.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. Таким образом, для выполнения преоб-