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

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

Получение конкретного фрагмента или фрагментов информации из больших объемов ранее сохраненных данных - это основная операция, называемая поиском и присущая многим вычислительным задачам. Как и в случае алгоритмов сортировки, описанных в главах 6-11, и, в частности, очередей с приоритетами из главы 9 "Очереди с приоритетами и пирамидальная сортировка" , мы работаем с данными, разделенными на части, или элементами (item), каждый из которых имеет ключ (key), используемый при поиске. Цель поиска - отыскание элементов с ключами, которые соответствуют заданному ключу поиска. Обычно поиск проводится для доступа к содержащейся в элементе информации (а не просто к ключу), чтобы выполнить ее обработку.

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

Определение 12.1. Таблица символов (или таблица имен, или, реже, таблица идентификаторов) - это структура данных элементов с ключами, которая поддерживает две базовые операции: вставку нового элемента и возврат элемента с заданным ключом.

Иногда таблицы символов называют также словарями (dictionary), по аналогии с хорошо знакомой системой упорядочения определений слов, когда они перечислены в книге в алфавитном порядке. Так, в словаре английского (или любого другого) языка " ключи " - это слова, а " элементы " - связанные со словами записи, содержащие перевод, транскрипцию и другую информацию. Для отыскания информации в словаре обычно используются алгоритмы поиска, основанные на расположении записей в алфавитном порядке. Телефонные книги, энциклопедии и другие справочники в основном организованы таким же образом, и некоторые из рассматриваемых методов поиска (например, алгоритм бинарного поиска в "Принципы анализа алгоритмов" и 12.4), также основываются на упорядоченности записей.

Преимущество компьютерных таблиц символов в том, что они гораздо динамичнее, чем словарь или телефонная книга. Поэтому большинство рассматриваемых методов строят структуры данных, которые не только позволяют использовать эффективные алгоритмы поиска, но и поддерживают эффективные реализации операций добавления новых элементов, удаления или изменения элементов, объединения двух таблиц символов в одну и т.п. В этой главе мы вновь рассмотрим многие вопросы, связанные с операциями, которые рассматривались в "Очереди с приоритетами и пирамидальная сортировка" применительно к очередям с приоритетами. Разработка динамических структур данных для поддержки поиска - одна из старейших и наиболее широко изученных задач в компьютерных науках; мы будем заниматься ей в этой главе и в лекциях 13-16. Мы увидим, что для реализации таблиц символов разработано (и продолжает разрабатываться) множество оригинальных алгоритмов.

Помимо вышеописанных основных применений, таблицы символов интенсивно изучаются компьютерными теоретиками и программистами, поскольку эти таблицы незаменимы для организации программного обеспечения в компьютерных системах. Таблица символов служит словарем для программы: ключи - это символические имена, используемые в программе, а элементы содержат информацию, описывающую объекты с этими именами. Начиная с зари развития компьютерной техники, когда таблицы символов позволили программистам перейти от использования числовых адресов в машинных кодах к символическим именам языка ассемблера, и завершая современными приложениями нового тысячелетия, когда символические имена используются во всемирных компьютерных сетях, быстрые алгоритмы поиска играли и будут играть в компьютерной обработке важную роль.

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

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

В "Принципы анализа алгоритмов" в качестве иллюстрации пользы математического анализа при разработке эффективных алгоритмов уже были рассмотрены два алгоритма поиска. Для полноты изложения мы повторим часть информации, приведенной в "Принципы анализа алгоритмов" , хотя вместо некоторых доказательств будут приведены просто ссылки на эту главу. Позже в данной главе мы будем использовать и основные свойства двоичных деревьев, рассмотренных в "Рекурсия и деревья" .

Абстрактный тип данных таблицы символов

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

Нас будут интересовать следующие операции:

  • Вставить (insert) новый элемент.
  • Найти (search) элемент (или элементы) по заданному ключу.
  • Удалить (delete) указанный элемент.
  • Выбрать (select) к-й по величине элемент в таблице символов.
  • Сортировать (sort) таблицу символов (вывести все элементы в порядке их ключей).
  • Объединить (join) две таблицы символов.

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

В общем случае мы будем использовать термин " алгоритм поиска " в значении " реализация АТД таблицы символов " , хотя этот термин скорее предполагает определение и построение структуры данных для таблицы символов и, в дополнение к поиску, реализацию операций абстрактного типа данных. Таблицы символов так важны для стольких компьютерных приложений, что во многих средах программирования они доступны как высокоуровневые абстракции. Стандартная библиотека C содержит программу bsearch - реализацию алгоритма бинарного поиска, описанного в разделе 12.4, а библиотека стандартных шаблонов C++ предоставляет множество таблиц символов, называемых " ассоциативными контейнерами " . Как обычно, реализации " вообще " трудно выполнить требования, предъявляемые к производительности специализированных приложений. Так что целью изучения многих оригинальных методов, разработанных для реализации абстракции таблицы символов, будет выработка понимания, которое поможет принять решение, когда использовать готовую реализацию, а когда разработать специальную, предназначенную для конкретного приложения.

