
- •Введение
- •Основные понятия и определения
- •Типы данных
- •1.1.1. Понятие типа данных
- •1.2.2. Внутреннее представление базовых типов в оперативной памяти
- •1.2.2. Внутреннее представление структурированных типов данных
- •1.2.3. Статическое и динамическое выделение памяти
- •Абстрактные типы данных (атд)
- •Понятие атд
- •1.2.2. Спецификация и реализация атд
- •Структуры данных
- •1.3.1. Понятие структуры данных
- •1.3.2. Структуры хранения — непрерывная и ссылочная
- •1.4.3. Классификация структур данных
- •Алгоритмы
- •1.4.1. Понятие алгоритма
- •1.4.2. Способы записи алгоритмов.
- •1.4.3. Введение в анализ алгоритмов Вычислительные модели
- •Задача анализа алгоритмов
- •Время работы алгоритма
- •Время выполнения в худшем и среднем случае
- •1.4.3. Введение в рекурсию
- •Первые примеры
- •1.5.1. Введение в «длинную» арифметику
- •1.5.2. Рекурсия
- •1.5.3. Поразрядные операции. Реализация атд «Множество»
- •2. Линейные структуры данных
- •2.1. Атд "Стек", "Очередь", "Дек"
- •2.2. Реализация стеков
- •2.2.1. Непрерывная реализация стека с помощью массива
- •2.2.2. Ссылочная реализация стека в динамической памяти
- •2.2.3. Примеры программ с использованием стеков
- •2.3. Реализация очередей
- •2.3.2. Непрерывная реализация очереди с помощью массива
- •2.3.2. Ссылочная реализация очереди в динамической памяти
- •2.3.3. Ссылочная реализация очереди с помощью циклического списка
- •2.3.4. Очереди с приоритетами
- •2.3.5. Пример программы с использованием очереди
- •2.4. Списки как абстрактные типы данных
- •2.4.1. Модель списка с выделенным текущим элементом
- •2.4.2. Однонаправленный список (список л1)
- •2.4.3. Двунаправленный список (список л2)
- •2.4.4. Циклический (кольцевой) список
- •2.5. Реализация списков с выделенным текущим элементом
- •2.5.1. Однонаправленные списки Ссылочная реализация в динамической памяти на основе указателей
- •2.5.2. Двусвязные списки
- •2.5.3. Кольцевые списки
- •2.5.4. Примеры программ, использующих списки Очередь с приоритетами на основе линейного списка
- •Задача Иосифа (удаление из кольцевого списка)
- •2.6. Рекурсивная обработка линейных списков
- •2.6.1. Модель списка при рекурсивном подходе
- •2.6.2. Реализация линейного списка при рекурсивном подходе
- •3. Иерархические структуры данных
- •3.1. Иерархические списки
- •3.1.1 Иерархические списки как атд
- •3.1.2. Реализация иерархических списков
- •3.2. Деревья и леса
- •3.2.1. Определения
- •3.2. Способы представления деревьев
- •3.2.3. Терминология деревьев
- •3.2.4. Упорядоченные деревья и леса. Связь с иерархическими списками
- •3.3. Бинарные деревья
- •3.3.1. Определение. Представления бинарных деревьев
- •3.3.2. Математические свойства бинарных деревьев
- •3.4. Соответствие между упорядоченным лесом и бинарным деревом
- •3.5. Бинарные деревья как атд
- •3.6. Ссылочная реализация бинарных деревьев
- •3.6.1. Ссылочная реализация бинарного дерева на основе указателей
- •3.6.2. Ссылочная реализация на основе массива
- •3.6.3. Пример — построение дерева турнира
- •3.7. Обходы бинарных деревьев и леса
- •3.7.1. Понятие обхода. Виды обходов
- •3.7.2. Рекурсивные функции обхода бинарных деревьев
- •3.7.3. Нерекурсивные функции обхода бинарных деревьев
- •3.7.4. Обходы леса
- •3.7.5. Прошитые деревья
- •3.8. Применения деревьев
- •3.8.1. Дерево-формула
- •3.8.2. Задача сжатия информации. Коды Хаффмана
- •4. Сортировка и родственные задачи
- •4.1. Общие сведения
- •4.1.1. Постановка задачи
- •4.1.2. Характеристики и классификация алгоритмов сортировки
- •4.2. Простые методы сортировки
- •4.2.1. Сортировка выбором
- •4.2.2. Сортировка алгоритмом пузырька
- •4.2.3.Сортировка простыми вставками.
- •4.3. Быстрые способы сортировки, основанные на сравнении
- •4.3.1. Сортировка упорядоченным бинарным деревом
- •Анализ алгоритма сортировки бинарным деревом поиска
- •4.3.2. Пирамидальная сортировка
- •Первая фаза сортировки пирамидой
- •Вторая фаза сортировки пирамидой
- •Анализ алгоритма сортировки пирамидой
- •Реализация очереди с приоритетами на базе пирамиды
- •4.3.2. Сортировка слиянием
- •Анализ алгоритма сортировки слиянием
- •4.3.3. Быстрая сортировка Хоара
- •Анализ алгоритма быстрой сортировки
- •4.3.4. Сортировка Шелла
- •4.3.5. Нижняя оценка для алгоритмов сортировки, основанных на сравнениях
- •4.4. Сортировка за линейное время
- •4.4.1. Сортировка подсчетом
- •4.4.2. Распределяющая сортировка от младшего разряда к старшему
- •4.4.3. Распределяющая сортировка от старшего разряда к младшему
- •5. Структуры и алгоритмы для поиска данных
- •5.1. Общие сведения
- •5.1.1. Постановка задачи поиска
- •5.1.2. Структуры для поддержки поиска
- •5.1.3. Соглашения по программному интерфейсу
- •5.2. Последовательный (линейный) поиск
- •5.3. Бинарный поиск в упорядоченном массиве
- •5.4. Бинарные деревья поиска
- •5.4.1. Анализ алгоритмов поиска, вставки и удаления Поиск
- •Вставка
- •Удаление
- •5.4.3. Реализация бинарного дерева поиска
- •5.5. Сбалансированные деревья
- •Определение и свойства авл-деревьев
- •Вращения
- •Алгоритмы вставки и удаления
- •Реализация рекурсивного алгоритма вставки в авл-дерево
- •5.5.2. Сильноветвящиеся деревья
- •Бинарные представления сильноветвящихся деревьев
- •5.5.3. Рандомизированные деревья поиска
- •5.6. Структуры данных, основанные на хеш-таблицах
- •5.6.2. Выбор хеш-функций и оценка их эффективности
- •Модульное хеширование (метод деления)
- •Мультипликативный метод
- •Метод середины квадрата
- •5.6.2. Метод цепочек
- •5.6.3. Хеширование с открытой адресацией
- •5.6.4. Пример решения задачи поиска с использованием хеш-таблицы
1.2.2. Внутреннее представление структурированных типов данных
Переменные структурированных типов, как правило, занимают непрерывную область памяти, т. е. соседние элементы занимают соседние ячейки памяти. Это обеспечивает возможность быстрого вычисления адреса элемента по его индексу (совокупности индексов в многомерных массивах или имени поля в записях). Такой способ доступа к элементам называется прямым доступом. Множества не составляют исключения, например, в языке Pascal множества представлены в виде битового массива.
Для обеспечения прямого доступа к элементам данных структурированных типов обычно создается дополнительная структура, называемая дескриптором, которая хранит адрес начала, размер одного элемента и, возможно, некоторые другие данные, необходимые для быстрого вычисления адреса конкретного элемента данных.
Например, для одномерного массива адрес элемента ai определяется так:
ai=a0+i*sizeof(ТипЭлементовМассива),
если нумерация элементов массива начинается с нуля, где a0 — адрес первого байта памяти, занимаемой массивом.
1.2.3. Статическое и динамическое выделение памяти
Современные языки программирования предоставляют различные способы выделения памяти для данных, обрабатываемых программой. Возможны следующие варианты.
Память под переменную выделяется до начала выполнения программы в соответствии с описанием типа и остается неизменной до конца программы. Это внешние (глобальные) переменные и статические переменные. Термин «статические» пришел из языка С и обозначает переменные, имеющие ограниченную область видимости, но существующие от начала на конца работы программы.
Память выделяется при входе в определенный блок программы также в соответствии с описанием типа и освобождается при выходе из блока (автоматические переменные);
Память выделяется (захватывается) по запросу из программы и освобождается также по запросу. При этом размер памяти может определяться типом переменной или явно указываться в запросе. Такие переменные называются динамическими.
Первые два способа называют статическим выделением памяти (не путать со статическими переменными в смысле языка С). Во многих случаях статическое выделение памяти ведет к ее неэффективному использованию, особенно для структурированных типов данных, т. к. не все выделенная область памяти реально заполняется данными и обрабатывается. Поэтому во многих языках есть удобные средства динамического формирования переменных. Возможности создания и использования динамических переменных тесно связаны с механизмами указателей, поскольку динамическая переменная не имеет статически заданного имени, и доступ к такой переменной возможен только через указатель.
Отметим важную особенность — при любом способе формирования память для переменной выделяется сразу одной непрерывной областью. Например, нельзя сначала выделить память под половину массива, а затем под оставшуюся половину, даже если пользоваться динамически формируемыми массивами. Прямой доступ к элементам достается дорогой ценой.
Абстрактные типы данных (атд)
Понятие атд
Набор стандартных типов во всех языках программирования ограничен. При этом операции, которые можно связать с каждым стандартным типом, жестко фиксированы в самом языке и не могут быть изменены программистом так, чтобы они стали частью этого типа. Заметим, что производные типы данных позволяют ограничить множество значений базового типа, но не переопределить операции. Структурированные типы данных также не являются исключением, поскольку набор стандартных средств для доступа к отдельным элементам составного значения жестко фиксирован.
Однако при решении самых разнообразных задач возникает потребность в новых типах данных, связанных со своими операциями, которые не определены для стандартных типов. В других случаях появляется необходимость изменить реализацию встроенных операций, что, увы, невозможно. Идя навстречу пожеланиям прикладных программистав, разработчики компиляторов могли бы расширять набор встроенных типов в новых версиях, но есть стандарт языка, который довольно консервативен. Кроме того, нельзя расширять набор встроенных типов до бесконечности.
Выход из этой ситуации состоит в предоставлении программисту возможности конструировать свои собственные типы, связывая с ними любой набор операций. В связи с этим в программировании появилось понятие абстрактного типа данных (АТД).
Этот термин появился в 1974 г. в статье Б.Лисков и С.Зиллеса [22], посвященной принципам разрабатывавшегося ими языка программирования CLU. Термин быстро распространился среди программистов и теоретиков программирования, хотя на самом деле абстрактные типы не более абстрактны, чем стандартные типы (точнее, стандартные типы так же абстрактны, как и АТД). Иногда используется термин «типы, определяемые пользователем», но и он не совсем точно отражает суть понятия, поскольку все типы, кроме встроенных, так или иначе определяются пользователем.
Обсудим смысл этого понятия. АТД, как и обычный тип данных, определяет множество значений и множество операций, применимых к этим значениям. Однако, в отличие от стандартных типов, реализация операций для абстрактного типа не встроена в язык программирования, а является частью определения АТД. При этом прикладные программы (они называются клиентами АТД) используют абстрактные типы так же, как стандартные типы, т.е. только через набор операций, определенных для этого АТД, и не имеют никакого доступа к его реализации.
Важно отметить, что таким образом можно расширять до бесконечности набор стандартных типов, добавляя именно те типы, которые нужны для решения тех или иных конкретных задач.
Подводя итог сказанному, можно считать АТД основными строительными конструкциями при разработке алгоритмов. В [12] АТД трактуются как исполнители. Имея заданный набор исполнителей с четко определенным набором операций, можно существенно сократить время разработки алгоритмов, при этом повысить надежность разработанного программного обеспечения.
АТД является полным, если он обеспечивает достаточно операций для того, чтобы все требующиеся пользователю действия с переменными этого типа (назовем их объектами) могли быть проделаны с приемлемой эффективностью. Полнота типа зависит от контекста использования. Если тип предполагается использовать в ограниченном контексте (например, одна программа), то должно быть обеспечено достаточно операций для этого контекста.
Если тип предназначен для общего использования, желательно иметь полный набор операций [13], который должен включать 3 класса различных операций.
Примитивные конструкторы. Эти операции создают объекты соответствующего типа, не используя никаких объектов в качестве аргумента. Конструктор предшествует другим операциям.
Конструкторы и(или) модификаторы. Эти операции используют в качестве аргументов объекты соответствующего им типа и создают другие объекты такого же типа либо модифицируют заданные объекты. Это группа операций, которая используется для выполнения основной работы с объектами.
Селекторы и(или) индикаторы. Эти операции используют в качестве аргументов объекты соответствующего им типа и возвращают результат другого типа (например, логического). Они используются для получения информации об объектах или получения отдельных элементов составного значения, но никак не изменяют объекты.
Например, для простых целочисленных типов операции сложения и вычитания являются конструкторами, а операции инкремента и декремента — модификаторами, операции сравнения при этом являются индикаторами.
Задача определения необходимого набора операций АТД является очень ответственной. К счастью, имеется довольно обширный, хорошо проработанный набор АТД широкого назначения, которые считаются стандартными. Они хорошо освещены в литературе, поэтому их назначение и смысл операций понятны. Во многих языках имеются библиотеки, содержащие набор стандартных АТД универсального назначения, например, библиотека STL в C++, библиотека VCL в Delphi и т.д..
В следующих разделах стандартные АТД будут рассмотрены достаточно подробно. Отметим, что сложные АТД могут строиться на основе более простых, используя их так же, как и стандартные типы данных.