Здравствуйте прошла курсы на тему Алгоритмы С++. Но не пришел сертификат и не доступен.Где и как можно его скаачат? |
Таблицы символов и деревья бинарного поиска
Деревья бинарного поиска
Для преодоления проблемы слишком высоких затрат на вставку в качестве основы для реализации таблицы символов мы будем использовать явную древовидную структуру. Такая структура данных позволяет разрабатывать алгоритмы с высокой средней производительностью операций найти, вставить, выбрать и сортировать. Этот метод рекомендуется для многих приложений и в компьютерных науках считается одним из наиболее фундаментальных.
Мы уже рассматривали деревья в "Рекурсия и деревья" , а сейчас просто вспомним терминологию. Определяющее свойство дерева (tree) заключается в том, что на каждый узел указывает только один другой узел, называемый родительским (parent). Определяющее свойство бинарного дерева (binary tree) - наличие у каждого узла обязательно двух ссылок, называемых левой и правой. Ссылки могут указывать на другие двоичные деревья или на внешние (external) узлы, которые не имеют ссылок. Узлы с двумя ссылками называются также внутренними (internal) узлами. Для выполнения поиска каждый внутренний узел содержит элемент со значением ключа; ссылки на внешние узлы называются пустыми (null) ссылками (то есть внешние узлы - это фиктивные узлы, которых на самом деле нет - прим. перев.). Процесс поиска зависит от результатов сравнения ключа поиска с ключами во внутренних узлах.
Определение 12.2. Дерево бинарного поиска (binary search tree - BST) - это бинарное дерево, с каждым из внутренних узлов которого связан ключ, причем ключ в любом узле больше или равен ключам во всех узлах левого поддерева этого узла и меньше или равен ключам во всех узлах правого поддерева этого узла.
В программе 12.8 BST-деревья используются для реализации операций найти, вставить, создать и подсчитать. В ней узлы в BST-дереве определяются как содержащие элемент (с ключом) и левую и правую ссылки. Левая ссылка указывает на BST-дерево с элементами с меньшими (или равными) ключами, а правая - на BST-дерево с элементами с большими (или равными) ключами.
При наличии этой структуры рекурсивный алгоритм поиска ключа в BST-дереве становится очевидным: если дерево пусто, поиск неудачен; если ключ поиска равен ключу в корне, поиск успешен. Иначе выполняется (рекурсивно) поиск в соответствующем поддереве. В программе 12.8 этот алгоритм непосредственно реализуется функцией searchR. Начиная с корня дерева и искомого ключа, мы вызываем рекурсивную функцию, которая принимает дерево в качестве первого параметра и ключ в качестве второго. На каждом шаге гарантируется, что никакие части дерева, кроме текущего поддерева, не могут содержать элементы с искомым ключом. Подобно тому, как в бинарном поиске при каждой итерации размер интервала уменьшается чуть более чем в два раза, текущее поддерево в дереве бинарного поиска также меньше предшествующего (в идеальном случае приблизительно вдвое). Процедура завершается либо когда будет найден элемент с искомым ключом (успешный поиск), либо когда текущее поддерево станет пустым (неудачный поиск).
Пример процесса поиска показан на диаграмме в верхней части рис. 12.4. Начиная сверху, процедура поиска в каждом узле приводит к рекурсивному вызову для одного из дочерних узлов этого узла; таким образом, поиск определяет некоторый путь по дереву. При успешном поиске путь завершается в узле, содержащем ключ, а в случае неудачи путь завершается во внешнем узле, как показано на средней диаграмме рис. 12.4.
Программа 12.8. Таблица символов на основе дерева бинарного поиска
В этой реализации функции search и insert используют приватные рекурсивные функции searchR и insertR, которые непосредственно отражают рекурсивное определение BST-деревьев. Обратите внимание на передачу аргумента по ссылке в функции insertR (см. текст). Ссылка head указывает на корень дерева.
template <class Item, class Key> class ST { private: struct node { Item item; node *l, *r; node(Item x) { item = x; l = 0; r = 0; } }; typedef node *link; link head; Item nullItem; Item searchR(link h, Key v) { if (h == 0) return nullItem; Key t = h->item.key(); if (v == t) return h->item; if (v < t) return searchR(h->l, v); else return searchR(h->r, v); } void insertR(link& h, Item x) { if (h == 0) { h = new node(x); return; } if (x.key() < h->item.key()) insertR(h->l, x); else insertR(h->r, x); } public: ST(int maxN) { head = 0; } Item search(Key v) { return searchR(head, v); } void insert(Item x) { insertR(head, x); } };
Для представления внешних узлов в программе 12.8 используются нулевые ссылки, а приватный член данных head указывает на корень дерева. Для создания пустого BST-дерева в head заносится нулевое значение. Можно также использовать фиктивный узел в корне и еще один для представления всех внешних узлов, как описано в различных вариантах для связных списков в таблица 3.1 (см. упражнение 12.53).
Поиск в программе 12.8 выполняется так же просто, как и обычный бинарный поиск; существенная особенность BST-деревьев заключается в том, что операцию вставить реализовать так же легко, как и операцию найти. Логика рекурсивной функции insertR, вставляющей новый элемент в BST-дерево, аналогична логике функции searchR: если дерево пусто, в h заносится ссылка на новый узел, содержащий этот элемент; если ключ поиска меньше ключа в корне, то элемент вставляется в левое поддерево, иначе элемент вставляется в правое поддерево. То есть аргумент, передаваемый по ссылке, изменяется лишь в последнем рекурсивном вызове, при вставке нового элемента. В разделе 12.8 и в "Сбалансированные деревья" будут рассмотрены более сложные древовидные структуры, которые естественным образом представляются с помощью этой же рекурсивной схемы, но которые чаще изменяют значение аргумента.
В процессе успешного поиска H в этом дереве (вверху) мы перемещаемся от корня вправо (поскольку H больше, чем A), затем влево в правом поддереве (поскольку H меньше S) и т.д., продолжая перемещаться вниз по дереву, пока не встретится H. В процессе неудачного поиска M (в центре) мы перемещаемся от корня вправо (поскольку M больше A), затем влево в правом поддереве корня (поскольку M меньше S) и т.д., продолжая перемещаться вниз по дереву, пока не встретится внешняя ссылка (левая ссылка узла N) в нижней части диаграммы. Для вставки M после неудачного поиска достаточно просто заменить ссылку, прервавшую поиск, указателем на M (внизу).
На рис. 12.5 и рис. 12.6 продемонстрировано создание BST-дерева с помощью вставок последовательности ключей в первоначально пустое дерево. Новые узлы присоединяются к пустым ссылкам в нижней части дерева, а в остальном структура дерева никак не изменяется. Поскольку каждый узел имеет две ссылки, дерево растет скорее в ширину, нежели в высоту.
При использовании BST-деревьев реализовать операцию сортировать совсем нетрудно. Построение BST-дерева эквивалентно сортировке элементов, поскольку при соответствующем обходе BST-дерево представляет собой отсортированный файл. На приводимых рисунках ключи упорядочены, если просматривать их слева направо (не обращая внимания на их высоту и ссылки). Программа работает только со ссылками, но простой поперечный обход дерева, по определению, обеспечивает выполнение этой задачи, что и демонстрирует рекурсивная реализация функции showR в программе 12.9. Для отображения элементов BST-дерева в порядке возрастания их ключей нужно отобразить левое поддерево в порядке возрастания его ключей (рекурсивно), затем корень, и затем правое поддерево в порядке возрастания его ключей (рекурсивно).
Программа 12.9. Сортировка с помощью BST-дерева
При поперечном обходе BST-дерева элементы посещаются в порядке возрастания их ключей. В этой реализации для вывода элементов в порядке возрастания их ключей используется функция-член show.
private: void showR(link h, ostream& os) { if (h == 0) return; showR(h->l, os); h->item.show(os); showR(h->r, os); } public: void show(ostream& os) { showR(head, os); }
Эта последовательность демонстрирует результат вставки ключей A S E R C H I N в первоначально пустое BST-дерево. Каждая вставка следует за неудачным поиском в нижней части дерева.
Эта последовательность демонстрирует вставку ключей G X M P L в BST-дерево, создание которого было начато на рис. 12.5.
При необходимости последовательного посещения всех элементов таблицы символов мы будем обращаться к обобщенной операции посетить для таблиц символов. Элементы BST-дерева можно посетить в порядке возрастания их ключей, заменив в только что приведенном описании слово " вывести " на " посетить " и, возможно, обеспечив передачу в качестве параметра функции, выполняющей посещение элемента (см. "Рекурсия и деревья" ).
Несомненно, представляет интерес и нерекурсивный подход к реализации поиска и вставки в BST-деревьях. При нерекусивной реализации процесс поиска состоит из цикла, в котором искомый ключ сравнивается с ключом в корне, затем выполняется перемещение влево, если ключ поиска меньше, и вправо - если он больше ключа в корне. Вставка состоит из индикации неудачного поиска (завершающегося на пустой ссылке) и последующей замены пустой ссылки указателем на новый узел. Этот процесс соответствует явной работе со ссылками вдоль пути вниз по дереву (см. рис. 12.4). В частности, чтобы иметь возможность вставить новый узел в нижней части дерева, необходимо сохранять ссылку на родителя текущего узла, как в реализации в программе 12.10. Как обычно, рекурсивная и нерекурсивная версии, по существу, эквивалентны, но изучение обоих подходов способствует нашему лучшему пониманию алгоритмов и структур данных.
В функциях BST-дерева в программе 12.8 нет явных проверок на наличие элементов с повторяющимися ключами. При вставке нового узла, ключ которого равен какому-либо ключу, уже вставленному в дерево, узел помещается справа от присутствующего в дереве узла. Одним из побочных эффектов подобного соглашения является то, что узлы с равными ключами не являются соседями в дереве (см. рис. 12.7). Однако их можно найти, продолжив поиск с точки, в которой функция search находит первое совпадение, пока не встретится пустая ссылка. Как было сказано в "Очереди с приоритетами и пирамидальная сортировка" , существуют и другие возможности обработки элементов с одинаковыми ключами.
Деревья бинарного поиска - аналог быстрой сортировки. Узел в корне дерева соответствует центральному элементу при быстрой сортировке (ключи слева от него не могут быть больше, а ключи справа не могут быть меньше его). В разделе 12.6 будет показано, как это наблюдение связано с анализом свойств деревьев.
Программа 12.10. Вставка в BST-дерево (нерекурсивная)
Вставка элемента в BST-дерево эквивалентна выполнению неудачного поиска этого элемента с последующим присоединением нового узла с этим элементом вместо пустой ссылки в месте завершения поиска. Присоединение нового узла требует запоминания родительского узла p текущего узла q при перемещении вниз по дереву. При достижении нижней части дерева p указывает на узел, ссылка которого должна указывать на новый вставленный узел.
void insert(Item x) { Key v = x.key(); if (head == 0) { head = new node(x); return; } link p = head; for (link q = p; q != 0; p = q ? q : p) q = (v < q->item.key()) ? q->l : q->r; if (v < p->item.key()) p->l = new node(x); else p->r = new node(x); }
Если BST-дерево содержит записи с одинаковыми ключами (вверху), они оказываются разбросанными по дереву - это видно на примере узлов A. Все одинаковые ключи размещаются вдоль пути поиска ключа от корня до внешнего узла, поэтому они легко доступны. Однако во избежание путаницы при использовании, наподобие " A, который под C, а не под E " , мы используем в примерах различные ключи (внизу).
Упражнения
12.46. Нарисуйте BST-дерево, образованное вставками элементов с ключами E A S Y Q U T I O N в первоначально пустое дерево.
12.47. Нарисуйте BST-дерево, образованное вставками элементов с ключами E A S Y Q U E S T I O N в первоначально пустое дерево.
12.48. Приведите количество сравнений, необходимых для помещения ключей E A S Y Q U E S T
I O N в первоначально пустую таблицу символов на основе BST-дерева. Считайте, что для каждого ключа выполняется операция найти, и затем, если поиск неудачен, операция вставить, как в программе 12.3.
12.49. Вставка ключей A S E R H I N G C в первоначально пустое дерево также дает дерево, показанное вверху рис. 12.6. Приведите десять других вариантов порядка этих ключей, которые дадут тот же результат.
12.50. Реализуйте функцию searchinsert для BST-деревьев (программа 12.8). Она должна искать в таблице символов элемент с таким же ключом, как и у данного элемента, а затем вставлять элемент, если такой ключ не найден.
12.51. Напишите функцию, которая возвращает количество элементов в BST-дереве с ключом, равным данному.
12.52. Предположим, что заранее известна частота обращения к ключам поиска в бинарном дереве.
Должны ли ключи вставляться в дерево в порядке возрастания или убывания ожидаемой частоты обращения к ним? Обоснуйте свой ответ.
12.53. Упростите код поиска и вставки в реализации BST-дерева в программе 12.8 с помощью двух фиктивных узлов: узла head, содержащего элемент с сигнальным ключом, который меньше всех остальных ключей, и правая ссылка которого указывает на корень дерева; и узла z, содержащего элемент с сигнальным ключом, который больше всех остальных ключей, и обе ссылки которого указывает на него самого, причем он представляет все внешние узлы (внешние узлы являются ссылками на z). (См. таблица 3.1).
12.54. Измените реализацию BST-дерева в программе 12.8 для хранения элементов с равными ключами в связных списках, размещенных в узлах дерева. Измените интерфейс, чтобы операция найти работала подобно операции сортировать (для всех элементов с искомым ключом).
12.55. В нерекурсивной процедуре вставки, приведенной в программе 12.10, для определения того, какую ссылку узла p необходимо заменить новым узлом, используется лишнее сравнение. Приведите реализацию, в которой это сравнение исключено.