Опубликован: 05.01.2015 | Доступ: свободный | Студентов: 2023 / 0 | Длительность: 63:16:00
Лекция 13:

Сбалансированные деревья

Характеристики производительности

Как для конкретного приложения осуществить выбор между рандомизированными BST-деревьями, скошенными BST-деревьями, RB-деревьями бинарного поиска и слоеными списками? До сих пор наше внимание было сосредоточено на различной природе гарантирования производительности, которую обеспечивают эти алгоритмы. Главными определяющими факторами всегда являются время и память, но необходимо учитывать и ряд других аспектов. В этом разделе мы кратко рассмотрим вопросы реализации, эмпирические исследования, оценки времени выполнения и требования к памяти.

Все алгоритмы на основе деревьев используют ротации; реализация ротаций на пути поиска — важная составная часть большинства алгоритмов для сбалансированных деревьев. Мы использовали рекурсивные реализации, в которых ссылки на узлы на пути поиска неявно сохраняются в локальных переменных в стеке рекурсии. Но каждый из этих алгоритмов может быть реализован и нерекурсивно, работая с постоянным количеством узлов и выполняя постоянное количество операций над ссылками в перерасчете на один узел во время нисходящего прохода по дереву.

Из трех основанных на деревьях алгоритмов проще всего реализовать рандомизированные BST-деревья. Здесь главные требования — надежность генератора случайных чисел и не слишком большие затраты времени на генерацию случайных битов. Скошенные деревья несколько более сложны, но являются очевидным обобщением стандартного алгоритма вставки в корень. RB-деревья бинарного поиска требуют еще немного большего кодирования, поскольку в них нужно проверять и изменять биты цвета. Одно из преимуществ RB-деревьев по сравнению с двумя другими алгоритмами — возможность использования битов цвета для проверки логики при отладке и для обеспечения быстрого поиска в любой момент времени на протяжении жизни дерева. Рассматривая скошенное BST-дерево, невозможно выяснить, все ли необходимые преобразования выполнил создавший его код; программная ошибка может приводить (только!) к проблемам, связанным с производительностью. Аналогично, ошибка в генераторе случайных чисел, используемом для рандомизированных BST-деревьев или слоеных списков, может привести к не замеченным в противном случае проблемам производительности.

Слоеные списки легко реализовать, и они особенно привлекательны, если требуется поддерживать полный набор операций для таблиц символов, поскольку для них можно легко сформулировать и естественно реализовать все операции — найти, вставить, удалить, объединить, выбрать и сортировать. Внутренний цикл для выполнения поиска в слоеных списках длиннее, чем в деревьях (для него требуется дополнительный индекс в массиве ссылок или дополнительный рекурсивный вызов для спуска на уровень ниже), поэтому время поиска и вставки несколько увеличено. Кроме того, слоеные списки ставят программиста в зависимость от генератора случайных чисел, поскольку отладка программы, поведение которой носит случайный характер — весьма сложная задача, и некоторые программисты очень не любят работать с узлами, содержащими случайное количество ссылок.

В таблица 13.1 приведены экспериментальные данные по производительности четырех рассмотренных в этой главе методов, а также элементарных реализаций BST-деревьев, описанных в "Таблицы символов и деревья бинарного поиска" , для 32-разрядных целых ключей. Приведенные в этой таблице данные подтверждают аналитические результаты, полученные в разделах 13.2, 13.4 и 13.5. RB-деревья работают со случайными ключами гораздо быстрее, чем другие алгоритмы. Пути в них на 35% короче, чем в рандомизированных или скошенных BST-деревьях, а в их внутренних циклах выполняется меньше действий. Рандомизированные деревья и слоеные списки требуют генерации по меньшей мере одного случайного числа для каждой вставки, а скошенные BST-деревья выполняют ротацию в каждом узле для каждой вставки и каждого поиска. Дополнительные затраты при использовании RB-деревьев бинарного поиска заключаются в проверке значений двух битов в каждом узле во время вставки, а иногда приходится выполнять и ротацию. При неравномерном доступе скошенные BST-деревья могут обеспечить более короткие пути, но эта экономия, скорее всего, будет перекрыта тем, что и для поиска, и для вставки потребуются ротации в каждом узле во внутреннем цикле, за исключением, быть может, крайних случаев.

Таблица 13.1. Экспериментальное сравнение реализаций сбалансированных деревьев
N Построение Неудачные поиски
B T R S C L B T R S C L
1250 0 1 3 2 1 2 1 1 0 0 0 2
2500 2 4 6 3 1 4 1 1 1 2 1 3
5000 4 7 14 8 5 10 3 3 3 3 2 7
12500 11 23 43 24 16 28 10 9 9 9 7 18
25000 27 51 101 50 32 57 19 19 26 21 16 43
50000 63 114 220 117 74 133 48 49 60 46 36 98
100000 159 277 447 282 177 310 118 106 132 112 84 229
200000 347 621 996 636 411 670 235 234 294 247 193 523
Обозначения:
B Стандартное BST-дерево (программа 12.8)
T BST-дерево, построенное вставками в корень (программа 12.13)
R Рандомизированное BST-дерево (программа 13.2)
S Скошенное BST-дерево (упражнение 13.33 и программа 13.5)
C RB-дерево бинарного поиска (программа 13.6)
L Слоеный список (программы 13.7 и 13.9)

