Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ukhanov_-_Struktury_i_algoritmy_obrabotki_danny...doc
Скачиваний:
0
Добавлен:
01.05.2025
Размер:
1.81 Mб
Скачать

Деревья

Деревом называется множество из N узлов, в котором выделен один узел, называемый корнем, а остальные узлы разбиты на попарно непересекающиеся множества, каждое из которых является деревом.

Пример:


I уровень

II уровень

Принята следующая терминология.

а – отец узлов b, c, d.

b, c, d – сыновья а.

b, c, d – братья.

e, f – потомки а.

Узел, не имеющий сыновей, называется листом. Дерево называется упорядоченным, если порядок следования сыновей существенен. В упорядоченном дереве левый брат считается старшим. Число уровней в дереве определяет его высоту. Число сыновей узла – его степень.

Лес – это множество, может быть пустое, состоящее из некоторого числа непересекающихся деревьев.

Примерами древовидной структуры являются генеалогические деревья, оглавленья.

Существует два способа представления дерева.

Первый способ:

typedef struct U {

... info;


//поле данных узла

//степень узла

//массив указателей на сыновей

//указатель на отца.


int nson;

struct U *son [MAXSON];

struct U *father; } UZEL;

Указатель на отца может и отсутствовать. Второй способ: typedef struct U {

... info;

struct B *first; //указатель на указатель на первого сына} MAIN_UZEL;

Структура узлов вспомогательного списка: typedef struct B {

MAIN_UZEL *U; //указатель на узел дерева

struct B *next; //указатель на следующий узел списка.

} VSP;

VSP

M AINUZEL

Бинарные деревья


е) lf

Деревья

Бинарные деревья состоят из корня и двух поддеревьев, которые являются в свою очередь являются бинарными деревьями. Эти деревья могут быть пустыми. Одно из поддеревьев называется левым, другое правым.

различны, так как в первом случае пусто правое поддерево, а во втором - левое.

Пример:

Арифметическое выражение можно представить с помощью бинарного дерева.

(а + Ь) / (с - d)

Для представления бинарного дерева достаточно иметь две связи в каждом узле и указатель на дерево.

Узел бинарного дерева представляется следующей структурой. typedef struct R { ... info;

struct R *llink; struct R *rlink; } NODE;

Для работы с деревом требуется некоторым способом просматривать узлы дерева, т.е. совершать обход дерева. Обойти дерево – это значит посетить каждый узел дерева некоторым систематическим образом ровно один раз. Существует несколько вариантов обхода бинарного дерева, определяемых рекурсивно.

• Прямой порядок. Сначала корень, затем левое поддерево, далее правое.

