- •Структуры и алгоритмы обработки данных
- •Алгоритмы. Основные определения
- •От задачи к программе
- •Написание программы.
- •Типы данных, структуры данных и абстрактные типы данных
- •Указатели и курсоры
- •Время выполнения программ
- •Измерение времени выполнения программы
- •Степень (порядок) роста
- •Вычисление времени выполнения программ
- •Линейные абстрактные типы данных атд «Список»
- •Реализация списков посредством массивов
- •Реализация списков с помощью указателей
- •Атд «Стек»
- •Атд «Очередь»
- •Реализация очередей с помощью циклических массивов
- •Нелинейные абстрактные типы данных Деревья
- •Порядок узлов
- •Прямой, обратный и симметричный обходы дерева
- •Помеченные деревья и деревья выражений
- •Представление деревьев с помощью массивов
- •Специальные виды множеств атд “Дерево двоичного поиска”
- •Атд "Словарь", основанный
- •Представление ориентированных графов
- •Атд для ориентированных графов
- •Задача нахождения кратчайшего пути
- •Int **c, // массив стоимостей
- •Int *d) // массив кратчайших
- •Остовные деревья минимальной стоимости
- •Обход графов
- •Поиск в ширину
- •Поиск в глубину
- •Сортировка
- •Простые схемы сортировки Метод «пузырька»
- •Сортировка вставками
- •Быстрая сортировка
- •Пирамидальная сортировка
- •«Карманная» сортировка
- •Порядковые статистики
- •Методы разработки алгоритмов. Типы алгоритмов
- •Алгоритмы «разделяй и властвуй» (метод декомпозиции)
- •Баланс подзадач
- •Динамическое программирование
- •Перемножение нескольких матриц
- •Шаг 1: строение оптимальной расстановки скобок
- •Шаг 2: рекуррентное соотношение
- •Шаг 3: вычисление оптимальной стоимости
- •Void MatrixChainOrder(int n, // кол-во матриц
- •Int p[], // размеры матриц
- •Int **s) // оптимальное k
- •Шаг 4: построение оптимального решения
- •Int **s, // таблица, полученная
- •Int I, // индексы
- •Когда применимо динамическое программирование?
- •Оптимальность для подзадач
- •Перекрывающиеся подзадачи
- •«Жадные» алгоритмы
- •"Жадные" алгоритмы как эвристики
- •Когда применим жадный алгоритм?
- •Принцип жадного выбора
- •Оптимальность для подзадач
- •Поиск с возвратом
- •Функции выигрыша
- •Метод ветвей и границ
- •Структуры данных и алгоритмы для внешней памяти
- •Внешняя сортировка
- •Хранение данных в файлах
- •Простая организация данных
- •Хешированные файлы
- •Индексированные файлы
- •Содержание
- •Глава I. Линейные абстрактные типы данных 31
- •Глава II. Сортировка 108
Представление деревьев с помощью массивов
При этом представлении каждый элемент массива A[i] является указателем или курсором на родителя узла i. Корень дерева имеет нулевой указатель.
Д анное представление основывается на том, что каждый узел, отличный от корня, имеет только одного родителя.
Используя это представление, родителя любого узла можно найти за фиксированное время. Прохождение по пути, т.е. переход по узлам от родителя к родителю, можно выполнить за время, пропорциональное количеству узлов пути.
Для реализации оператора LEBEL можно использовать другой массив, в котором хранить метку узла i.
О
1
2
3
4
5
6
7
8
9
10
0
1
1
2
2
5
5
5
3
3
Представление деревьев с использованием списков сыновей
В этом представлении для каждого узла дерева формируется список сыновей. Эти списки можно представить любым способом. Вот как будет выглядеть наше дерево при таком представлении
П редставление левых сыновей и правых братьев (левый ребенок – правый сосед)
В каждом узле хранится:
-
указатель на родителя,
-
указатель на самого левого сына узла,
-
указатель на ближайшего справа (следующего по старшинству) брата узла.
Узел x тогда и только тогда не имеет сыновей, когда указатель
left_child(x) = NULL.
Если узел x – самый правый сын родителя, то right_sibling(x) = NULL.
Двоичные (бинарные) деревья
В компьютерных науках часто используются деревья, в которых все вершины имеют ограниченное сверху количество потомков.
Например, если это число 2, то любая вершина содержит не более двух (2,1 или 0) потомков, такие деревья называются бинарными или двоичными.
Реализация с помощью указателей на «левого ребенка» и «правого ребенка».
Коды Хаффмана
Примером применения двоичных деревьев в качестве структур данных могут служить коды Хаффмана.
Они широко используются при сжатии информации (выигрыш может составить от 20% до 90% в зависимости от типа файла). Алгоритм Хаффмана находит оптимальные коды символов исходя из частоты использования этих символов в сжимаемом тексте с помощью жадного выбора.
Предположим, в файле длиной 100 000 встречаются только латинские буквы от а до f и известны их частоты.
|
A |
B |
C |
D |
E |
F |
Количество в тысячах |
45 |
13 |
12 |
16 |
9 |
5 |
Равномерный код |
000 |
001 |
010 |
011 |
100 |
101 |
Неравномерный код |
0 |
101 |
100 |
111 |
1101 |
1100 |
Мы хотим построить двоичный код, в котором каждый символ представляется в виде конечной последовательности битов, называемой кодовым словом.
При использовании равномерного кода, в котором все кодовые слова имеют одинаковую длину, на каждый символ надо потратить как минимум 3 бита.
На весь файл уйдет 300 000 бит.
Но если часто встречающиеся символы закодировать короткими последова-тельностями битов, а редко встречающиеся – длинными, то можно построить более экономный неравномерный код.
В приведенном примере неравномерного кода на весь файл уйдет 224 000 бит, что дает около 25% экономии.
Будем раcсматривать только коды, в которых из двух последовательностей битов, представляющих различные символы, ни одна не является префиксом другой.
Такие коды называются префиксными кодами хотя логичнее было бы называть их «безпрефиксными». Можно показать, что для любого кода, обеспечивающего однозначное восстановление информации, существует не худший его префиксный код. Поэтому, ограничившись префиксными кодами, мы ничего не теряем.
При кодировании каждый символ заменяется на свой код. Например, строка ABC запишется как 0101100. Для префиксного кода декодирование однозначно и выполняется слева направо.
Для эффективной реализации декодирования надо хранить информацию о коде в удобной форме.
Одна из возможностей – представить код в виде двоичного дерева, листья которого соответствуют кодируемым символам. При этом путь от вершины дерева до кодируемого символа определяет кодирующую последовательность битов: поворот налево дает 0, а поворот направо – 1.
Оптимальному коду (для данного файла) всегда соответствует двоичное дерево, в котором всякая вершина, не являющаяся листом, имеет двоих детей. Такое свойство позволяет легко доказать, что дерево оптимального префиксного кода для файла, в котором используются все символы из некоторого множества С и только они, содержит только |С| листьев и ровно |C| –1 узлов, не являющихся листьями.
Хаффман построил жадный алгоритм, который строит оптимальный префиксный код, который называют кодом Хаффмана.
Алгоритм строит дерево Т, соответствующее оптимальному коду, снизу вверх, начиная с множества из |С| листьев и делая |С| –1 “слияний”. Предполагаем, что для каждого символа из С задана его частота f[c]. Для нахождения двух объектов, подлежащих слиянию, может быть использована очередь с приоритетами Q, использующая частоты f в качестве рангов – сливаются два объекта с наименьшими частотами. В результате слияния получается новый объект, частота которого считается равной сумме частот двух сливаемых объектов.
Пример применения алгоритма Хаффмана.
F:5 E:9 C:12 B:13 D:16 A:45