Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
evm_k_dosroku.doc
Скачиваний:
8
Добавлен:
08.11.2019
Размер:
7.64 Mб
Скачать

6 Списки

6.1 Ссылочный подход

1. элементы данных образуют линейную цепочку, т.е. для каждого элемента существует "следующий" и "предыдущий" (естественно, кроме концевых элементов, для которых существует только один такой соседний элемент);

  1. в каждый момент времени в этой линейной цепочке определена некоторая текущая позиция и нам разрешен доступ к элементу в этой текущей позиции (или к его непосредственным "соседям").

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

  3. добавление и удаление элементов может происходить в окрестности текущей позиции, причем эти операции не должны приводить к массовым пересылкам данных в памяти;

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

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

Будем называть указателем списка переменную (или переменные), хранящие указатель на доступный в данный момент элемент (или два соседних элемента) списка. Изменяя значение этого указателя ("передвигая" указатель списка), мы можем добраться до любой позиции в списке, т.е. обеспечить выполнение условия 3.

6.2 Одно- и двунаправленные списки

Сначала мы рассмотрим реализацию двунаправленного списка, в котором каждый элемент (кроме первого и последнего) ссылается на два соседних с ним элемента. Примем следующие соглашения по поводу задания структуры этого списка:

  • каждый элемент списка представляет собой составной объ­ект, хранящий в себе содержательную информацию данного элемента и два указателя — на следующий и предыдущий элементы;

  • в каждый момент времени разрешен доступ только к двум соседним элементам списка (будем называть их элемент до указателя и элемент за указателем), пару указателей на позиции этих двух элементов будем называть указателем текущей позиции;

  • удалять можно только те элементы, на которые показывает указатель текущей позиции (т.е. элементы до или за указа­телем), при этом соответствующая ссылка указателя списка перемещается на предыдущий (соответственно, следующий) элемент;

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

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

7 Деревья

7.1 Определения и обходы

Понятие списка, которое мы рассмотрели в предыдущем разделе, естественным образом обобщается на структуры с множествен­ными связями между отдельными элементами. Одной из таких структур является дерево. В математике дерево определяется как связный граф, не содержащий циклов. Если в этом графе выделена одна вершина, то говорят, что дерево имеет корень. Мы будем рассматривать именно такие деревья. Наличие корня в дереве сразу определяет некоторое отношение подчиненности вершин. Для большей четкости изложения введем несколько определений.

Определение 1. Расстоянием (или длиной пути) между двумя вершинами А и В дерева назовем количество ребер графа, содержащихся в связной цепочке ребер, соединяющих А и В. (Поскольку дерево — это граф без циклов, расстояние между двумя вершинами определяется однозначно.)

Определение 2. Вершина В называется потомком вершины А, если расстояние между А и В равно 1 и расстояние от А до корня меньше, чем расстояние от В до корня дерева. Вершину А будем также называть родителем вершины В.

Определение 3. Будем говорить, что вершина А принадле­жит k-му уровню дерева, если расстояние от А до корня дерева равно к.

Очевидно, что уровень 0 дерева содержит только корень, а уровень к дерева образован потомками всех вершин уровня к 1.

Определение 4. Ветвью дерева назовем связную последо­вательность вершин, начинающуюся в корне и оканчивающуюся

на вершине, не имеющей потомков. Длиной ветви назовем число вершин в этой ветви.

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

Чтобы реализовать хранение и работу с данными, содержа­щимися в вершинах дерева, нам необходимо каким-то образом представить существующие связи между между отдельными вершинами. Так как каждая вершина связана ребрами не более, чем с тремя другими вершинами, то мы можем естественным образом обобщить списочный подход и для представления вершины ввести следующий класс1:

class TreeNode

{ public:

Type value;

TreeNode *prev,*left,«right;

};

где указатели left и right показывают на потомков, a prev — на родителя. При этом, если вершина не имеет соседей в каком-нибудь направлении, то соответствующий указатель имеет нулевое значение.

