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

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

Последовательный поиск

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

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

Программа 12.5. Таблица символов (упорядоченная) на основе массива

Подобно программе 12.4, в этой реализации используется массив элементов, но здесь не требуется, чтобы ключи были небольшими целыми числами. Упорядоченность массива обеспечивается тем, что при вставке нового элемента большие элементы сдвигаются, освобождая место, как при сортировке вставками. Потом функция search выполняет просмотр массива, когда нужно найти элемент с заданным ключом. Если просмотр дошел до элемента с большим ключом, возвращается значение nullItem. Реализации функций select и sort тривиальны, а реализация функции remove оставлена в качестве упражнения (см. упражнение 12.16).

template <class Item, class Key>
class ST
  { private:
      Item nullItem, *st;
      int N;
    public:
      ST(int maxN)
        { st = new Item[maxN+1]; N = 0; }
      int count()
        { return N; }
      void insert(Item x)
        { int i = N++; Key v = x.key();
while (i > 0 && v < st[i-1].key())
  { st[i] = st[i-1]; i--; }
st[i] = x;
        }
      Item search(Key v)
        { for (int i = 0; i < N; i++)
  if (!(st[i].key() < v)) break;
if (v == st[i].key()) return st[i];
return nullItem;
        }
      Item select(int k)
        { return st[k]; }
      void show(ostream& os)
        { int i = 0; while (i < N) st[i++].show(os); }
  } ;
      

Можно использовать другой подход и создать реализацию, в которой упорядоченность элементов в массиве не обязательна. При вставке новый элемент помещается в конец массива; во время поиска осуществляется последовательный просмотр массива. Характерная особенность этого подхода состоит в том, что операция вставить выполняется быстро, а операции выбрать и сортировать требуют значительно большего объема работы (для обеих требуется один из методов, описанных в лекциях 7-10) . Удаление элемента с заданным ключом можно выполнить, найдя его, а затем переместив в его позицию последний элемент массива и уменьшив размер массива на 1; удаление всех элементов с заданным ключом реализуется повторением этой операции. Если доступен дескриптор, позволяющий определить индекс элемента в массиве, то поиск не требуется, и операция удалить выполняется за постоянное время.

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

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

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

Программа 12.6. Таблица символов (неупорядоченная) на основе связного списка

В данной реализации операций создать, подсчитать, найти и вставить используется односвязный список, каждый узел которого содержит элемент с ключом и ссылкой. Функция insert помещает новый элемент в начало списка и тратит на это постоянное время. Для выполнения просмотра списка функция-член search использует приватную рекурсивную функцию searchR.

Поскольку список не упорядочен, реализации операций выбрать и сортировать опущены.

#include <stdlib.h>
template <class Item, class Key>
class ST
  { private:
    Item nullItem;
    struct node
      { Item item; node* next;
        node(Item x, node* t)
{ item = x; next = t; }
        } ;
        typedef node *link;
        int N;
        link head;
        Item searchR(link t, Key v)
{ if (t == 0) return nullItem;
  if (t->item.key() == v) return t->item;
  return searchR(t->next, v);
}
public:
  ST(int maxN)
  { head = 0; N = 0; } int count()
      { return N; }
    Item search(Key v)
      { return searchR(head, v); }
    void insert(Item x)
      { head = new node(x, head); N++; }
  };
      

Чтобы подробнее проанализировать последовательный поиск для случайных ключей, сначала рассмотрим затраты на вставку новых ключей, отдельно для случаев успешного и неудачного поиска. Первый часто называют попаданием при поиске, а второй - промахом при поиске. Нас интересуют затраты как для попаданий, так и для неудач, в среднем и худшем случаях. Вообще-то в реализации с использованием упорядоченного массива (см. программу 12.5) каждый элемент проверяется двумя операциями сравнения (== и <). В главах 12-16 в целях анализа мы будем считать эту пару одним сравнением, поскольку обычно их можно эффективно объединить с помощью низкоуровневой оптимизации.

