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

Поразрядный поиск

Trie-деревья

В этом разделе мы рассмотрим деревья поиска, которые позволяют использовать разряды ключей для проведения поиска подобно DST-деревьям, но ключи которых упорядочены, что позволяет поддерживать рекурсивные реализации операции сортировать и других операций таблиц символов, как для BST-деревьев. Основная идея заключается в хранении ключей только в нижней части дерева, в листьях. Результирующая структура данных обладает рядом полезных свойств и служит основой для нескольких эффективных алгоритмов поиска. Впервые эта структура была создана Брианде (Briandais) в 1959 г., и поскольку она оказалась удобной для выборки (retrieval), в 1960 г. Фредкин (Fredkin) дал ей специальное название trie. Обычно это слово произносится как " трайи " или " трай " (похоже на try — попытка, англ.), чтобы отличать его от " tree " (дерево). Наверно, в соответствии с принятой в книге терминологией следовало бы ввести термин " trie-деревья бинарного поиска " , но термин trie-дерево повсеместно используется и всем понятен. В этом разделе рассматривается базовая бинарная версия, в разделе 15.3 — ее важная модификация, а в разделах 15.4 и 15.5 — базовая многопутевая версия trie-деревьев и их варианты.

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

В trie-дереве ключи хранятся в листьях бинарного дерева. Вспомните, что было сказано в "Рекурсия и деревья" : лист в дереве — это узел, не имеющий дочерних узлов, что отличает его от внешнего узла, который интерпретируется как пустой дочерний узел. В бинарном дереве под листом понимается внутренний узел с пустыми левой и правой ссылками. Хранение ключей в листьях, а не во внутренних узлах позволяет использовать разряды ключей для управления поиском, как для DST-деревьев в разделе 15.1, сохраняя при этом свойство, что все ключи, текущий разряд которых равен 0, попадают в левое поддерево, а все ключи, текущий разряд которых равен 1 — в правое.

Определение 15.1. Trie-дерево — это бинарное дерево с ключами, связанными с каждым из его листьев, которое рекурсивно определяется следующим образом. Trie-дерево из пустого множества ключей представляет собой пустую ссылку. Trie-дерево из единственного ключа — это лист, содержащий данный ключ. И, наконец, trie-дерево из множества ключей мощностью более 1 — это внутренний узел, левая ссылка которого указывает на trie-дерево с ключами, начинающимися с бита 0, а правая — на trie-дерево с ключами, начинающимися с бита 1, если для построения поддеревьев удалить ведущий бит.

Каждый ключ в trie-дереве хранится в листе, который находится на пути, заданном последовательностью ведущих разрядов ключа. И наоборот, каждый лист в trie-дереве содержит единственный ключ, который начинается с разрядов, определенных путем из корня к этому листу. Пустые ссылки в не листовых узлах соответствуют последовательностям ведущих разрядов, которые не присутствуют ни в одном ключе trie-дерева. Следовательно, для поиска ключа в trie-дереве нужно всего лишь пройти по нему в соответствии с разрядами ключа, как в DST-деревьях, но при этом не нужно выполнять сравнения во внутренних узлах. Поиск начинается с левого разряда ключа и с верхушки дерева и проходит по левой ссылке, если текущий разряд равен 0, и по правой — если 1, перебирая разряды ключа по одному слева направо. Поиск, закончившийся на пустой ссылке, неудачен; поиск, закончившийся в листе, может быть завершен одним сравнения с ключом, поскольку этот узел содержит единственный ключ в дереве, который может быть равен искомому. Реализация этого процесса приведена в программе 15.2.

Для вставки ключа в trie-дерево вначале, как обычно, выполняется поиск. Если поиск завершается на пустой ссылке, она, как обычно, заменяется ссылкой на новый лист, содержащий ключ. Но если поиск заканчивается в листе, необходимо продолжить перемещение вниз по дереву, добавляя внутренний узел для каждого разряда, значение которого совпадает для искомого и найденного ключей; завершится этот процесс тем, что оба ключа в листьях, являющихся дочерними узлами внутреннего узла, будут соответствовать первому разряду, в котором они отличаются. Пример поиска и вставки в trie-дереве показан на рис. 15.6; процесс построения trie-дерева вставками ключей в первоначально пустое дерево представлен на рис. 15.7. Полная реализация алгоритма вставки приведена в программе 15.3.

Программа 15.2. Поиск в trie-дереве

В этой функции разряды ключа используются для управления переходами при перемещении вниз по дереву, так же, как и в программе 15.1 для DST-деревьев. Возможны три варианта: если поиск доходит до листа (с обеими пустыми ссылками), то это единственный узел trie-дерева, который может содержать запись с ключом v. В этом случае выполняется проверка, действительно ли этот узел содержит v (успешный поиск) или какой-то другой ключ, ведущие разряды которого совпадают с v (неудачный поиск). Если поиск доходит до пустой ссылки, то вторая ссылка родительского узла не должна быть пустой и, следовательно, в trie-дереве существует какой-то другой ключ, отличающийся от искомого текущим разрядом, т.е. поиск неудачен. В программе предполагается, что ключи различны и (если ключи могут иметь различную длину) ни один ключ не является префиксом другого ключа. Член item не используется в не листовых узлах.

private:
  Item searchR(link h, Key v, int d)
    { if (h == 0) return nullItem;
      if (h->l == 0 && h->r == 0)
        { Key w = h->item.key();
          return (v == w) ? h->item : nullItem;
        }
      if (digit(v, d) == 0)
        return searchR(h->l, v, d+1);
      else
        return searchR(h->r, v, d+1);
    }