Такой способ представления каждой отдельной вершины является исчерпывающим для бинарного дерева.

Рекурсивное определение дерева.

1. Пустое множество вершин есть (пустое) дерево;

  1. Одна вершина, не связанная ни с какой другой вершиной, образует дерево и является корнем этого дерева;

  2. Пусть имеется к деревьев Ti,...,Tk с корнями Аi,..., Ак, тогда, связывая вершину А с вершинами Ai,...,Ak, мы получим новое дерево, в котором А есть корень, а Тi .. .Тк — поддеревья.

Определение 6. Назовем ключом вершины дерева некоторое значение, которое однозначно вычисляется по содержательной информации, хранящейся в данной вершине, и связано неко­торым отношением порядка с ключами других вершин. Ключ используется для поиска или сравнения вершин дерева.

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

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

Идеально сбалансированное дерево решает поставленную задачу, так как в нем длина любой ветви не превосходит величины [log2 ЛГ] + 1. К сожалению, формирование такого дерева достаточно трудоемкая процедура. Поэтому мы несколько ослабим требования на идеальность балансировки дерева и попы­таемся за счет этого построить более эффективные алгоритмы. Итак, вместо идеально сбалансированного дерева введем понятие сбалансированного дерева.

Определение 9. Длиной дерева будем называть длину его максимальной ветви.

Определение 10. Бинарное дерево называется сбалансиро­ванным1, если в любой его вершине длины левого и правого поддеревьев отличаются не более чем на 1.

Двоичное дерево поиска (by Wiki)

Двоичное дерево поиска — это структура данных двоичное дерево, в котором данные , привязанные к каждому узлу представляют собой пару (key, value) (ключ и значение) , причём на ключах определена операция сравнения "меньше", и для всех узлов дерева выполнено свойство, называемое свойством дерева поиска:

у всех узлов левого поддерева произвольного узла n значение ключей меньше, нежели значения ключа узла n,

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

1 Основные операции в двоичном дереве поиска

1.1 Поиск элемента (FIND)

1.2 Добавление элемента (ADD)

1.3 Удаление узла (DEL)

1.4 Обход дерева (TRAVERSE)

1.5 Сортировка с помощью двоичного дерева поиска

Основные операции в двоичном дереве поиска

Базовый интерфейс двоичного дерева поиска состоит из трех операций:

FIND(K) — поиск узла, в котором хранится пара (key, value) с key = K.

ADD(K,V) — добавление в дерево пары (key, value) = (K, V).

DEL(K) — удаление узла, в котором хранится пара (key, value) с key = K.

Этот абстрактный интерфейс является общим случаем, например, таких интерфейсов, взятых из прикладных задач:

«Телефонная книжка» — хранилище записей (имя человека, его телефон) с операциями поиска и удаления записей по имени человека, и операцией добавления новой записи.

Domain Name Server — хранилище пар (доменное имя, IP адрес) с операциями модификации и поиска.

Namespace — хранилище имен переменных с их значениями, возникающее в трансляторах языков программирования.

По сути, двоичное дерево поиска — это структура данных, способная хранить таблицу пар (key, value) и поддерживающая три операции: FIND, ADD, DEL.

Кроме того, интерфейс двоичного дерева включает ещё три дополнительных операции обхода узлов дерева: INFIX_TRAVERSE, PREFIX_TRAVERSE и POSTFIX_TRAVERSE. Первая из них позволяет обойти узлы дерева в порядке неубывания ключей.

Поиск элемента (FIND)

Дано: дерево Т и ключ K.

Задача: проверить, есть ли узел с ключом K в дереве Т, и если да, то вернуть ссылку на этот узел.

Алгоритм:

Если дерево пусто, сообщить, что узел не найден, и остановиться.

Иначе сравнить K со значением ключа корневого узла X.

Если K=X, выдать ссылку на этот узел и остановиться.

