
- •Введение
- •Основные понятия и определения
- •1.1. Типы данных
- •1.1.1. Понятие типа данных
- •1.1.2. Внутреннее представление базовых типов в оперативной памяти
- •1.1.3. Внутреннее представление структурированных типов данных
- •1.1.4. Статическое и динамическое выделение памяти
- •1.2. Абстрактные типы данных (атд)
- •1.2.1. Понятие атд
- •1.2.2. Спецификация и реализация атд
- •1.3. Структуры данных
- •1.3.1. Понятие структуры данных
- •1.3.2. Структуры хранения — непрерывная и ссылочная
- •1.3.3. Классификация структур данных
- •1.4. Понятие алгоритма
- •1.5. Введение в анализ алгоритмов
- •1.5.1. Вычислительные модели
- •1.5.2. Показатели эффективности алгоритма
- •1.5.3. Постановка задачи анализа алгоритмов
- •1.5.4. Время работы алгоритма
- •Время выполнения в худшем и среднем случае
- •1.5.5. Асимптотические оценки сложности алгоритмов
- •Точная асимптотическая оценка θ
- •Верхняя асимптотическая оценка о
- •Нижняя асимптотическая оценка ω
- •Наиболее часто встречающиеся асимптотические оценки
- •1.6. Анализ рекурсивных алгоритмов
- •1.6.1. Рекурсия и итерация
- •1.6.2. Пример анализа рекурсивного алгоритма
- •1.7. Первые примеры
- •1.7.1. Введение в «длинную» арифметику
- •1.7.2. Примеры рекурсивных алгоритмов
- •1.7.3. Поразрядные операции. Реализация атд «Множество»
- •2. Линейные структуры данных
- •2.1. Атд "Стек", "Очередь", "Дек"
- •2.1.1. Функциональная спецификация стека
- •2.1.2. Функциональная спецификация очереди
- •2.1.3. Деки
- •2.1.4. Общие замечания по реализации атд
- •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.5.1. Каноническое соответствие между бинарным деревом и упорядоченным лесом
- •3.5.2. Взаимосвязь бинарных деревьев и иерархических списков
- •3.6. Ссылочная реализация бинарных деревьев
- •3.6.1. Ссылочная реализация бинарного дерева на основе указателей
- •3.6.2. Ссылочная реализация на основе массива
- •3.6.3. Пример — построение дерева турнира
- •3.7. Обходы бинарных деревьев и леса
- •3.7.1. Понятие обхода. Виды обходов
- •3.7.2. Пример обходов — дерево-формула
- •3.7.3. Рекурсивные функции обхода бинарных деревьев
- •3.7.3. Нерекурсивные функции обхода бинарных деревьев
- •Прямой порядок обхода (клп)
- •Центрированный порядок обхода (лкп)
- •Обратный порядок обхода (лпк)
- •Обход в ширину
- •3.7.4. Обходы леса
- •3.7.5. Прошитые деревья
- •3.8. Применение деревьев для кодирования информации — деревья Хаффмана
- •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.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. Пример решения задачи поиска с использованием хеш-таблицы
2.2.3. Примеры программ с использованием стеков
Стеки широко используются при разработке трансляторов, например, при анализе и вычислении арифметических выражений. Эта задача является предметом отдельного курса, но в качестве введения в проблему приведем компактные примеры анализа и преобразования скобочного выражения.
Пример 1.
Приведенная программа проверяет правильность расстановки скобок в заданной строке текста, длина которой в принципе не ограничена. При этом просматриваются все символы скобок: круглых, квадратных и фигурных. Учитывается, что различные скобки могут быть вложены одна в другую. Список из открывающихся скобок строится по принципу стека. Это значит, что та скобка, которая была помещена в стек последней, будет извлечена из него первой, и легко проверить, соответствует ли закрывающая скобка своей открывающей. Все остальные символы программа игнорирует.
Для проверки соответствия скобок используются две вспомогательные строки с открывающимися и закрывающимися скобками. Стандартная функция strchr() возвращает указатель на найденный символ, поэтому, чтобы найти номер данного символа, приходится вычитать из этого указателя указатель на начало строки. Все остальное просто.
Листинг 2.1 Проверка правильности расстановки всех видов скобок в строке
#include <iostream.h>
#include "stack.h"
#include "stack.cpp"
void main()
{ char s[80];
cout<<"Введите строку, содержащую скобки "; cin.getline(s,80);
stack<char> st;
char *kind1="([{", *kind2=")]}";
for (int i=0; i<strlen(s); i++)
{ if(strchr(kind1,s[i])) st.push(s[i]);
if(strchr(kind2,s[i]))
if((st.isnull())||(strchr(kind1,st.pop())-kind1!=strchr(kind2,s[i])-kind2))
{ cout<<"Ошибка!";cin.get(); return;
}
}
if (!st.isnull()) cout<<"Ошибка!";
else cout<<"Ошибок нет";
st.makenull(); cin.get();
}
Пример 2.
Усложним задачу. Пусть имеется скобочное выражение, в котором присутствуют только круглые скобки. Требуется скобки самого глубокого уровня вложенности оставить без изменения, скобки следующего уровня заменить на квадратные, а все остальные скобки заменить на фигурные.
Листинг 2.2 Преобразование скобок
#include <iostream.h>
#include "stack.h"
#include "stack.cpp"
int main()
{ char s[80]; stack<char> st;
cout<<"Введите строку, содержащую только круглые скобки "; cin.getline(s,80);
int l; //Уровень вложеннности скобок в данный момент
for (int i=0; i<strlen(s); i++)
{ if (s[i]=='(') {st.push(i); l=1;}
if (s[i]==')')
if (st.isnull()) {cout<<"Ошибка!!!"; cin.get(); return 1;}
else {
int p=st.pop();
if (l==2) {s[p]='['; s[i]=']';}
if (l>2) {s[p]='{'; s[i]='}';}
l++;
}
}
if (!st.isnull()) cout<<"Ошибка!!!";
else cout<<"Получили:\n"<<s;
st.makenull(); cin.get();
}
2.3. Реализация очередей
2.3.2. Непрерывная реализация очереди с помощью массива
Реализация очереди при помощи массива немного сложнее, чем реализация стека. Вспомним, что вставка и удаление элементов выполняются с разных концов. Легко сделать вставку в конец массива (хвост очереди), если еще есть свободные ячейки памяти, но сложнее выполнить удаление первого элемента массива (это голова очереди). Поэтому способ реализации, при котором голова очереди совпадает с первым элементом массива, используют только в тех задачах, где элементы добавляются в очередь постепенно, а опустошение очереди происходит сразу целиком. В этом случае достаточно иметь только один дополнительный указатель — на хвост.
В том случае, когда необходимо эффективно реализовать и вставку, и удаление элементов, обычно используют два дополнительных. указателя — на начало очереди (голову) и конец очереди (хвост). Назовем указатели head и tail.В качестве таких указателей могут выступать как индексы элементов массива, так и непосредственно указатели на элементы. При реализации очереди на С++ обычно используются непосредствено указатели на голову и на хвост. Тогда вставка и удаление элементов очереди реализуется с помощью изменения значений этих указателей (рис.2.3). По аналогии со стеком, в пустой очереди эти указатели будут иметь значение NULL (причем одновременно оба, только один из них никогда не может быть нулевым).
Рис.2.3. Представление очереди при помощи массива и двух указателей (в начальный момент и спустя некоторое время)
При вставках и удалениях элементов очередь как бы передвигается по массиву, постепенно приближаясь к его границе При этом в начале массива появляется свободное пространство за счет освободившихся при удалении элементов. Как только хвост очереди достигнет верхней границы массива, следующие элементы будут добавляться уже в свободные ячейки в начале массива (рис.2.4). Такую реализацию очереди называют кольцевой или циклической.
Рис.2.4. Еще одно состояние очереди (хвост оказался ниже головы)
Определение очереди при реализации с помощью массива и двух указателей может иметь, например, следующий вид (считаем, что тип элементов type_of_data и максимальный размер массива maxlength уже определены):
struct queue
{ type_of_data data[maxlength]; // массив данных очереди
type_of_data *head,*tail; // указатели на голову и хвост
// базовые функции для работы с очередью
queue(){head=tail=NULL;}//конструктор - пустая очередь
void enqueue(type_of_data x); //добавление элемента в хвост
type_of_data gethead(); //получение элемента из головы
void dequeue();// удаление (извлечение) элемента из головы
bool isnull() { return head==NULL; } // проверка на пустоту
void makenull(){queue();} // доп. функция очистки очереди
};
Рассмотрим подробнее реализацию методов добавления и удаления элементов.
При добавлении необходимо обеспечить циклическое перемещение указателя tail по массиву data и своевременно обнаружить попытку переполнения очереди. В полностью заполненной очереди хвост находится непосредственно перед головой (это справедливо и для случая, когда первый элемент очереди совпадает с первым элементом массива, поскольку перемещение по массиву выполняется циклически). Попытка добавить еще один элемент в очередь приведет к тому, что значения указателей на хвост и голову сравняются, что будет служить признаком переполнения очереди (аварийная ситуация). Однако если перед добавлением элемента очередь была пуста, то равенство указателей на хвост и голову означает всего-навсего, что очередь состоит из одного элемента.
void queue::enqueue (type_of_data x) // вставка элемента
{ if (isnull()) // добавляем самый первый элемент
{tail=head=data;} //имя массива-указатель на первый элемент
else // очередь не пуста
{ tail++; //теперь проверим выход хвоста за границы массива
if (tail==data+maxlength)
tail=data; // поместили хвост в начало массива
if (head==tail) {cerr << "Очередь переполнена"; exit(2);}
}
*tail=x;// если все в порядке, поместили x в хвост очерели
}
Теперь будем извлекать элементы из непустой очереди. Двигаясь по масиву, голова наконец дойдет до хвоста (остался один последний элемент). Этот случай нужно отследить особо, т. к. в результате должна получиться пустая очередь.
void queue::dequeue () //извлечение элемента
{ if (isnull()) { cerr << "Очередь пуста"; exit(1); }
if (head==tail) // в очереди только один элемент
{head=tail=NULL;} // установили признак пустой очереди
else//поднимаем голову и проверяем выход за границы массива
{ head++; if (head==data+maxlength) head=data; }
}