public:
  Item search(Key v)
    { return searchR(head, v, 0); }
      
 Поиск и вставка в trie-дереве

Рис. 15.6. Поиск и вставка в trie-дереве

Ключи в trie-дереве хранятся в листьях (узлах с обеими пустыми ссылками); пустые ссылки в не листовых узлах соответствуют последовательностям разрядов, не найденным ни в одном ключе trie-дерева.

При успешном поиске ключа H = 01000 в этом дереве (вверху) мы переходим из корня влево (поскольку первый бит в двоичном представлении ключа равен 0), затем вправо (поскольку второй бит равен 1), где и обнаруживаем H — единственный ключ в дереве, начинающийся с битов 01. Ни один из присутствующих в дереве ключей не начинается с 101 или 11, и эти последовательности битов приводят в trie-дереве к двум пустым не листовым ссылкам.

Чтобы вставить ключ I (внизу), придется добавить три не листовых узла: один — соответствующий 01, с пустой ссылкой, соответствующей 011; один — соответствующий 010, с пустой ссылкой, соответствующей 0101; и один — соответствующий 0100 с ключом H = 01000 в листе слева от него и с ключом I = 01001 в листе справа.

Программа 15.3. Вставка в trie-дерево

Для вставки нового узла в trie-дерево вначале, как обычно, выполняется поиск. В случае неудачного поиска возможны два варианта.

Если неудачный поиск завершен не в листе, пустая ссылка, на которой закончился поиск, как обычно, заменяется ссылкой на новый узел.

Если неудачный поиск завершен в листе, используется функция split, создающая по одному новому внутреннему узлу для каждой битовой позиции, в которой искомый и найденный ключ совпадают. Этот процесс завершается созданием одного внутреннего узла для самого левого разряда, в котором эти ключи различаются. Оператор switch в функции split преобразует два проверяемых разряда в число для переключения на один из четырех возможных случаев. Если разряды одинаковы (случай 002 = 0 или 112 = 3 ), разбиение продолжается; если разряды различны (случай 012 = 1 или 102 = 2 ), разбиение прекращается.

  private:
    link split(link p, link q, int d)
      { link t = new node(nullItem); t->N = 2;
         Key v = p->item.key(); Key w = q->item.key();
        switch(digit(v, d)*2 + digit(w, d))
          { case 0: t->l = split(p, q, d+1); break;
            case 1: t->l = p; t->r = q; break;
            case 2: t->r = p; t->l = q; break;
            case 3: t->r = split(p, q, d+1); break;
          }
        return t;
      }
    void insertR(link& h, Item x, int d)
      { if (h == 0) { h = new node(x); return; }
        if (h->l == 0 && h->r == 0)
          { h = split(new node(x), h, d); return; }
        if (digit(x.key(), d) == 0)
          insertR(h->l, x, d+1);
        else
          insertR(h->r, x, d+1);
      }
  public:
    ST(int maxN)
      { head = 0; }
    void insert(Item item)
      { insertR(head, item, 0); }
      

Поскольку алгоритм не обращается к пустым ссылкам в листьях и не хранит элементы в не листовых узлах, можно сократить объем используемой памяти с помощью конструкции union или пары производных классов, определив узлы как принадлежащие к одному из этих двух типов (см. упражнения 15.20 и 15.21). Но пока мы пойдем более простым путем, используя единственный тип узлов, который применялся в BST-деревьях, DST-деревьях и других структурах бинарных деревьев: внутренние узлы характеризуются пустыми ключами, а листья — пустыми ссылками; однако мы будем помнить, что при необходимости можно сэкономить память, теряемую из-за этого упрощения. В разделе 15.3 будет рассмотрено усовершенствование алгоритма, исключающее потребность в нескольких типах узлов, а в главе 16 "Внешний поиск" приводится реализация, в которой используется конструкция union. А теперь рассмотрим основные свойства trie-деревьев, вытекающие из определения и приведенных примеров.

 Построение trie-дерева

Рис. 15.7. Построение trie-дерева

На этой последовательности рисунков показан результат вставки ключей A S E R C H I N в первоначально пустое trie-дерево.

Лемма 15.2. Структура trie-дерева не зависит от порядка вставки ключей: для каждого данного множества различных ключей существует уникальное trie-дерево.

Этот фундаментальный факт, который можно доказать индукцией по поддеревьям — отличительная особенность trie-деревьев: для всех остальных рассмотренных деревьев поиска структура создаваемого дерева зависит и от набора ключей, и от порядка их вставки. $\blacksquare$

Левое поддерево trie-дерева содержит все ключи, ведущий разряд которых равен 0, а правое поддерево — все ключи, ведущий разряд которых равен 1.

Это свойство trie-деревьев обусловливает прямое соответствие с поразрядным поиском: поиск по бинарному trie-дереву разбивает файл совершено так же, как при бинарной быстрой сортировке (см. "Поразрядная сортировка" ). Такое соответствие становится очевидным при сравнении trie-дерева, показанного на рис. 15.6, с диаграммой разбиения для бинарной быстрой сортировки на рис. 10.4 (не считая незначительного различия в ключах); это аналогично соответствию между поиском по бинарному дереву и быстрой сортировкой, отмеченному в "Таблицы символов и деревья бинарного поиска" .

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

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

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

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

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

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