Если K>X, рекурсивно искать ключ K в правом поддереве Т.

Если K<X, рекурсивно искать ключ K в левом поддереве Т.

Добавление элемента (ADD)

Дано: дерево Т и пара (K,V).

Задача: добавить пару (K, V) в дерево Т.

Алгоритм:

Если дерево пусто, заменить его на дерево с одним корневым узлом ((K,V), nil, nil) и остановиться.

Иначе сравнить K с ключом корневого узла X.

Если K>=X, рекурсивно добавить (K,V) в правое поддерево Т.

Если K<X, рекурсивно добавить (K,V) в левое поддерево Т.

Удаление узла (DEL)

Дано: дерево Т с корнем n и ключом K.

Задача: удалить из дерева Т узел с ключом K (если такой есть).

Алгоритм:

Если дерево T пусто, остановиться

Иначе сравнить K с ключом X корневого узла n.

Если K>X, рекурсивно удалить K из правого поддерева Т.

Если K<X, рекурсивно удалить K из левого поддерева Т.

Если K=X, то неоходимо рассмотреть два случая.

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

Если оба ребёнка присутствуют, то

найдём узел m, являющийся самым левым узлом правого поддерева;

скопируем значения полей (key, value) узла m в соответствующие поля узла n.

у родителя узла m заменим ссылку на узел m ссылкой на правого ребёнка узла m (который, в принципе, может быть равен nil).

освободим память, занимаемую узлом m (на него теперь никто не указывает, а его данные были перенесены в узел n).

Обход дерева (TRAVERSE)

Есть три операции обхода узлов дерева, отличающиеся порядком обхода узлов.

Первая операция — INFIX_TRAVERSE — позволяет обойти все узлы дерева в порядке возрастания ключей и применить к каждому узлу заданную пользователем функцию call_back_function. Эта функция обычно работает только c парой (K,V), хранящейся в узле. Операция INFIX_TRAVERSE реализуется рекурсивным образом: сначала она запускает себя для левого поддерева, потом запускает данную функцию для корня, потом запускает себя для правого поддерева.

INFIX_TRAVERSE ( call_back_function ) — обойти всё дерево, следуя порядку (левое поддерево, вершина, правое поддерево).

PREFIX_TRAVERSE ( call_back_function ) — обойти всё дерево, следуя порядку (вершина, левое поддерево, правое поддерево).

POSTFIX_TRAVERSE ( call_back_function ) — обойти всё дерево, следуя порядку (левое поддерево, правое поддерево, вершина).

INFIX_TRAVERSE:

Дано: дерево Т и функция f

Задача: применить f ко всем узлам дерева Т в порядке возрастания ключей

Алгоритм:

Если дерево пусто, остановиться.

Иначе

Рекурсивно обойти правое поддерево Т.

Применить функцию f к корневому узлу.

Рекурсивно обойти левое поддерево Т.

В простейшем случае, функция f может выводить значение пары (K,V). При использовании операции INFIX_TRAVERSE будут выведены все пары в порядке возрастания ключей. Если же использовать PREFIX_TRAVERSE, то пары будут выведены в порядке, соответствующим описанию дерева, приведённого в начале статьи.

Сортировка с помощью двоичного дерева поиска

Бинарное дерево поиска можно использовать для сортировки. Для этого берётся пустое дерево, к нему добавляют все элементы массива, а затем, используя алгоритм "Обход дерева", записывают элементы дерева в массив в возрастающем порядке.

Если элементы массива различны и расположены в случайном порядке, а длина массива N, алгоритм требует в среднем O(NlogN) операций. Если они уже отсортированы в возрастающем или убывающем порядке, то дерево становится несбалансированным (т.е. у него появляется много пустых веток). Тогда алгоритм требует O(N2) операций, и это худший возможный случай. Чтобы сбалансировать дерево следует использовать алгоритм пирамиды или красно-чëрное дерево.

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