- •Введение
- •Основные понятия и определения
- •Типы данных
- •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. Пример решения задачи поиска с использованием хеш-таблицы
5.6.2. Метод цепочек
Наиболее простой и естественный способ разрешения коллизий состоит в том, что все элементы с одинаковыми хеш-адресами объединяются в связный список (хеш-цепочку). Тогда хеш-таблица представляет собой массив из N указателей на хеш-цепочки. Такая структура показана на рис. 521.
Рис.5.21. Хеш –таблица при использовании метода цепочек
В этом случае в наихудшем случае время поиска пропорционально размеру хеш-цепочки (списка), поскольку нужную хеш-цепочку можно найти с константной сложностью, а для того, чтобы найти элемент в цепочке, придется двигаться по ней последовательно. Операции вставки и удаления элементов выполняются за константное время как в обычном связном списке, обычно их совмещают с операцией поиска, чтобы лишний раз не вычислять хеш-функцию.
Если исходное множество данных состоит из K элементов, а размер массива указателей N, то средняя длина списков будет K/N элементов. Если можно оценить среднее значение K, то можно выбрать N так, чтобы в каждом списке было всего несколько элементов. Тогда время выполнения всех операций будет малой постоянной величиной. Если размер исходного множества непредсказуем или динамично изменяется, данный метод применять не рекомендуется.
Реализация хеш-таблицы с использованием метода цепочек довольно проста. Сами цепочки обычно реализуются в виде однонаправленных связных списков, для хранения указателей на цепочки используется обычный массив. Следуя принятым в данной главе соглашениям, можно записать следующие определения структур данных:
typedef int T_key; //тип ключа, может быть любым
typedef char T_data;//тип связанных данных, любой
struct item //структура элемента данных – ключ и связанные данные
{ T_key key; //ключ
T_data data; //связанные данные
};
struct h_item //структура элемента хеш-цепочки
{ item data; // элемент данных
h_item *next; //ссылка на следующий элемент
}
h_item *h_table[размер массива указателей] //массив указателей на хеш-цепочки
Пример реализации хеш-таблицы для решения конкретной задачи будет приведен ниже.
5.6.3. Хеширование с открытой адресацией
Если в памяти имеется непрерывная область достаточных размеров, то в этом случае можно вообще отказаться от ссылок при реализации хеш-таблицы. Такой способ реализация хеш-таблицы называется хешированием с открытой адресацией [9, 14]. В [3] такая хеш-таблица называется закрытой, очевидно, имеется в виду, что она закрыта для расширения. Этот метод накладывает еще более жесткие ограничения на размер входных данных, чем метод цепочек, но для случая статических входных данных он вполне годится.
Коллизии в этом случае разрешаются следующим образом. В случае, если вычисленный по ключу хеш-адрес оказывается занятым, каким-либо способом находится другая незанятая позиция, куда и помещается новый элемент. Если все позиции заняты, то элемент вставить нельзя (место кончилось). Этот процесс поиска подходящей позиции называется исследованием хеш-таблицы [14], а количество позиций, просмотренных до того, как найдена подходящая позиция, называют количеством проб.
Наиболее простым способом разрешения коллизий является линейное зондирование. При линейном зондировании hi(x)=(h(x)+i) mod N. Предположим, N=8, ключи a,b,c,d имеют хеш-значения h(a)=3, h(b)=0, h(c)=4, h(d)=3. Например, если мы хотим вставить элемент d, а сегмент 3 уже занят, то мы проверим 4-й сегмент, если и он занят, то 5-й, 6-й, 7-й, 0-й, 1-й, 2-й.
Пусть сначала вся хеш-таблица пуста. Поместим в неё последовательно элементы a, b, c, d. Элемент a попадёт в 3-й сегмент, b – в 0-й, c – в 4-й. При вставке элемента d оказывается, что 3-й элемент уже занят. Проверяем 4-й элемент, но он тоже занят. Пятый элемент свободен – туда и помещаем d.
0 |
B |
1 |
пусто |
2 |
пусто |
3 |
A |
4 |
C |
5 |
D |
6 |
пусто |
7 |
пусто |
Посмотрим, как выполняется поиск элемента x. Будем сначала считать, что элементы из хеш-таблицы никогда не удаляются. Тогда при поиске элемента x необходимо просмотреть всю последовательность, начиная с вычисленного хеш-адреса, пока не будет найден x, не встретится пустая позиция, или не будут просмотрены все позиции последовательности. Легко объяснить, почему при достижении пустого элемента поиск можно прекратить – ведь при вставке элемент вставляется в первый пустой сегмент, следовательно, далее элемент находиться не может.
Но если элементы из хеш-таблицы всё-таки удаляются, то при достижении пустого элемента мы уже не можем прекратить поиск, так как возможно, что искомый элемент находится в одной из следующих позиций последовательности. Для повышения скорости поиска иногда используется следующий приём – при удалении элемента его позиция помечается специальным образом, так чтобы ее можно было отличить от изначально пустой позиции. При выполнении вставки такие позиции рассматриваются как свободные.
Вернёмся к вышеприведённому примеру. Пусть нам нужно проверить, содержится ли в множестве элемент e, где h(e)=4. Проверяем сегменты 4,5 и 6. Сегмент 6 пустой, следовательно, элемента e в множестве нет.
Предположим теперь, что мы удалили элемент c и проверяем, содержится ли в множестве элемент d:
0 |
B |
1 |
пусто |
2 |
пусто |
3 |
A |
4 |
удален |
5 |
D |
6 |
пусто |
7 |
пусто |
Мы проверяем элемент 3, затем переходим к элементу 4. Он помечен как удаленный, поэтому не останавливаемся в нём и переходим к элементу 5, где и находим D.
Рассмотренное нами линейное зондирование – далеко не самый лучший способ разрешения коллизий. Как только несколько последовательных элементов будут заполнены (образуя группу), любой новый элемент при попытке вставки в эти позиции будет вставлен в конец группы, увеличивая её длину. Отсюда следует, что при таком расположении элементов увеличивается время выполнения операций поиска, вставки, удаления элемента.
Имеются методы организации хеширования с открытой адресацией, обеспечивающие в среднем меньшее количество проб, с ними можно познакомиться, например, в [10, 14].