Лемма 12.2. Последовательный поиск в таблице символов с N элементами требует выполнения порядка N/2 сравнений при успешном поиске (в среднем).

См. лемму 2.1. Доказательство применимо к массивам или связным спискам, упорядоченным или неупорядоченным. $\blacksquare$

Лемма 12.3. Последовательный поиск в таблице символов с N неупорядоченными элементами требует постоянного количества шагов для выполнения вставок и N сравнений при неудачном поиске (всегда).

Эти утверждения справедливы для представлений как массивами, так и связными списками, и следуют непосредственно из реализаций (см. упражнение 12.20 и программу 12.6). $\blacksquare$

Лемма 12.4. Последовательный поиск в таблице символов из N упорядоченных элементов требует порядка N/2 операций для вставки, успешного поиска и неудачного поиска (в среднем).

См. лемму 2.2. И опять эти утверждения справедливы для представлений как массивами, так и связными списками, и следуют непосредственно из реализаций (см. программу 12.5 и упражнение 12.21). $\blacksquare$

Построение упорядоченных таблиц с помощью последовательных вставок по своей сути эквивалентно выполнению алгоритма сортировки вставками из "Элементарные методы сортировки" . Общее время, необходимое для построения таблицы, квадратично зависит от количества элементов, поэтому для построения больших таблиц этот метод неприменим. Однако если в небольшой таблице нужно выполнять очень большое количество операций найти, то поддержка упорядоченности элементов вполне оправданна, поскольку в соответствии с леммами 12.3 и 12.4 этот подход может вдвое уменьшить время при неудачном поиске. Если элементы с повторяющимися ключами не должны храниться в таблице, то дополнительные затраты на поддержку упорядоченности таблицы не столь велики, как может показаться, т.к. вставка выполняется только после неудачного поиска и, следовательно, время, затрачиваемое на вставку, пропорционально времени, затрачиваемому на поиск.

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

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

Эти результаты во взаимосвязи с другими алгоритмами, рассматриваемыми далее в этой главе и "Сбалансированные деревья" и "Хеширование" , сведены в таблицу 12.1. В разделе 12.4 будет рассмотрен бинарный поиск, сводящий время поиска до lg N, и поэтому широко используемый при работе со статическими таблицами (когда вставки выполняются сравнительно редко).

Таблица 12.1. Затраты на вставку и поиск в таблицах символов
Худший случай В среднем
вставить найти выбрать вставить успешный поиск неудачный поиск
Распределяющий массив 1 1 M 1 1 1
Упорядоченный массив N N 1 N/2 N/2 N/2
Упорядоченный связный список N N N N/2 N/2 N/2
Неупорядоченный массив 1 N Nlg 1 N/2 N
Неупорядоченный связный список 1 N Nlg 1 N/2 N
Бинарный поиск N lgN 1 N/2 lgN lgN
Дерево бинарного поиска N N N lgN lgN lgN
Красно-черное дерево lgN lgN lgN lgN lgN lgN
Рандомизированное дерево N* N* N* lgN lgN lgN
Хеширование 1 N* Nlg 1 1 1

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

В разделах 12.5-12.9 мы рассмотрим деревья бинарного поиска, которые обеспечивают время поиска и вставки, пропорциональное lgN, но только в среднем. В "Сбалансированные деревья" будут рассмотрены красно-черные деревья и рандомизированные деревья бинарного поиска, которые, соответственно, гарантируют логарифмическую производительность либо существенно увеличивают ее вероятность. В "Хеширование" мы познакомимся с хешированием, которое обеспечивает поиск и вставку за постоянное время в среднем, но не позволяет эффективно выполнять операцию сортировать и некоторые другие операции. В "Поразрядный поиск" будут изучаться методы поразрядного поиска, аналогичные методам поразрядной сортировки из "Поразрядная сортировка" ; в "Внешний поиск" исследуются методы, применимые к файлам на внешних носителях.

Упражнения

