- •Введение
- •Основные понятия и определения
- •Типы данных
- •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. Пример решения задачи поиска с использованием хеш-таблицы
Алгоритмы вставки и удаления
Для обеспечения сбалансированности АВЛ-дерева придется существенно усложнить функции добавления и удаления элементов. Сейчас наша задача состоит в том, чтобы проанализировать усложненные алгоритмы вставки и удаления и убедиться, что их несколько увеличившаяся временная сложность все же не превысила логарифмическую.
Очевидно, что баланс каждого узла удобно хранить в самом узле вместе с другими данными, чтобы можно было легко получать и корректировать его значение при выполнении любых действий (вставок, удалений и вращений). В принципе, можно было бы завести и отдельный массив для хранения балансов, но большого смысла в этом нет.
Общий алгоритм вставок и удалений состоит из двух шагов.
Выполнить вставку или удаление элемента в соответствии с алгоритмом для обычного бинарного дерева. При включении нового узла как листа определить для него нулевой показатель сбалансированности.
Пройти обратно до корня по тому же самому пути поиска, по которому только что пришли, при этом проверить и пересчитать баланс каждого узла и выполнить нужное вращение, если этот показатель принял недопустимое значение.
Как видим, при вставке и удалении придется два раза выполнить перемещения по пути поиска (сначала вперед, потом обратно), но при этом мы не потеряли логарифмической сложности от количества узлов, поскольку путь поиска не превышает высоты АВЛ-дерева, которая все время поддерживается на своем минимальном уровне,
Таким образом, в АВЛ-дереве операции поиска, вставки и удаления имеют логарифмическую сложность от количества узлов дерева независимо от исходных данных.
Как известно, при вставке и удалении элементов бинарного дерева поиска можно применять рекурсивный и нерекурсивный алгоритмы. Оба варианта можно использовать и при реализации АВЛ-дерева, но, пожалуй, легче модифицировать рекурсивные алгоритмы, поскольку пройденный путь поиска все равно нужно запоминать, чтобы именно по нему вернуться обратно, выполняя балансировку. В нерекурсивном варианте придется использовать дополнительный стек, а в рекурсивном варианте этот путь уже запоминается в системном стеке программы, так что останется только им воспользоваться.
В качестве примера приведем реализацию рекурсивного алгоритма вставки в АВЛ-дерево.
Реализация рекурсивного алгоритма вставки в авл-дерево
Дополним уже имеющуюся структуру узла бинарного дерева поиска еще одним полем (назовем его balance), которое будет служить для хранения баланса узла. Вообще-то для хранения баланса узла достаточно всего двух бит, но в реализации на С++ определим для него тип short.
Для реализации алгоритма вставки нам потребуются вспомогательные функции. Это четыре функции , соответствующие большим и малым правым и левым вращениями, и еще одна функция c именем rotate, в которой будет приниматься решение, какое именно из вращений нужно выполнить.
Алгоритм функции rotate основан на анализе балансов. Если значение этого показателя для узла положительно, значит, перевешивает правое поддерево и нужно выполнять правое вращение, и наоборот, при отрицательном значении нужно выполнять левое вращение. Решение о том, большое или малое вращение нужно выполнить, можно принять на основе рисунков 5.8 и 5.11.
Функции вращения потребуют большой аккуратности, но в основном они сводятся к изменению нескольких указателей. Следует иметь в виду, что после преобразований нужно изменить значения балансов для тех узлов, которые участвовали во вращении.
При наличии данных вспомогательных функций сама функция вставки элемента будет реализовывать общую логику рекурсивной вставки в бинарное дерево поиска. При этом после каждого рекурсивного вызова функции вставки в левое или правое поддерево узла будем корректировать баланс этого узла. Если вызывалась функция вставки для правого поддерева, изменение высоты этого поддерева нужно прибавить к показателю сбалансированности узла, а если для левого— вычесть. Если при этом получим значения 2 или -2 , вызываем функцию вращения.
Для реализации этого алгоритма добавим еще один параметр в функцию вставки — изменение высоты поддерева (в листинге это параметр d). При добавлении узла этот параметр будет принимать значение 1 (узел всегда добавляется в пустое поддерево), а при вращениях, наоборот, высота поддерева уменьшается на 1 (т.е. d=-1).
Реализация рекурсивного алгоритма вставки в АВЛ-дерево приводится в листинге 5.4. Кроме функции вставки и всех необходимых вспомогательных функций, приводится функция построения АВЛ-дерева из заданной последовательности и функция нисходящего обхода, которая строит левое скобочное представление полученного дерева (этого вполне достаточно, чтобы убедиться в том, что построенное дерево является сбалансированным).
Листинг 5. Вставка в АВЛ-дерево с восстановлением сбалансированности
#include <iostream.h>
#include <stdlib.h>
typedef int T_key; //тип ключа, может быть любым
typedef char T_data;//тип связанных данных, любой
struct item //структура элемента
{ T_key key; //ключ
T_data data; //связанные данные
};
struct node // структура узла дерева
{item data; //данные типа item
node *left, *right; // указатели на детей
short balance; // показатель сбалансированности
node(item x) // конструктор, вызывается при создании узла
{data=x;left=right=NULL;balance=0;}
};
typedef node* avlbst; //avlbst - avl binary seach tree
//Малое правое вращение:
void SmallRightRotate(avlbst root)
{ avlbst x,y; item t;
x=root; y=x->right;
t=x->data; x->data=y->data; y->data=t;
x->right=y->right;
y->right=y->left; y->left=x->left;
x->left=y;
x->balance= y->balance=0; //изменяем balance для x и y
}
//Большое правое вращение:
void LargeRightRotate(avlbst root)
{ avlbst x,y,z; item t;
x=root; y=x->right; z=y->left;
t=x->data;x->data=z->data; z->data=t;
y->left=z->right;
z->right=z->left; z->left=x->left;
x->left=z;
x->balance=0;
if (z->balance==0) y->balance=z->balance=0;
else
if (z->balance==-1) { y->balance=0; z->balance=1;}
else {z->balance=0; y->balance=-1;}
}
//Малое левое вращение (аналогично малому правому):
void SmallLeftRotate(avlbst root)
{ avlbst x,y; item t;
x=root; y=x->left;
t=x->data; x->data=y->data; y->data=t;
x->left=y->left;
y->left=y->right; y->right=x->right;
x->right=y;
x->balance=y->balance=0;
}
//Большое левое вращение (аналогично большому правому):
void LargeLeftRotate(avlbst root)
{ avlbst x,y,z; item t;
x=root; y=x->left; z=y->right;
t=x->data;x->data=z->data; z->data=t;
y->right=z->left;
z->left=z->right; z->right=x->right;
x->right=z;
x->balance=0;
if (z->balance==0)
y->balance=z->balance=0;
else
if (z->balance==-1)
{ y->balance=0; z->balance=1;}
else
{ z->balance=0; y->balance=-1;}
}
// функция определяет, какое именно вращение нужно
void rotate(avlbst root)
{ if (root->balance==2) //Правое вращение
if (root->right->balance<0) LargeRightRotate(root);
else SmallRightRotate(root);
else //Левое вращение
if (root->left->balance>0) LargeLeftRotate(root);
else SmallLeftRotate(root);
}
bool insertavl_rec(avlbst &root, item x, int &d)
{ //параметр d - изменение высоты текущего поддерева
if (!root) // дерево пусто – терминальная ветвь
{ root=new node(x); d=1;//создали узел-высота увеличилась на 1
if(root)return true; else return false;
}
// рекурсивная ветвь
if (x.key<root->data.key) // нужно двигаться влево
{ insertavl_rec(root->left,x,d); // вставка в левое поддерево
root->balance=root->balance-d; //корректируем balance у отца
if (abs(root->balance)==2) //если отец (root) разбалансирован
{ rotate(root); d--;}// вызываем нужное вращение
} // оно уменьшило высоту дерева
else // нужно двигаться вправо
{ insertavl_rec(root->right,x,d); // вставка в правое поддерево
root->balance=root->balance+d; //пришли справа, d прибавляется
if (abs(root->balance)==2) // при разбалансировке
{ rotate(root); d--;} // выполняем вращение
}
}
// функция-тест строит дерево из примера (рис.5.13)
void randtree(avlbst &root)
{ item x; int a[7]={4,5,7,2,1,3,6}; int d=0;
for (int i=0;i<7; i++)
{ x.key=a[i];
x.data=rand()%26+65; // случайные заглавные латинские буквы
insertavl_rec(root,x,d);
}
}
// вывод дерева в КЛП порядке (для проверки сбалансированности)
void out(avlbst root)
{if (!root) return;
cout<<root->data.key<<' '<<root->data.data<<"; ";
cout<<'('; out(root->left); cout<<")(";
out(root->right);cout<<')';
}