Опубликован: 06.10.2011 | Доступ: свободный | Студентов: 1678 / 90 | Оценка: 4.67 / 3.67 | Длительность: 18:18:00
Лекция 8:

Хэш-таблицы, стеки, очереди

< Лекция 7 || Лекция 8: 123 || Лекция 9 >

7.4. Очереди

Очереди с их политикой FIFO ("первый пришел – первый ушел") полезны во многих приложениях. Вот типичные примеры.

  • При моделировании, особенно в варианте, известном как моделирование дискретных событий. Программа выполняет шаги, моделируя события, происходящие в некотором процессе – на сборочной линии, собирающей машины из комплектующих деталей, в информационной сети, передающей сообщения, в магазине, обслуживающем покупателей. Часто обработка событий в этих случаях удовлетворяет политике FIFO, и очередь представляет возникающие события в процессе.
  • Аналогичное моделирование требуется и при организации графического интерфейса пользователя (GUI), где события инициируются пользователем – щелчки мыши, нажатия клавиш, перемещение курсора – и должны обрабатываться в порядке их возникновения.
  • В операционных системах и при организации параллельного программирования часто применима схема "поставщик – потребитель", где один процесс – поставщик – генерирует некоторую информацию, другой – потребитель – читает и обрабатывает ее в порядке поступления. Структура, используемая для обмена информацией, представляет очередь в варианте, называемом "буфер".
Поставщик – потребитель, взаимодействие через буфер

Рис. 7.15. Поставщик – потребитель, взаимодействие через буфер

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

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

Очередь на связном списке

Рис. 7.16. Очередь на связном списке

Операция put(v) реализуется просто как rep.put_front(v) (здесь, как обычно, rep задает список). Запрос item возвращает последний элемент списка, а remove – удаляет его. Класс LINKED_QUEUE из EiffelBase сопровождается инвариантом

is_always_after: not empty implies rep.after
    

Выполнение инварианта гарантирует, что курсор всегда находится в конце списка.

Представление очереди массивом немного изощреннее, чем для стеков, поскольку необходимо добавлять элементы на одном конце, а удалять на другом. В этом случае требуются два указателя, которые в классе ARRAYED_ QUEUE называются in_index и out_index, и оба являются закрытыми атрибутами. Запрос count по-прежнему дает число элементов очереди. Естественным, но не лучшим решением является хранение элементов очереди в интервале in_index .. out_index, как показано на рисунке:

Возможное состояние до очереди на массиве

Рис. 7.17. Возможное состояние до очереди на массиве

Реализация для такого представления очевидна:

remove: out_index = out_index + 1,
put(v): rep[in_index]:= v; in_index:= in_index +1
    

При таком подходе память, отведенная массиву, может быть быстро исчерпана при добавлении элементов, даже если происходит их удаление, поскольку пространство в начале массива останется неиспользованным:

Очередь на массиве в момент достижения правого конца массива

Рис. 7.18. Очередь на массиве в момент достижения правого конца массива

Решение: когда маркер in_index превосходит емкость capacity, то операция put должна по кругу возвращаться в начало массива, аналогично должна вести себя remove. Концептуально массив превращается в круг:

Массив как бублик

Рис. 7.19. Массив как бублик

Вот пример реализации put в классе ARRAYED_QUEUE:

put (v: G)
        — Добавить v как новый элемент.
    do
        if count + 1 = rep.count then grow end
        rep [in_index]:= v
        in_index:= (in_index + 1) \\ capacity
        if in_index = 0 then in_index:= capacity end
    end
    

