- •Введение
- •Основные понятия и определения
- •Типы данных
- •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. Пример решения задачи поиска с использованием хеш-таблицы
3.7.2. Рекурсивные функции обхода бинарных деревьев
Первые три порядка обхода, которые приводятся в таблице 3.5, легко реализуются рекурсиным вызовом функции обхода для левого и правого поддеревьев в том порядке, которого требует алгоритм. Требуется только пояснить шаг «посетить корень». Поскольку обходы деревьев могут потребоваться для любой обработки значений, находящихся в узлах, то будем считать, что этот шаг обозначает именно ту обработку, которая нужна для конкретной задачи.
Функции имеют очень компактный код, который приведен в листинге 3.4, а для простоты предполагается, что вся обработка состоит только в выводе на экран значения, которое хранится в узле. Для того, чтобы сделать данные функции универсальными, можно добавить еще один параметр— внешнюю функцию Visit, в которой будет сосредоточена вся обработка узла.
Листинг 3.4. Реализация рекурсивных обходов бинарного дерева
// Дополнение к файлу bintree.cpp
void forward(bt t)//прямой порядок обхода
{ if(t) {cout<<root(t); forward(left(t)); forward(right(t));}
}
void reverse(bt t)// обратный порядок обхода
{ if(t) {reverse (left(t));reverse(right(t));cout << root(t);}
}
void central(bt t)// центрированный порядок обхода
{ if (t) {central(left(t)); cout << root(t); central(right(t));}
}
Заметим, что обход в ширину не имеет простой рекурсивной реализации. Ниже мы приведем алгоритм обхода в ширину с использованием вспомогательной структуры данных — очереди.
3.7.3. Нерекурсивные функции обхода бинарных деревьев
Учитывая важность эффективной реализации обходов бинарных деревьев, рассмотрим нерекурсивный способ обходов, использующий цикл обхода по дереву. Вспомним, что рекурсивные функции используют внутренний (системный) стек программы, в который при каждом рекурсивном вызове помещается параметр (в данном случае указатель на узел) и адрес возврата. Следовательно, для реализации нерекурсивных алгоритмов логично использовать вспомогательный (внешний) стек, которая будет использоваться для тех же целей, что и системный стек программы в рекурсивных способах.
Введем вспомогательный стек s. При использовании языков высокого уровня мы не сможем в точности воспроизвести в нем содержимое системного стека, да это и не нужно. В литературе [8,10,14] приводятся различные алгоритмы нерекурсивного обхода, в которых вспомогательный стек используется только для хранения указателей на узлы дерева. Различные способы обхода различаются порядком, в котором эти указатели заталкиваются в стек и извлекаются из него.
Предположим, что уже имеется реализация шаблона стека с типом данных bt — указатель на узел бинарного дерева (для этого можно воспользоваться реализацией типа stack, которая была рассмотрена в предыдущей главе). Напомним базовые функции (методы) структуры stack:
push(p) – заталкиваем p в стек
pop() – извлекаем элемент с вершины стека
isnull() – проверяем стек на пустоту.
Наиболее прост в реализации алгоритм прямого обхода, который приводится в [14]. Напомним, что любая функция обхода получает в качестве параметра корень дерева. Поместим его в стек. Затем на каждом шаге итерации сначала извлекаем элемент с верхушки стека. При прямом обходе корень обрабатывается первым, после чего указатель на корень уже не нужен, а в стек помещается сначала указатель на правое поддерево, а затем на левое (пустые указатели в стек не помещаются). На следующем шаге итерации из стека извлекается и обрабатывается именно корень левого поддерева (он на вершине), после чего в стек заталкиваются указатели на его правое и левое поддеревья. Цикл повторяется до тех пор, пока стек не опустеет (это произойдет после извлечения крайнего правого листа). В листинге 3.5. приведена функция прямого обхода.
Листинг 3.5. Нерекурсивная реализация прямого обхода
void forwardstack(bt t) // нерекурсивный обход в прямом порядке
{ stack<bt> s; // структура stack должна быть реализована!
s.push(t); bt p;
while (!s.isnull())
{ p=s.pop();
cout << root(p)<<" "; //любая обработка узла
if(right(p)) s.push(right(p));
if(left(p)) s.push(left(p));
}
}
Проанализируем выполнение данной функции, поскольку алгоритм достаточно универсален и его можно приспособить и для некоторых других видов обхода. Пусть функция применяется к дереву из семи узлов, которое изображено на рис.3.9. Тогда будет выполнено семь шагов цикла (итераций), а последовательность извлекаемых элементов и содержимое стека в начале каждого шага представлены в табл. 3.6. На каждом шаге итерации в стек добавляются поддеревья очередного узла (1,2 или 0 в зависимости от их количества) и извлекается ровно один узел с вершины.
Таблица 3.6
Содержимое стека при прямом порядке обхода
|
№ итерации |
Извлекаемый узел |
Содержимое стека |
1 |
a |
a |
|
2 |
b |
bc |
|
3 |
d |
dec |
|
4 |
e |
ec |
|
5 |
g |
gc |
|
6 |
c |
c |
|
7 |
f |
f |
Алгоритмы центрированного и обратного обходов несколько сложнее, поскольку в этом случае корень каждого поддерева нельзя обрабатывать сразу, поэтому придется поместить его в стек и двигаться дальше по левой ветви, помещая в стек все узлы до первого листа. В [7] приводится подобный алгоритм для выполнения центрированного обхода (листинг 3.6).
Листинг 3.6. Нерекурсивная реализация центрированного обхода
void centralstack(bt t) // в центрированном порядке
{ stack<bt> s; bt p=t;
do // помещаем в стек узел и переходим к левому сыну
{ if (p) { s.push(p); p=left(p);}
else
// дошли до левого листа, начинаем извлекать узлы
{if (!s.isnull())p=s.pop(); else return;
cout << root(p) <<" "; // обработали корень
p=right(p); //правое поддерево проходим после корня
}
}
while (true);
В табл. 3.7 представлено содержимое стека перед каждым извлечением очередного элемента. В данном алгоритме обход дерева из семи узлов выполняется за 14 итераций, поскольку на каждой итерации в стек заносится или из него извлекается один узел.
Таблица 3.7
.Содержимое стека при центрированном порядке обхода
|
№ итерации |
Извлекаемый узел |
Содержимое стека |
4 |
d |
dba |
|
5 |
d |
ba |
|
8 |
g |
gea |
|
9 |
e |
ea |
|
10 |
a |
a |
|
12 |
c |
c |
|
14 |
f |
f |
Алгоритм обратного обхода на основе вспомогательного стека s реализовать сложнее, чем два других способа. Однако в этом случае можно применить небольшую хитрость, вспомнив, что слово «обратный» означает противоположный порядок по отношению к прямому. Поэтому можно использовать приведенный выше алгоритм прямого обхода, модифицировав его следующим образом.
Вместо обработки узла (корня поддерева) будем помещать его в еще один, дополнительный стек (назовем его стеком вывода sout). Тогда на заключительном этапе алгоритма элементы дополнительного стека будут обработаны в порядке, обратном тому, в котором они поступили в стек.
Поскольку обратный порядок означает ЛПК, а не ПЛК (правое-левое-корень — такой порядок действительно обрабатывал бы узлы в противоположном по отношение к прямому порядке), то внесем еще одно изменение — сначала будем помещать в стек указатель на левого сына, а затем на правого.
Листинг 3.7. Нерекурсивная реализация обратного обхода
void reversestack(bt t)// нерекурсивный обход в обратном порядке
// используем дополнительный стек для вывода в обратном порядке
{ stack<bt> s,sout;
s.push(t); bt p;
while (!s.isnull())
{ p=s.pop();
sout.push(p);//вместо обработки узла помещаем его в стек
if(left(p)) s.push(left(p));
if(right(p)) s.push(right(p));
}
while (!sout.isnull()) // обработка узлов в обратном порядке
{p=sout.pop(); cout << root(p)<<" ";}
}
Интересно, что этот же самый алгоритм можно применить и для реализации обхода в ширину, также выполнив две модификации.
Вместо стека используем очередь, которая как раз подходит для этих целей, поскольку при продвижении от корня к листьям в нее будут попадать все более удаленные от корня элементы. При этом, чем раньше узел попал в очередь, тем раньше он будет обработан.
Вторая модификация такая же, как и для обратного обхода — сначала помещаем в очередь левого сына, а после него правого. Это тоже понятно, поскольку узлы обходятся слева направо.
Предполагаем, что используется структура queue, реализованная в предыдущей главе. Напомним основные операции:
enqueue, dequeue – помещение элемента в очередь и удаление из очереди;
isnull – проверка на пустоту
Листинг 3.8. Реализация обхода бинарного дерева в ширину
void widthstack(bt t) // обход в ширину
{ queue<bt> q; // структура queue должна быть реализована!
q.enqueue(t); bt p;
while (!q.isnull())
{ p=q.dequeue();
cout << root(p)<<" ";
if(left(p)) q.enqueue (left(p));
if(right(p)) q.enqueue (right(p));
}
}
Все приведенные в данном разделе функции обхода в конце работы оставляют вспомогательные структуры пустыми, т. е. не требуют никакой дополнительной «уборки» памяти.