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

Абстрактные типы данных

Абстрактные объекты и коллекции объектов

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

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

В разделе 3.1 "Элементарные структуры данных" , в котором обсуждалось применение простых типов данных для написания программ, не зависящих от типов объектов, для указания типов элементов данных использовалось описание typedef. Этот подход позволяет использовать один и тот же код для, скажем, целых чисел и чисел с плавающей точкой за счет простого изменения typedef. При использовании указателей типы объектов могут быть сколь угодно сложными. При таком подходе часто приходится делать неявные предположения относительно операций, выполняемых с объектами (например, в программе 3.2 предполагается, что для объектов типа Number определены операции сложения, умножения и приведения к типу float), и, кроме того, представление данных не скрывается от клиентских программ. Абстрактные типы данных позволяют сделать явными любые предположения относительно операций, выполняемых с объектами данных.

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

После реализации класса Item для обобщенных объектов (либо выбора подходящего встроенного класса) мы будем пользоваться механизмом шаблонов языка С++ для написания кода, который является обобщенным относительно типов объектов. Например, операцию обмена для обобщенных элементов можно определить следующим образом:

template <class Item>
void exch(Item &x, Item &y)
  { Item t = x; x = y; y = t; }
        

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

Разобравшись с классами обобщенных объектов, можно перейти к рассмотрению коллекций (collection) объектов. Многие структуры данных и алгоритмы, которые будут рассмотрены в данной книге, применяются для реализации фундаментальных АТД, представляющих собой коллекции абстрактных объектов и создаваемых с помощью двух следующих операций:

  • вставить новый объект в коллекцию.
  • удалить объект из коллекции.

Такие АТД называются обобщенными очередями (generalized queue). Как правило, для удобства в них также явно включаются следующие операции: создать (construct) структуру данных (конструктор) и подсчитать (count) количество объектов в структуре данных (или просто проверить, пуста ли она). Могут также потребоваться операции уничтожить (destroy) структуру данных (деструктор) и копировать (copy) структуру данных (конструктор копирования); эти операции будут рассмотрены в разделе 4.8.

Когда объект вставляется в коллекцию, тут все понятно, но какой объект имеется в виду при удалении объекта из коллекции? В разных АТД для коллекций объектов применяются различные критерии для определения того, какой объект удалять в операции удаления, и устанавливаются различные правила, связанные с этими критериями. Помимо операций вставить и удалить, мы столкнемся с рядом других естественных операций. Многие алгоритмы и структуры данных, рассматриваемые в книге, были спроектированы с целью обеспечения эффективной реализации различных поднаборов этих операций и для разнообразных критериев выполнения операции удаления и других правил. Эти АТД концептуально просты и широко используются в огромном множестве вычислительных задач - поэтому они заслуживают серьезного внимания.

Мы рассмотрим некоторые из этих фундаментальных структур данных, их свойства и примеры применения; в то же самое время мы используем их в качестве примеров, чтобы проиллюстрировать основные механизмы, используемые для разработки АТД. В разделе 4.2 будет рассмотрен стек магазинного типа (pushdown stack), в котором для удаления объектов используется следующее правило: всегда удаляется объект, вставленный последним. В разделе 4.3 будут описаны различные применения стеков, а в разделе 4.4 - реализации стеков, при этом реализации будут отделены от приложений. После обсуждения стеков мы вернемся к процессу создания нового АТД в контексте абстракции объединение-поиск для задачи связности, которая была рассмотрена в "Введение" . А затем мы вернемся к коллекциям абстрактных объектов и рассмотрим очереди FIFO и обобщенные очереди (которые на данном уровне абстракции отличаются от стеков только правилом удаления элементов), а также обобщенные очереди без повторяющихся элементов.

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

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

В языке C++ классы, реализующие коллекции абстрактных объектов, называются контейнерными классами (container class). Некоторые из структур данных, которые будут рассмотрены ниже, реализованы в библиотеке языка C++ или ее расширениях (стандартной библиотеке шаблонов - Standard Template Library). Во избежание путаницы мы будем редко ссылаться на эти классы, и излагать материал, начиная с самых основ.

Упражнения

  • 4.6. Дайте определение для класса Item, в котором для проверки равенства чисел с плавающей точкой используется перегруженная операция ==. Считайте два числа с плавающей точкой равными, если абсолютная величина их разности, деленная на большее (по абсолютной величине) из двух чисел, меньше чем 10-6.
  • 4.7. Дайте определение класса Item и перегрузите операции == и << так, чтобы их можно было использовать в программе обработки игральных карт.
  • 4.8. Перепишите программу 3.1 так, чтобы в ней использовался класс обобщенных объектов Item. Ваша программа должна работать для любого типа объектов класса Item, которые могут выводиться при помощи операции <<, генерироваться случайным образом статической функцией-членом rand(), и для которых определены операции + и /.

