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

Таблицы символов и деревья бинарного поиска

Деревья бинарного поиска

Для преодоления проблемы слишком высоких затрат на вставку в качестве основы для реализации таблицы символов мы будем использовать явную древовидную структуру. Такая структура данных позволяет разрабатывать алгоритмы с высокой средней производительностью операций найти, вставить, выбрать и сортировать. Этот метод рекомендуется для многих приложений и в компьютерных науках считается одним из наиболее фундаментальных.

Мы уже рассматривали деревья в "Рекурсия и деревья" , а сейчас просто вспомним терминологию. Определяющее свойство дерева (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 и в "Сбалансированные деревья" будут рассмотрены более сложные древовидные структуры, которые естественным образом представляются с помощью этой же рекурсивной схемы, но которые чаще изменяют значение аргумента.

 Поиск и вставка в дереве бинарного поиска

Рис. 12.4. Поиск и вставка в дереве бинарного поиска

В процессе успешного поиска 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); }
      
 Создание дерева бинарного поиска

Рис. 12.5. Создание дерева бинарного поиска

Эта последовательность демонстрирует результат вставки ключей A S E R C H I N в первоначально пустое BST-дерево. Каждая вставка следует за неудачным поиском в нижней части дерева.

 Создание дерева бинарного поиска (продолжение)

Рис. 12.6. Создание дерева бинарного поиска (продолжение)

Эта последовательность демонстрирует вставку ключей 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);
  }
      
 Повторяющиеся ключи в деревьях бинарного поиска

Рис. 12.7. Повторяющиеся ключи в деревьях бинарного поиска

Если 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 необходимо заменить новым узлом, используется лишнее сравнение. Приведите реализацию, в которой это сравнение исключено.

Бактыгуль Асаинова
Бактыгуль Асаинова

Здравствуйте прошла курсы на тему Алгоритмы С++. Но не пришел сертификат и не доступен.Где и как можно его скаачат?

Александра Боброва
Александра Боброва

Я прошла все лекции на 100%.

Но в https://www.intuit.ru/intuituser/study/diplomas ничего нет.

Что делать? Как получить сертификат?