Как и в случае с сортировкой, мы будем изучать методы без определения типов обрабатываемых элементов. Столь же подробно, как в "Элементарные методы сортировки" , будут рассматриваться реализации, использующие интерфейс, в котором определены тип Item и базовые абстрактные операции с данными. Мы ознакомимся с методами как на основе сравнений, так и поразрядные, где в качестве индексов используются ключи или части ключей. Чтобы подчеркнуть различие ролей, которые играют при поиске элементы и ключи, мы расширим понятие элемента, которое использовалось в главах 6-11: сейчас элементы типа Item содержат ключи типа Key. Поскольку теперь требуется (слегка) больше элементов, чем было необходимо для ознакомления с алгоритмами сортировки, будем считать, что они оформлены как абстрактные типы данных, реализованные с помощью классов C++, как показано в программе 12.1. Функция-член key() предназначена для извлечения ключей из элементов, а перегруженная операция ==проверяет равенство двух ключей. В этой главе и "Сбалансированные деревья" также перегружается операция < для сравнения значений двух ключей, что бывает полезно при поиске; алгоритмы поиска, описанные в "Хеширование" и "Поразрядный поиск" , основываются на извлечении частей ключей с помощью базовых поразрядных операций, которые использовались в главе 10 "Поразрядная сортировка" . Кроме того, предполагается, что элементы инициализируются пустыми (null) значениями, и что клиенты имеют доступ к функции null() , которая может проверить, является ли элемент пустым.

Программа 12.1. Пример реализации АТД элемента

Это определение класса элементов, которые представляют собой небольшие записи, состоящие из целочисленных ключей и связанной с ними информации (значения с плавающей точкой), иллюстрирует основные соглашения в отношении элементов таблиц символов. Наши реализации таблиц символов являются клиентскими программами, в которых сравнение ключей выполняют операции == и <, а функции-члены key() и null() позволяют, соответственно, получить значение ключа и проверить, является ли элемент пустым.

В определения типа элемента включены также функции scan (чтение Item), rand (генерация случайного Item) и show (вывод Item), которые будут использоваться драйверами. Это позволяет создавать и тестировать различные реализации таблиц символов, состоящие из различных типов элементов.

#include <stdlib.h>
#include <iostream.h>
static int maxKey = 1000;
typedef int Key;
class Item
  { private:
      Key keyval;
      float info;
    public:
      Item()
        { keyval = maxKey; }
      Key key()
        { return keyval; }
      int null()
        { return keyval == maxKey; }
      void rand()
        { keyval = 100 0*::rand()/RAND MAX;
        info = 1.0*::rand()/RAND MAX; }
      int scan(istream& is = cin)
        { return (is >> keyval >> info) != 0; }
      void show(ostream& os = cout)
        { os << keyval << " " << info << endl; }
  };
ostream& operator<<(ostream& os, Item& x)
  { x.show(os); return os; }
      

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

Чтобы использовать при поиске интерфейсы и реализации для чисел с плавающей точкой, строк и более сложных элементов, описанных в "Элементарные методы сортировки" , нужно только обеспечить нужные определения для Key, key(), null() и операций == и <, а также сделать функции rand, scan и show, функциями-членами, которые правильно обращаются к ключам.

Программа 12.2 представляет собой интерфейс, определяющий базовые операции таблицы символов (за исключением операции объединить). Этот интерфейс будет использоваться в этой и нескольких следующих главах как интерфейс между клиентскими программами и всеми реализациями поиска. Мы не будем использовать АТД первого класса в смысле "Абстрактные типы данных" (см. упражнение 12.6), поскольку в большинстве программ используется только одна таблица, а добавление конструкторов копирования, перегруженных операций присваивания и деструкторов, хоть это и несложная задача в большинстве реализаций, все же отвлекало бы от важных характеристик алгоритмов. В программе 12.2 можно было бы также определить версию интерфейса для работы с дескрипторами элементов, как в программе 9.8 (см. упражнение 12.7), но обычно это излишне усложняет программу, если можно манипулировать элементом с помощью ключа. В интерфейсе не указан способ определения элемента, который нужно удалить. В большинстве реализаций используется интерпретация " удалить элемент с ключом, равным заданному " , при этом подразумевается предварительный поиск. В других реализациях, где используются дескрипторы и можно проверить идентичность элемента, поиск перед удалением не обязателен, и поэтому в них возможны более быстрые алгоритмы. А при изучении алгоритмов для операции объединить - в приложениях, в которых обрабатываются несколько таблиц символов - хорошо бы использовать реализации АТД первого класса для таблицы символов, где сведены к минимуму затраты времени и памяти (см. раздел 12.9).

В некоторых алгоритмах не предполагается наличие какого-либо определенного порядка ключей, и поэтому для сравнения ключей в них используется только операция ==(без <), однако во многих реализациях таблиц символов задействуется отношение порядка ключей, используемое операцией < для структурирования данных и управления поиском. Кроме того, абстрактные операции выбрать и сортировать явно используют упорядоченность ключей. Функция сортировать выполняет лишь вывод всех элементов в выходной поток по порядку, но не обязательно сортирует их. Ее можно легко обобщить до функции, которая перебирает элементы в порядке их ключей и, возможно, применяет к каждому из них процедуру, переданную в аргументе. Функции сортировать, используемые для таблиц символов, мы называем show (вывести), поскольку наши реализации выполняют вывод содержимого таблицы символов в порядке возрастания. В алгоритмах, в которых операция < не используется, нет необходимости сравнивать ключи друг с другом, поэтому такие алгоритмы могут не поддерживать операции выбрать и сортировать.

Дмитрий Уколов
Дмитрий Уколов
Михаил Новопашин
Михаил Новопашин
Виталий Лубман
Виталий Лубман
Россия
Наталья Горбунова
Наталья Горбунова
Россия, Черноголовка