Эти сравнительные значения времени построения и поиска в BST-деревьях, образованных из случайных последовательностей N 32-разрядных чисел, при различных значениях N, показывают, что все методы обеспечивают хорошую производительность даже для очень больших таблиц, но RB-деревья работают значительно быстрее других методов. Во всех методах используется стандартный поиск в BST-дереве, за исключением скошенных BST-деревьев, где при поиске выполняется скос для перемещения часто посещаемых узлов ближе к вершине, и слоеных списков, в которых используется, по сути дела, этот же алгоритм, но по отношению к другой структуре данных.

Скошенные BST-деревья не требуют использования дополнительной памяти под информацию о балансе; RB-деревья бинарного поиска требуют 1 дополнительный бит, а рандомизированные BST-деревья требуют наличия поля счетчика. Во многих приложениях поле счетчика используется и для других целей, поэтому для рандомизированных BST-деревьев оно может и не вызывать дополнительных затрат. На самом деле добавление этого поля может потребоваться и при использовании скошенных BST-деревьев, RB-деревьев бинарного поиска или слоеных списков. При необходимости RB-деревья бинарного поиска можно сделать столь же эффективными по памяти, как и скошенные BST-деревья, исключив бит цвета (см. упражнение 13.65). В современных приложениях объем памяти не столь важен, как когда-то, однако аккуратный программист всегда избегает напрасных затрат. Например, необходимо помнить, что некоторые системы для небольшого поля счетчика или 1-разрядного поля цвета в узле могут использовать целое 32-разрядное слово, а некоторые другие системы могут упаковывать поля в памяти так, что их распаковка требует значительного дополнительного времени. Если объем памяти ограничен, слоеные списки с большим параметром t могут уменьшить объем памяти, требуемый для ссылок, почти в два раза — ценой более медленного (но все же логарифмического) поиска. Некоторые приемы позволяют реализовать основанные на деревьях методы с использованием лишь одной ссылки на узел (см. упражнение 12.68).

Подводя итоги, можно сказать, что все рассмотренные в этой главе методы обеспечивают высокую производительность типичных приложений, при этом каждый метод обладает собственными достоинствами для тех, кто заинтересован в разработке высокопроизводительных реализаций таблиц символов. Скошенные BST-деревья обеспечивают высокую производительность как метод самоорганизующегося поиска, особенно когда типичны частые обращения к небольшому набору ключей. Рандомизированные BST-деревья обычно работают быстрее, и с их помощью легче реализовать полнофункциональные таблицы символов. Слоеные списки просты для понимания и могут обеспечить логарифмическую зависимость времени поиска при меньших по сравнению с другими методами затратах памяти. А RB-деревья бинарного поиска привлекательны для библиотечных реализаций таблицы символов, поскольку они обеспечивают гарантированные границы производительности в худшем случае и наиболее быстрые алгоритмы поиска и вставки случайных данных.

Кроме специфичных использований в приложениях, это множество решений задачи разработки эффективных реализаций АТД таблицы символов важно также и потому, что оно иллюстрирует фундаментальные подходы к разработке алгоритмов, которые можно использовать и для решения других задач. При постоянной потребности в простых оптимальных алгоритмах мы часто сталкиваемся с почти оптимальными алгоритмами, подобными рассмотренным в этой главе. Вообще-то, как можно было заметить на примере сортировки, алгоритмы, основанные на сравнениях — начало, но далеко не конец. Переходя к абстракциям более низкого уровня, в которых возможна обработка фрагментов ключей, можно разрабатывать реализации, которые работают даже быстрее, чем рассмотренные в этой главе, что и будет продемонстрировано в "Хеширование" и "Поразрядный поиск" .

Упражнения

13.85. Разработайте реализацию таблицы символов, использующую рандомизированные BST-деревья, которая содержит деструктор, конструктор копирования и перегруженную операцию присваивания и поддерживает операции создать, подсчитать, найти, вставить, удалить, объединить, выбрать и сортировать для АТД таблицы символов первого класса с поддержкой клиентских дескрипторов элементов (см. упражнения 12.6 и 12.7).

13.86. Разработайте реализацию таблицы символов, использующую слоеные списки, которая содержит деструктор, конструктор копирования и перегруженную операцию присваивания и поддерживает операции создать, подсчитать, найти, вставить, удалить, объединить, выбрать и сортировать для АТД таблицы символов первого класса с поддержкой клиентских дескрипторов элементов (см. упражнения 12.6 и 12.7).

Никита Андриянов
Никита Андриянов
Дмитрий Уколов
Дмитрий Уколов
Роман Коваленко
Роман Коваленко
Украина, Херсон, Херсонский Государственный Морской Институт, 2009