2) ( 3 • Обратный порядок. Сначала левое поддерево, затем корень, далее правое.

1 ) (3 • Концевой порядок. Сначала левое поддерево, затем правое, далее корень.

1 ) (2 Пример:

При использовании прямого обхода получим последовательность: a b d e f g c p q s r l. Обратный обход: d f e g b a c s q r p l. Концевой обход: f g e d b s r q l p c a.

Рассмотрим функции обхода. Поскольку алгоритм обхода рекурсивный, то и здесь используется рекурсия. Прямой. void TreeD (NODE *root) {

if (root = = NULL) return;

обработка (root);

TreeD (root -> llink);

TreeD (root -> rlink); }

Обратный. void TreeO (NODE *root) {

if (root = = NULL) return;

TreeD (root -> llink);

обработка (root);

TreeD (root -> rlink); }

Концевой. void TreeK (NODE *root) {

if (root = = NULL) return; TreeD (root -> llink); TreeD (root -> rlink); обработка (root); }

Рекурсия может быть реализована с использованием механизма стека. void TreeO (NODE *root) { NODE *a; NODE *stack [MAXSTACK]; //массив указателей на узлы

//история спуска по дереву вдоль левых ветвей int v = -1; //указатель на вершину стека

а = root; for (;;) { while (а != NULL){ //спускаемся вниз по левым связям.

stack [ ++v] = а; а = а -> llink; }

if (v < 0) return; //дерево пройдено полностью

а = stack [v -]; обработка (а); а = а -> rlink; } }

Вставка узла в дерево не вызывает сложностей. Для этого нужно только правильно направить связи.

Удаление узла из бинарного дерева должно происходить так, чтобы порядок следования узлов в обходе дерева не был нарушен. Легко удаляется лист или узел с пустой левой или правой связью. Удаление узла с непустыми связями может проходить по такому алгоритму. Найти предшественника или последователя в обратном порядке с пустыми связями, удалить его, а затем вернуть на место узла, который на самом деле следовало удалить. Пример:

Рассмотрим некоторую последовательность: 5 3 7 2 4 8 6 1 Будем строить дерево по принципу - если меньше, то влево. Получим такое дерево.

Тогда обратный обход даст сортированную последовательность: 1 2 3 4 5 6 7 8. Если дерево хорошо организовано, то его длина ~ log2N, N число узлов в дереве.

Прошитые деревья

Если в дереве с N узлами имеется N-1 непустая связь, то N+1 связь пуста, ибо общее число связей 2N. Таким образом, число пустых связей велико. Поля, занятые пустыми связями, можно занять другой полезной информацией, которая упрощает прохождение дерева. Вместо пустых

связей поместим указатели на некоторые другие узлы дерева. Эти связи будем называть “нитями” в отличие от основных. Чтобы отличать основные связи от нитей, заведем в каждом узле два однобитовых поля l и r, принимающих значения 1, если связь является нитью и 0, если связь является основной.

Обозначим *P и P* – соответственно указатели на предшественника и последователя узла P при обходе в обратном порядке.

Сопоставим обычное дерево и дерево, прошитое для обхода в обратном порядке:

обычное дерево

прошитое дерево для обратного обхода

правая связь

левая связь

правая связь

левая связь

Р -» rlink = Q

P ^ llink = NULL

P -» rlink = Q, r = 0

P -» llink = *P, 1=1

P -» rlink = NULL

P -» llink = Q

P -> rlink = P*, r = 1

P -» llink = Q, 1 = 0

Здесь вместо пустой связи будем хранить указатель на предшественника или последователя узла при обходе в обратном порядке.

Тогда прим обратном обходе получим последовательность: q e l d b a r s t f c g. Пунктирными линиями изображены связи-нити.

Если дерево прошито, то легко можно определить последователя для данного узла. Структура прошитого дерева: typedef struct А { ... info; char г, 1; struct A *llink; struct A *rlink; } UZEL;

Большое преимущество прошитых деревьев состоит в том, что упрощаются алгоритмы их прохождения. Сначала напишем функцию, возвращающую указатель на последователя в обратном порядке обхода.

UZEL* Next (UZEL *р) { if (p-»r) { р = р -> rlink; while (р -> 1) р = р -> llink; return р; } else {

return р -> rlink; } } Тогда обход дерева будет выглядеть очень просто.

найти узел P

while (P != NULL) { обработка (P); P = next (P);

}

Отметим, что алгоритм больше не является рекурсивным, поэтому необходимость в стеке для обхода прошитого дерева отпадает.

Для того, чтобы программы правильно работали с пустыми деревьями можно использовать голову, для которой исходное поддерево является левым поддеревом. В этом случае связи llink узла p и rlink узла g должны указывать на голову.

До сих пор представление деревьев производилось с помощью всюду прошитых или не прошитых деревьев. Существует промежуточный способ. Если отсутствует правая или левая прошивки дерева, то деревья называются соответственно право- и лево-прошитыми.

Так же можно определить прошивки для прямого и концевого обхода.

Деревья без связей

Подходящий выбор представления дерева в первую очередь определяется видом операций, выполняемых над деревьями. В частности, можно использовать методы последовательного распределения памяти. Этот способ является наиболее подходящим в случае, когда требуется компактное представление дерева, размеры и конфигурация которого в процессе выполнения программы не должны сильно изменяться. Два наиболее простых способа представления деревьев заключается в том, что опускается поле llink или rlink. Последовательное расположение узлов в памяти замечает связь одного из этих типов.

Пусть в дереве вообще не будут храниться левые связи. Левые связи отображаются с помощью физического соседства. Если узел имеет левого сына, то он лежит сразу за отцом. Рассмотрим на примере прямого обхода.

d

Прямой обход дает последовательность: a b c d f p q s r.

Обозначим символом J отсутствие левого сына у узла, а стрелкой изобразим правую связь.

Тогда дерево, не использующее поле llink можно изобразить:

a b cjdjf рТн' sj rj

Связь llink не нужна, так как она либо равна нулю, либо указывает на следующий элемент последовательности.

Предложенное представление дерева связано с непроизводительной тратой места, так как многие поля rlink пусты. Поэтому существует еще один способ представления дерева, основанный на последовательном использовании памяти - вообще без связей. Все узлы располагаются в

последовательной памяти (нумерация узлов ведется с единицы). Узел с номером X имеет узлы с номерами 2X и 2X+1 в качестве сыновей.

Т.е. для описанного выше дерева будем иметь следующую таблицу распределения памяти.

1

2

3

4 5

6

7

8

9 10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

a

b f c d

p

q

s r

Преобразование дерева общего вида к бинарному

Всякое дерево можно представить в виде бинарного. Алгоритм преобразования следующий. Оставляем связь отца с самым левым сыном. Братьев свяжем правыми связями. Такой алгоритм гарантирует единственность преобразования.

Пример:

Таблицы

Рассмотрим простейшую таблицу.

фамилия, имя, отчество

почтовый адрес

телефон

Одна строчка в таблице называется записью. Каждая колонка таблицы называется полем.

Отличительную характеристику записи таблицы называют ключом или признаком. Все операции над таблицами задаются по отношению к ключу, а выполняются над всей записью.

Ключ – это аргумент поиска записей в таблице. Ключ может состоять из одного или нескольких полей таблицы.

Ключ называется первичным, если поле или группа полей, составляющих ключ, уникальным образом определяют запись. Иначе поиск по первичному ключу можно определить как “найти единственную”. Первичный ключ не обязательно единственен.

Вторичный ключ не определяет запись уникальным образом, т.е. поиск “найти многие”.

К типичным операциям над таблицами относят:

  1. Включение: дана пара ключ-данные, требуется включить запись в таблицу так, чтобы впоследствии ее можно было найти по ключу.

  2. Изменение: найти ключ, изменить данные записи.

  3. Исключение: по заданному ключу исключить всю запись из таблицы.

4. Поиск: дан ключ, найти соответствующие данные. Таблица может иметь индекс. Индексом называется таблица, состоящая из двух полей:

ключа и номера записи. Пример:

ключ

номер записи

Иванов

21

Иванов

353

Иванов

14

Петров

16

На одну таблицу можно навесить несколько индексов и сделать ее доступной. Существуют четыре способа организации таблиц:

  • куча (последовательные);

  • сортированные;

  • древовидные;

  • рассеянные. В каждом конкретном случае следует выбирать наиболее подходящий способ,

обеспечивающий эффективное выполнение требуемых операций.

Куча

Вставка.

Запись помещается либо в свободную позицию, либо в конец.

Поиск.

Поиск производится последовательным перебором. Успешный поиск по первичному ключу потребует просмотра в среднем полтаблицы. Неудачный поиск (поиск того, чего в таблице нет) – просмотр всей таблицы. Поиск по вторичному ключу всегда требует просмотра всей таблицы.

Удаление.

Тут существует два способа.

  1. Наивный. Запись физически удаляется. Все нижележащие записи перемещаются на позицию вверх. Эта операция требует просмотра половины всех записей таблицы.

  2. Запись помечается как удаленная (* в первом байте записи рассматривается как пометка на удаление). Возникает проблема использования свободного пространства. Таким образом, помеченные на удаление записи придется рано или поздно удалять физически. Для этого используется операция упаковка (Pack). Лучше, чтобы этих операций было меньше.

Все операции над кучей требуют время, пропорциональное размеру таблицы. Следовательно, кучу целесообразно применять для небольших таблиц или когда скорость работы с таблицей не важна.

Сортированные таблицы

Вставка.

Существует два способа.

  1. Наивный. Находим место вставки (например, методом дихотомии). Физически перемещаем вниз все записи от места вставки вниз. Затраченное время ~ N.

  2. С каждой записью может быть ассоциирована область переполнения, которая может быть организована как куча, список, дерево. Дихотомией находится место вставки и если там не тот ключ, то обходим область переполнения, связанную с этой записью.

Поиск.

Поиск осуществляется методом дихотомии - деление отрезка пополам. Для поиска в массиве целых чисел можно предложить функцию:

//в массиве t размера п найти запись, соответствующую ключу key и вернуть ее номер. int dihot (int *t, int n, int key) { int i, j, k; i = 0; j = n - 1; while (i < j) { k = (i + j) / 2; if (t [k] < key) i = k; else j = k; }

if (t [i] = = key) return i; else return -1; }

Оценим время выполнения наиболее экономного алгоритма поиска. При каждом сравнении ключей ставится вопрос типа “совпадают ли ключи” или “больше-меньше”. Сравнение дает ответ “да-нет”. Таким образом, М сравнений дает 2м возможных исходов. Искомый ключ в таблице из N записей, может находиться в любом из N мест. Следовательно, требуется распознать N ситуаций, т.е. количество исходов не должно быть меньше N: 2м > N. Логарифмируя, получаем количество сравнений М > log2N .

Удаление.

Тоже два способа.

  1. Наивный. Удалить физически и передвинуть следующие записи. Время выполнения ~ N.

  2. Пометить запись как удаленную. Присоединить ее к списку свободного пространства нельзя, т.к. нарушится метод дихотомии. Потребуется обслуживание такой таблицы.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]