- •Введение
- •Основные понятия и определения
- •Типы данных
- •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. Пример решения задачи поиска с использованием хеш-таблицы
2.2.3. Примеры программ с использованием стеков
Стеки широко используются при разработке трансляторов, например, при анализе и вычислении арифметических выражений. Эта задача является предметом отдельного курса, но в качестве введения в проблему приведем компактные примеры анализа и преобразования скобочного выражения.
Пример 1.
Приведенная программа проверяет правильность расстановки скобок в заданной строке текста, длина которой в принципе не ограничена. При этом просматриваются все символы скобок: круглых, квадратных и фигурных. Учитывается, что различные скобки могут быть вложены одна в другую. Список из открывающихся скобок строится по принципу стека. Это значит, что та скобка, которая была помещена в стек последней, будет извлечена из него первой, и легко проверить, соответствует ли закрывающая скобка своей открывающей. Все остальные символы программа игнорирует.
Для проверки соответствия скобок используются две вспомогательные строки с открывающимися и закрывающимися скобками. Стандартная функция strchr() возвращает указатель на найденный символ, поэтому, чтобы найти номер данного символа, приходится вычитать из этого указателя указатель на начало строки. Все остальное просто.
Листинг 2.8 Проверка правильности расстановки всех видов скобок в строке
#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.9 Преобразование скобок
#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).
Рис.2.3. Представление очереди при помощи массива и двух указателей (в начальный момент и спустя некоторое время)
При вставках и удалениях элементов очередь как бы передвигается по массиву, постепенно приближаясь к его границе При этом в начале массива появляется свободное пространство за счет освободившихся при удалении элементов. Как только хвост очереди достигнет верхней границы массива, следующие элементы будут добавляться уже в свободные ячейки в начале массива (рис.2.4). Такую реализацию очереди называют кольцевой или циклической.
Рис.2.4. Еще одно состояние очереди
Продолжаем заполнять очередь, изображенную на рис. 2.4. В какой-то момент все элементы массива могут оказаться заполнены, в этом случае хвост будет находиться непосредственно перед головой. Теперь будем извлекать элементы один за другим. Двигаясь по масиву, голова наконец дойдет до хвоста (остался один последний элемент). Когда и он будет извлечен, голова опять окажется перед хвостом, точно так же как и в случае полностью заполненной очереди.
Отсюда вывод: по значениям этих двух указателей нельзя определить, пуста очередь или полностью заполнена.
Можно предложить различные способы решения данной проблемы, например:
ввести дополнительную логическую переменную, которая показывает, пуста ли очередь, или переменную, в которой хранится количество элементов очереди;
хранить количество элементов очереди вместо указателя на хвост очереди (индекса хвоста), но тогда его придется вычислять при каждой вставке. Такой способ реализации предлагается в [8].
В любом случае нельзя допускать переполнения очереди, так же как и нельзя извлекать элементы, если очередь пуста, поэтому при реализации операций вставки и удаления необходимо предусмотреть обработку соответствующих аварийных ситуаций.
При реализации очереди на С++ обычно используются непосредствено указатели на голову и на хвост. При этом признаком пустой очереди можно считать нулевое значение этих указателей, не вводя дополнительной переменной. По этим признакам можно отследить аварийную ситуацию при попытке извлечь элемент из пустой очереди. Ситуация, связанная с переполнением очереди, также легко отслеживается, поскольку известно максимальное число элементов очереди maxlength.
Листинг 2.3 содержит циклическую реализацию очереди с помощью массива.
Листинг 2.3. Реализация структуры queue с помощью массива
#include <stdlib.h>
#include <iostream.h>
typedef int type_of_data; // тип элементов (может быть любым)
const maxlength=100;
struct queue
{ type_of_data q[maxlength]; // массив данных очереди
type_of_data *head,*tail; // указатели на голову и хвост
queue() { head=tail=NULL; } // конструктор
// прототипы функций для работы с очередью
void enqueue(type_of_data x); //добавление элемента в хвост очереди
type_of_data dequeue();// извлечение элемента из головы очереди
bool isnull() { return head==NULL; } //проверка на пустоту
void makenull(){ head=tail=NULL; }// очистка очереди (создание пустой очереди)
};
// реализация функций для работы с очередью
void queue::enqueue (type_of_data x) //вставка
{ if (isnull())
{ tail=q; head=q; } // в языке С++ имя массива - указатель на его первый элемент
else
{ tail++; if (tail>q+maxlength-1) tail-=maxlength; // добавили элемент в хвост
if (head==tail) { cerr << "Очередь переполнена\n"; exit(2); }
}
*tail=x;
}
type_of_data queue::dequeue () //извлечение
{ if (isnull()) { cerr << "Очередь пуста\n"; exit(1); }
int x=*head;
if (head==tail) { head=NULL; tail=NULL; }
else { head++; if (head>q+maxlength-1) head-=maxlength; }
return x;
}