- •Введение
- •Основные понятия и определения
- •Типы данных
- •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. Пример решения задачи поиска с использованием хеш-таблицы
Задача Иосифа (удаление из кольцевого списка)
В качестве второго примера рассмотрим классическую задачу, иллюстрирующую действия с кольцевым списком. Пусть имеется группа людей, которые встали в круг. Затем много раз произносится считалочка из K слов, отсчитывая людей по кругу, при этом каждый K-тый игрок выходит из круга. Все это продолжается до тех пор, пока в круге не останется только один человек.
Программа создает кольцевой список из n элементов, а затем удаляет из него n-1 элементов, которые по номеру являются k-ми относительно первого элемента (если число k (количество слов в считалке) превышает количество элементов, то список обходится несколько раз). На каждом шаге (после удаления очередного элемента) отображается содержимое списка, которое представляет собой индексы оставшихся в нем людей.
Листинг 2.6. Удаление из кольцевого списка (считалочка)
#include <iostream.h>
struct item
{ int index;
item *next;
};
struct list_lk // структура дл кольцевого списка
{ item *last; // указатель на последнего участника
list_lk() { last=NULL; }
void ins(int i); // вставка элемента после элемента *last
void del(int k); // удаление k-го по счету элемента
void print(); // выводим людей, оставшихся в списке
bool isnull() { return last==NULL; }
};
// реализация функций
void list_lk::ins(int i) // i - индекс (номер) участника
{ item *temp=new item;
temp->index=i;
if (!isnull()) // если список непустой
{ temp->next=last->next;
last->next=temp; last=temp;
}
else
{ temp->next=temp; last=temp; //первый и последний человек
}
}
void list_lk::del(int k)
{ if (isnull()) { cerr<<"Список пуст!"; exit(1); }
item *x=last->next, *temp=last; // 1-е слово считалки
for (int i=0; i<k-1; i++) // остальные k-1 слов считалки
{ temp=x; x=x->next;
}
temp->next=x->next; // удаляем элемент *х (ему предшествует *temp)
if (x==last) last=temp;
delete x;
}
void list_lk::print()
{ if (isnull()) { cerr<<"Список пуст!"; exit(1); }
item *temp=last->next;
do // обходим список пока снова не вернемся в его начало
{ cout<<temp->index<<" ";
temp=temp->next;
}
while (temp!=last->next);
cout<<endl;
}
int main()
{ int n,k,i; list_lk l;
cout<<"Введите число участников ";
cin>>n; cin.get();
cout<<"Введите число слов в считалке ";
cin>>k; cin.get();
for (i=0; i<n; i++) // добавляем в список n человек
l.ins(i+1);
for (i=0; i<n-1; i++) { // удаляем по считалке n-1 человек
l.del(k);
cout<<"Count "<<i+1<<": "; // выводим людей, оставшихся //в списке
l.print();
}
return 0;
}
2.6. Рекурсивная обработка линейных списков
2.6.1. Модель списка при рекурсивном подходе
Рассмотренный ранее подход к организации линейных списков ориентирован на итеративную (циклическую) обработку, при которой на каждом шаге выделяется текущий элемент списка и все действия выполняются относительно этого элемента.
Рассмотрим теперь другой подход к организации и обработке списков, основанный на систематическом применении рекурсии и не предполагающий явного выделения текущего элемента [2]. На рис.2.16 изображен линейный список, разделенный на две неравные части: "голову" (первый элемент списка) и "хвост" (все остальное).
Рис.2.16. Модель линейного списка при рекурсивном подходе к его обработке
Используя такой подход, дадим рекурсивное определение линейного списка. Линейный список —это или пустой список (не содержащий ни одного элемента) или представляет собой упорядоченную пару "голова – хвост", в которой голова есть элемент базового типа α, а хвост, в свою очередь, есть линейный список ( возможно пустой ).
Одной из распространенных форм представления определенных таким образом списков является так называемая скобочная запись, применяемая, например, в языке функционального программирования Lisp. При этом для представления упорядоченной пары "голова – хвост" используется точка как разделитель, поэтому ее часто называют точечной парой. Пустой список обозначается символом Nil.
Например, скобочная запись списка из элементов a, b, c, d типа α имеет вид ( a . ( b . ( c . ( d . Nil ) ) ) ) или в сокращенной записи ( a b c d ). Переход к сокращенной записи производится с помощью отбрасывания конструкции . Nil и удаления точки с парой скобок везде, где они встречаются. Пробелы в сокращенной записи используются для обеспечения однозначности прочтения конструкции, количество их выбирается произвольно.
Такая модель линейного списка предполагает принципиально новый набор базовых операций, отличный от набора базовых операций для модели списка с выделенным текущим элементом.
Выделим базовые операции для рекурсивной обработки списков:
функция формирования пустого списка — назовем ее Nil;
предикат IsNull (список пуст),
функция Head возвращает значение первого элемента (головы списка);
функция Tail возвращает хвост непустогос списка, т.е. список, получаемый из исходного списка после удаления из него головного элемента.
функция Cons (Construct) строит новый список из переданных ей в качестве аргументов головы и хвоста.
Здесь функция IsNull является индикатором, Head и Tail — селекторы, Cons — конструктор.
Данный набор функций является базовым в языках функционального программирования. Например, в языке Lisp функция Head имеет имя CAR, а функция Tail называется CDR (обозначение Cons такое же как и в языке Lisp). Дело в том, что автор языка LISP Джон Маккарти (США) реализовал первую LISP-систему на машине IBM 605 и использовал регистры c названиями CAR и CDR для хранения головы и хвоста списка.
Обратим внимание, что функции Head и Tail могут быть определены только для непустых списков, хотя функция Tail может возвратить и пустой список («хвост»). Функция Cons, в свою очередь, формирует только непустой список. Заметим, что список, разбитый с помощью функций Head и Tail на голову и хвост, можно восстановить с помощью функции Cons.
Определенная проблема возникает с пустым списком, который нельзя использовать как параметр функций Head и Tail, но можно использовать в качестве «хвоста» в функции Cons, которая в этом случае сформирует список из одного единственного элемента. Поэтому в любой реализации придется аккуратно отслеживать ситуацию, когда список является пустым, и уметь формировать пустой список.
Запишем формальную функциональную спецификацию списка. Обозначим список элементов типа α как List(α ), непустой список — Non_null_list( α ), пустой список обозначим Null_list (α).
0. Nil : Null_list(α);
1. IsNull : List( α ) Boolean;
2. Head : Non_null_list( α ) α;
3. Tail : Non_null_list( α ) List( α );
4. Cons : α List( α ) Non_null_list( α );
Выстроим систему аксиом для данных базовых операций. Пусть x имеет тип α, y — список элементов типа α List(α ), z — непустой список Non_null_list( α ).
A1. IsNull( Nil ) = true;
A2. IsNull( Cons( x , y ) ) = false;
A3. Head( Cons( x , y ) ) = x;
A4. Tail( Cons( x , y ) ) = y;
A5. Cons( Head( z ) , Tail( z ) ) = z.
Пустой список рассматривается здесь как значение типа List( α ), возвращаемое функцией без параметров Nil.
Все остальные операции над линейными списками выполняются при помощи соответствующей суперпозиции рекурсивных вызовов данных базовых функций.
Так, доступ к произвольному элементу списка осуществляется с помощью функций Head и Tail.
Например, если список y = (a b c d),
то Head(y) = a, Head(Tail(y)) = b,
а
Head(Tail(Tail(Tail(y)))) = d.
Понятно, что такой способ доступа к элементам сильно отличается от рассмотренного ранее передвижения по списку при помощи перемещения указателя текущего элемента. Например, при рекурсивном подходе невозможно явно выделить однонаправленные и двунаправленные списки, однако можно организовать передвижение по списку в любом направлении, используя разную последовательность рекурсивных вызовов.
Сформировать любой непустой список можно только одним способом — используя функцию Cons. Например, сформируем список из одного и трех элементов:
( a ) = ( a . Nil ) = Cons( a , Nil );
( a b c ) = ( a. ( b. ( c . Nil ) ) ) = Cons( a , Cons ( b , Cons ( c , Nil ) ) ).
Отметим, что построение каждой точечной пары в скобочной записи списка требует однократного применения конструктора Cons. При этом можно очень легко добавлять элементы в «голову» списка однократным вызовом Cons, а добавление в другие позиции требует «разборки» списка при помощи селекторов и последующей сборки при помощи конструктора. При определенном навыке использования рекурсивных вызовов функций можно легко «разбирать» и «собирать» списки, добавляя, удаляя и переставляя элементы