АТД для стека магазинного типа

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

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

Определение 4.2. Стек магазинного типа - это АТД, который включает две основные операции: вставить, или втолкнуть (push), новый элемент и удалить, или вытолкнуть (pop), элемент, вставленный последним.

Когда мы говорим об АТД стека магазинного типа, мы считаем, что существует достаточно хорошее описание операций втолкнуть и вытолкнуть, чтобы клиентская программа могла их использовать, а также некоторая реализация этих операций в соответствии с правилом удаления элементов такого стека: последним пришел, первым ушел (last-in, first-out, сокращенно LIFO).

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

 Пример стека магазинного типа (очереди LIFO)

Рис. 4.1. Пример стека магазинного типа (очереди LIFO)

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

Как было сказано в предыдущем разделе, для того чтобы можно было писать программы, использующие абстракцию стека, сначала необходимо определить интерфейс. Поэтому мы объявляем набор общедоступных функций-членов, которые будут использоваться в реализациях класса (см. программу 4.4). Все остальные члены класса объявляются приватными (private), и тем самым обеспечивается, что эти функции будут единственной связью между клиентскими программами и реализациями. В "Введение" и "Элементарные структуры данных" мы уже видели важность определения абстрактных операций, на которых основаны требуемые вычисления. Сейчас мы рассматриваем механизм, позволяющий записывать программы, в которых применяются эти абстрактные операции. Для реализации такой абстракции используется механизм классов, который позволяет скрыть структуры данных и реализацию от программы-клиента. В разделе 4.3 будут рассмотрены примеры клиентских программ, использующих абстракцию стека, а в разделе 4.4 - соответствующие реализации.

Первая строка кода интерфейса для АТД стека в программе 4.4 добавляет в этот класс шаблон C+ + , позволяющий клиентским программам указывать вид объектов, которые могут заноситься в стек.

Объявление

STACK<int> save(N) указывает, что элементы стека save должны быть типа int (и что стек может вместить не более N элементов). Программа-клиент может создавать стеки, содержащие объекты типа float, или char, или любого другого типа (даже типа STACK) - для этого необходимо просто изменить параметр шаблона в угловых скобках. Мы можем считать, что указанный класс замещает в реализации класс Item везде, где он встречается.

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

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

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

Программа 4.4. Интерфейс АТД стека

Используя то же соглашение, что и в программе 4.3, мы определяем АТД стека через объявление общедоступных функций. При этом предполагается, что представление стека и любой другой код, зависящий от реализации, являются приватными, чтобы можно было изменять реализации, не изменяя код клиентских программ. Кроме того, в этом интерфейсе применяется шаблон, что позволяет программам-клиентам использовать стеки, содержащие объекты любых классов (см. программы 4.5 и 4.6), а в реализациях для обозначения типа объектов стека использовать ключевое слово Item (см. программы 4.7 и 4.8). Аргумент конструктора STACK задает максимальное количество элементов, которые можно поместить в стек.

template <class Item>
class STACK
  {
    private:
      // Программный код, зависящий от реализации
    public:
      STACK(int);
      int empty() const;
      void push(Item item);
      Item pop();
  };
        

Упражнения

  • 4.9. В последовательности

    E A S * Y * Q U E * * * S T * * * I O * N * * *

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

  • 4.10. Используя те же соглашения, что и в упражнении 4.9, вставьте звездочки в последовательность E A S Y таким образом, чтобы последовательность значений, возвращаемых операциями вытолкнуть, была следующей: (1) E A S Y; (2) Y S A E; (3) A S Y E; (4) A Y E S; либо докажите, что такая последовательность невозможна.
  • 4.11. Предположим, что даны две последовательности букв. Разработайте алгоритм, позволяющий определить, можно ли в первую последовательность добавить звездочки так, чтобы эта последовательность, выполненная как последовательность стековых операций (в смысле упражнения 4.10), дала в результате вторую последовательность.
Александра Боброва
Александра Боброва

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

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

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

Никита Андриянов
Никита Андриянов
Владимир Хаванских
Владимир Хаванских
Россия, Москва, Высшая школа экономики
Вадим Рычков
Вадим Рычков
Россия, Москва, МГТУ Станкин