Первый оператор выделяет массиву дополнительную память, если ее действительно не хватает. Процедура grow просто вызывает resize для нашего массива. Увеличение in_index выполняется по модулю capacity (i \\ j дает остаток от деления i на j, а i // j дает целую часть от деления нацело). Реализация настраиваемая (смотри заключительный if…), массив rep может индексироваться от 1 до capacity, но может – рекомендованное упражнение – индексироваться, начиная с нуля.

Очереди при подходящей реализации столь же эффективны, как и стеки.

Операция Метод в классе очереди Сложность Комментарий
Доступ к старейшему элементу item O(1)
Добавление элемента put O(1) При автоматической перестройке иногда O(count)
Удаление старейшего элемента remove O(1)

7.5. Итерирование структуры данных

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

Определение: итератор
Итератор – это механизм, который может применять одну или несколько операций к элементам контейнера, рассматривая это как операцию над структурой в целом.

Мы уже видели многие примеры итерирования контейнерных структур. Все они соответствуют общему образцу: если your_list относится к типу LINKED_LIST [T] или к более общему типу LIST [T] (для любой реализации списка) и есть процедура

some_opereation(x: T)
    

то следующая схема итерирует операцию над списком

from
        your_list.start
invariant
        — Все операции перед курсором уже подверглись операции some_operation
until
        your_list.after
loop
        some_operation (your_list.item)
        your_list.forth
variant
        your_list.count – your_list.index + 1
end
    

Как альтернатива, операция применяется только к элементам, которые удовлетворяют условию, заданному функцией your_condition (x:T): BOOLEAN. В этом случае тело цикла следует изменить следующим образом:

if your_condition (your_list.item) then
        some_operation (your_list.item)
end
    

Другие варианты итерирования включают:

  • применение операции ко всем элемента контейнера, пока не встретится элемент, удовлетворяющий (не удовлетворяющий) некоторому условию;
  • выяснение, существует ли по крайней мере один элемент (все элементы), удовлетворяющий некоторому условию.

Выделение общих схем является правильной стратегией. Еще лучше создать повторно используемый код, чтобы не приходилось каждый раз писать его заново. И на самом деле, на Eiffel можно применять итерационный механизм без написания циклов, используя такие методы, как do_all и do_if, которые применимы ко всем классам, задающим списки. Они раз и навсегда охватывают все предшествующие циклические структуры, так что два последних примера можно записать гораздо проще:

your_list.do_all (agent your_operation)
your_list.do_if (agent your_operation, agent your_condition)
    

Здесь agent your_operation обозначает объект, который представляет процедуру your_operation, готовую к применению к каждому элементу, а agent your_condition аналогично задает запрос. Для понимания деталей следует дождаться рассмотрения агентов в последующих лекциях.

7.6. Другие структуры

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

7.7. Дальнейшее чтение

Дональд Кнут (2005)

Рис. 7.20. Дональд Кнут (2005)
Альфред Ахо (2007)

Рис. 7.21. Альфред Ахо (2007)
  1. Дональд Кнут: "Искусство программирования", т 1. "Основные алгоритмы", т 3. "Сортировка и Поиск", М., Мир. 1976 г. (Последнее издание в России – 2008 г.)

    Широко известный учебник по структурам данных и алгоритмам. Часть из задуманного 7-томного выпуска, из которого вышли в печать три тома (некоторые главы четвертого известны в виде отдельных выпусков).

  2. Ахо А., Хопкрофт Дж., Ульман Дж. "Построение и анализ вычислительных алгоритмов", М., Мир, 1976 г.

    Компактный обзор наиболее важных алгоритмов и структур данных. До сих пор остается великолепным обзором в этой области.

  3. Кормен Т., Лейзерсон Ч., Ривест Р. "Алгоритмы: построение и анализ", 2002 г.

    Великолепный современный учебник.

  4. Bertrand Meyer "Reusable Software", Prentice Hall, 1994.

    Изложение принципов проектирования, применяемых при построении качественных, повторно используемых библиотек; сопровождается примерами из EiffelBase.

7.8. Ключевые концепции этой лекции

  • Статическая типизация делает программы более ясными и позволяет обнаруживать многие ошибки на этапе компиляции.
  • Универсальный класс имеет один или несколько родовых параметров, представляющих типы. Это обеспечивает гибкость и, в частности, полезно при описании контейнерных структур.
  • Структуры данных должны поддерживать перестройку, позволяющую настраивать размер в зависимости от объема приходящих данных.
  • Для согласованности библиотек желательна политика стандартного именования методов.
  • Абстрактная сложность позволяет оценить производительность алгоритмов вне зависимости от выбора "железа", фокусируясь на поведении алгоритма для данных больших размеров и игнорируя аддитивные и мультипликативные константные множители.
  • Нотация "О-большое", как в O(n^2), выражает абстрактную сложность.
  • Массивы обеспечивают доступ и замену элементов за константное время благодаря индексам из фиксированного интервала. Хотя перестройка размера массива возможна, они не подходят в случаях частой вставки или удаления элементов.
  • Хеш-таблицы обобщают массивы, позволяя вместо целочисленных индексов использовать почти произвольные ключи, например строки, сохраняя при этом доступ и замену элементов в основном за константное время.
  • Списки описывают последовательные структуры, и в варианте со ссылками поддерживают быстрые операции вставки и удаления.
  • Распределители позволяют вам получать доступ, вставку и удаление элементов в строго определенном месте. Политика LIFO ("последний пришел – первый ушел") управляет стеками, FIFO ("первый пришел – первый ушел") – очередями.
  • Стеки, в частности, полезны для представления вложенных структур и интенсивно используются в компиляторах и операционных системах. Реализация стека массивом является общепринятой; на одном массиве возможно размещение двух стеков.
  • Очереди особенно полезны при моделировании и в параллельном программировании, где известны как буферы. При реализации очереди массивом последний должен рассматриваться как закольцованный.

Новый словарь

Abstract complexity Абстрактная сложность Activation record Активизационная запись
Actual generic parameter Фактический родовой параметр Array Массив
Complexity Сложность Call chain Цепочка вызовов
Cursor Курсор Correctness Корректность
Dynamic typing Динамическая типизация Dispenser Распределитель
Formal generic parameter Формальный родовой параметр FIFO Первый пришел – первый ушел
Generic derivation Родовое порождение Generic class Универсальный (родовой) класс
Hash table Хеш-таблица Genericity Универсальность
Linked list Связный (односвязный) список Heap Куча
List Список LIFO Последний пришел – первый ушел
Priority queue Очередь с приоритетами Parameter Параметр
Stack Стек Queue Очередь
Static typing Статическая типизация Run-time stack Стек периода выполнения
Validity Правильность

7.9. Упражнения

7.9.1. Словарь

Дайте точные определения всем терминам словаря.

7.9.2. Карта концепций

Добавьте новые термины в карту концепций, построенную в предыдущих лекциях.

7.9.3. Два в одном

Напишите класс DOUBLE_STACK [G], реализующий два стека на одном массиве. Вы можете назвать соответствующие методы put1, put2, remove1, remove2 и так далее. Стеки имеют ограниченный размер, так что позаботьтесь включить правильные предусловия и инварианты класса.

7.9.4. Индексация, начинающаяся с нуля

Реализация, которую мы рассматривали для очередей, построенных на массиве, подобна классу ARRAYED_QUEUE из библиотеки EiffelBase, но без наследования, использует массив rep, индексируемый начиная с единицы.

  1. Используя как образец реализацию put, данную в тексте, напишите процедуру remove и процедуру создания make, задающую пустую очередь.
  2. Перепишите все три метода, используя индексацию массива, начинающуюся с нуля.

7.9.5. Обращение списка

Напишите процедуру обращения списка для двусвязного списка и списка, построенного на массиве, поместив их в класс, наследующий от соответствующих классов EiffelBase: TWO_WAY_LIST или ARRAYED_LIST.

< Лекция 7 || Лекция 8: 123 || Лекция 9 >