12.16. Добавьте операцию удалить в реализацию таблицы символов на основе упорядоченного массива (программа 12.5).

12.17. Для таблиц символов на основе списка (программа 12.6) и массива (программа 12.5) реализуйте функции searchinsert. Они должны искать в таблице символов элемент с ключом, равным ключу заданного элемента, и при неудачном поиске вставить этот элемент.

12.18. Реализуйте операцию выбрать для реализации таблицы символов на основе списка (программа 12.6).

12.19. Приведите количество сравнений, необходимых для помещения ключей E A S Y Q U E S T I O N в первоначально пустую таблицу с использованием АТД, реализованных с помощью одного из четырех элементарных подходов: упорядоченный или неупорядоченный массив или список. Пусть для каждого ключа выполняется поиск, и в случае неудачи выполняется вставка, как в упражнении 12.17.

12.20. Для интерфейса таблицы символов из программы 12.2 реализуйте операции создать, найти и вставить, используя для представления таблицы символов неупорядоченный массив. Характеристики производительности программы должны соответствовать таблица 12.1.

12.21. Для интерфейса таблицы символов из программы 12.2 реализуйте операции создать, найти и вставить, используя для представления таблицы символов упорядоченный связный список. Характеристики производительности программы должны соответствовать таблица 12.1.

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

12.23. Напишите программу-драйвер измерения производительности, которая использует функцию insert для заполнения таблицы символов, а затем функции select и remove для ее опустошения; эти операции должны многократно повторяться для случайных последовательностей ключей различной длины, от малой до большой. Программа должна замерять время каждого выполнения и выводить средние значения в виде текста или графика.

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

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

12.26. Какую реализацию таблицы символов лучше использовать для приложения, в котором в произвольном порядке выполняется 102 операций вставить, 103 операций найти и 104 операций выбрать? Обоснуйте свой ответ.

12.27.( В действительности это упражнение состоит из пяти упражнений). Выполните упражнение 12.26 для пяти других вариантов сочетания операций и частоты их использования.

12.28. Алгоритм самоорганизующегося поиска - это алгоритм, который изменяет порядок элементов так, чтобы часто запрашиваемые элементы встречались в начале поиска. Измените реализацию операции найти для упражнения 12.20 так, чтобы при каждом успешном поиске она помещала найденный элемент в начало списка, сдвигая на одну позицию вправо все элементы от начала списка до освободившейся позиции. Эта процедура называется эвристикой перемещения вперед (move-to-front).

12.29. Приведите порядок ключей после того, как элементы с ключами E A S Y Q U E S T I O N помещаются в первоначально пустую таблицу с помощью операции найти и последующей вставить в случае неудачного поиска, с использованием эвристики самоорганизующегося поиска перемещением вперед (см. упражнение 12.28).

12.30. Напишите программу-драйвер для методов самоорганизующегося поиска, в которой таблица символов заполняется N ключами с помощью функции insert, а затем выполняется 10N успешных поисков в соответствии с известным распределением вероятности.

12.31. Воспользуйтесь решением упражнения 12.30 для сравнения времен выполнения реализации из упражнения 12.20 и времени выполнения реализации из упражнения 12.28 для N = 10, 100 и 1000, используя распределение вероятности, при котором операция найти выполняется для i-го наибольшего ключа с вероятностью 1/2i при ${1}\leq{i}\leq{N}$.

12.32. Выполните упражнение 12.31 для распределения вероятности, при котором операция найти выполняется для i-го наибольшего ключа с вероятностью HN/i при ${1}\leq{i}\leq{N}$. Это распределение называется законом Зипфа.

12.33. Сравните эвристику перемещения вперед с оптимальной организацией для распределений из упражнений 12.31 и 12.32 - а именно с хранением ключей в порядке возрастания (в порядке уменьшения ожидаемой частоты обращения к ним). То есть в упражнении 12.31 вместо решения из упражнения 12.20 воспользуйтесь программой 12.5.

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

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

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

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

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

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