скачать Лекция 5 Структуры данных Математические модели данных. Базовые типы данных. Ссылочные типы данных. Древовидные структуры данных. Упорядоченные бинарные деревья. АВЛ-деревья. Хэш-таблицы. Данные – это то над чем производятся действия. Действия формально описываются системой или, более точно, композицией функций, вычислимых алгоритмом (механической процедурой или подходящими процессорами). По всей видимости, характер данных определяет специфику функций (специфику алгоритма). Самыми простыми данными являются натуральные числа и рекурсивные функции, которые определяют все мыслимые сложные действия над такими элементарными объектами. Гёделем было показано, что любая задача порождения или распознавания объекта любой сложности может быть сведена к процедуре, которая называется гёделизацией. Объекты могут быть закодированы натуральными числами, а действия с ними – рекурсивными функциями. Сложность такого кодирования (гёделизации) объектов произвольной природы является самостоятельной и весьма сложной задачей. Собственно, до появления развитых языков программирования (FORTRAN, ALGOL, PASCAL, C) программирование сложных задач сводилось к построению последовательности операций над двоичными кодами. Возникает вопрос: можно ли разделить сложность решения задачи между конструированием сложных данных и конструированием сложных функций. В настоящее время основное направление развития языков программирования связано с разработкой описания «сложных» данных и «сложных» функций над этими данными. При решении конкретных задач всегда приходится подбирать «сладкую парочку» – данные и алгоритм.
ММД представляет собой формальное описание данных, позволяющее конструировать сложные данные из простых. Здесь необходимо вспомнить конструкторские механизмы, которые были изучены для описания алгоритмов. Напомним, а) для машинного описания алгоритма – это логическая структура алгоритма (ЛСА), задаваемая в виде правил «машины состояний» (SM); б) для описания алгоритма композиций функции – это механизм конструирования рекурсивных функций; в) для описания алгоритма в виде исчисления – это формулы (коды объекта) и правила вывода (ПВ) новых формул (кодов объекта). Алгоритм «работает» с процессором, а данные «работают» с памятью; они должны записываться , храниться и извлекаться из нее. ММД должна учитывать именно эту специфику данных.
и структуры данных (СД) АТД = ![]() A – множество элементов данных, P – множество отношений над. A. F – множество функций над отношениями из P. ![]() ![]() Конкретный тип данных всегда требует определения структур данных ![]() ![]() Языки программирования содержат АТД, которые называются базовыми типами и конструкторские механизмы порождения сложных АТД из базовых.
Два отношения позволяют человеку понимать и организовывать окружающий мир: 1) отношение «состоит из» и 2) отношение «имеет значение». На фундаментальность этих способов понимания и организации мира в любой области человеческой деятельности впервые обратил внимание А.А.Богданов в своей работе «Тектология или всеобщая организационная наука» (1913). Мы уже встречались с понятием «структурированного программирования», где понятие программа уточнялось следующим образом. Программа «состоит» из последовательности программ, либо альтернативных программ, либо циклических программ. Информационная структура алгоритма (ИСА), задающая сложную функцию, «уточнялась» подстановкой образующих функций. Например, функция y = x1 + x2 «состоит из» двух аргументов x1 и x2, которые определяют структуру функции, x1 и x2 «имеют значения» (принимают значения) либо 1, либо 2, либо 3, либо (но не все вместе) из множества натуральных чисел. Отметим что отношение структуризации «состоит из» строится как конъюнктивное высказывание. Объект «состоит из» объекта 1 и объекта 2 и т.д. Отношение «имеет значение» строится из дизъюнктивных высказываний. Объект может быть определен как объект 1, либо как объект 2, либо… и т.д. Искусственные объекты реального мира строятся (организуются) с учётом этих фундаментальных отношений. На рис.1 представлена структура объекта «самолёт». Однократной дугой отмечены отношения «состоит из» двукратной дугой – «имеет значение». Сложная структура слов некоторого языка задаётся правилами формальной грамматики, которые описывают замену (подстановку) одного фрагмента слова на другой и фактически задают отношение «состоит из», а альтернативные правила замены передают отношение «имеют значение» (фрагмент «состоит из» фрагмента 1 и фрагмента 2, либо из фрагмента 3 и т.д.). На рис.2 представлено структурное описание болезни «грипп». Диагностическая таблица состоит из набора признаков: x1– кашель и x2 – температура и x3 – насморк. Каждый признак имеет значения 1 (имеет место) либо 0 (не имеет место). Болезнь «грипп» описывается прецедентами (отношение «либо»), если набор признаков относится к болезни «грипп», то он отмечается «1». Для таблицы построено пространство признаков в котором «*» отмечены точки, задающие отношение «грипп». Проведенная минимизация формулы высказывания «грипп» даёт следующее структурное описание: ![]() При конструирования структур данных всегда средствами языка записывают отношения «состоит из» и «имеет значение». ![]() Рис. 1. Структурное описание объекта «Самолет». ![]() Рис. 2. Структурное описание болезни «Грипп». Структурная формула гриппа: (x1 & x3) x2. ^ Конструкции базовых типов во всех процедурных языках повторяют структуру памяти ЭВМ. История появления базовых типов связана с построения формальных моделей памяти, которые с одной стороны отменяли бы жёсткую физическую адресацию, но, с другой стороны, позволяли, введённую формальную идентификацию элементов данных , отображать на структуру физической памяти. Чтобы подчеркнуть это различие, говорят о логической памяти (формальной модели) и физической памяти. ^ Задаёт отображение данных ![]() ![]()
В этом случае выделяется прямая адресация для данных, которые находится физически в оперативной памяти и индексно – последовательная, если данные находятся на диске либо ленте.
Для логической памяти, имеющую сложную или даже очень простую СД (например, прямую адресацию) определяются операции порождения СД и операции распознавания СД. Базовыми операциями (элементарными) являются следующие. а) ^ . Содержательно это означает, дан элемент ![]() б) ^ . Задан идентификатор «х», найти «х»в СД. Для различных функций адресации и СД приходится создавать собственный алгоритм поиска. В этом случае удобно создавать определённые классы СД, и для каждого класса свой алгоритм поиска. в) ^ . Эта операция является отживающим атавизмом, связанным с экономией памяти. Операция состоит из поиска элемента и действий, анулирующих его связи в СД. ^ Конструктивные механизмы порождения СД можно разбить на два типа: 1) описания конечных структур, обычно это формулы, записанные специальными конструкциями языка программирования 2) рекурсивные схемы (индуктивные описания), записанные также при помощи специальных конструкций языка. Примером индуктивного описания может служить описание структуры арифметического выражения в языке программирования, использующего формализм Бэкуса – Наура, который является контекстно – свободной грамматикой, о чём говорилось выше. ^ Базовые типы данных это те СД, которые разрешены в языках программирования и понимаются соответствующим транслятором. Например базовыми типами для языка FORTRAN являлись только натуральные и действительные числа. Далее рассматриваются базовые типы, характерные для языка PASCAL. Заметим, что PASCAL был первым языком, в котором были введены сложные типы данных и были предложены рекурсивные механизмы описания бесконечных СД. Большинство языков по примеру PASCAL’я дополнительно ввели аналогичные типы данных. ^ . Стандартные типы данных – это типы, которые являются встроенными для большинства ЭВМ. Они включают целые числа (integer), логические значения (booleаn), множество символов печати (char), вещественные числа (real). Для данных типа integer определены точные операции (+) – сложение, (-) – вычитание, (*) – умножение, для boolean определены значения true (истина), false (ложь) и операции () – дизъюнкция, (&) – конъюнкция, () – отрицание. ^ Эти СД определяют множество заданное перечислением своих значений, т.е. множество объединяющие элементы данных с отношением «иметь значение». ![]() Например, type цвет=(красный, зелёный, синий). По сути дела ![]() 2.3. Массив Наиболее распространённая СД, начиная с языка FORTRAN. Массив «состоит из» одинаковых элементов данных с так называемым «случайным доступом» с функцией адресации ![]() ![]() Описание СД «массив» задаётся формулой – ![]() Т – имя типа, I – индекс обычно «имеет значение» из множества N – натуральных чисел, Т0 – некоторый простой тип, например, real, char и т.д. Сложная СД «массив от массива» аналогична конструкции «функция от функции». ![]() 2.4. Запись. СД «запись» представляет объединение («состоит из») элементов, принадлежащих к различным типам. ![]() ![]() ……… ![]() end По сути дела СД «запись» задаёт декартово произведение множеств Z1, Z2,…, Zn – ![]() 2.5. Множество Конструкция СД «множество» задаёт множество элементов объединённых отношением «имеет значение» ![]() Значениями типа Т является множество элементов типа Т0. Например, ![]() означает, что задано множество значений ![]() Иногда значением конструкции « ![]() ![]() ![]() АТД «множество»= ![]() Где – операция пересечения множеств, – объединение множеств, / – разность множеств. ^ Общее свойство СД, которые обсуждались выше, заключалось в их «конечности». Число элементов массива – конечно, число записей – конечно, число элементов множества значений тоже конечно. Например, определение языка L на алфавите A есть ![]() ![]() ![]() ![]() Определение последовательности как АТД с операцией приписывания (конкатенации), задаётся по индукции:
Для последовательностных СД объём памяти, который необходим для их размещения заранее неизвестен. Трансляторы, работающие с такими СД, должены обладать процедурами динамического распределения памяти. Большинство языков программирования имеет возможность задавать последовательностные СД в виде файла. ![]() где Т0 – любой базовый тип данных. Очень важное свойство файла – последовательный доступ. Файл строится (порождается) при помощи последовательного добавления элементов данных (например, в конец). Поиск элементов данных (распознавание) производится последовательно (от начала до конца). Принято считать, что файл находится в одном из двух состояний: 1) порождение (запись), 2) поиск (чтение). Заканчивая описания базовых типов данных, необходимо сделать замечание: произвольная организация сложных структур «данные от данных» (по анологии «функция от функции») не всегда допускается в языках программирования и, как правило, допустимая структуризация описывается в документации на определённую версию транслятора. ^ Пара элементов данных может быть связана отношением «следует за». Порождение таких структур может быть конечным описанием таких пар, либо, в случае бесконечных структур, некоторым рекурсивным механизмом. Поиск (распознавание) элемента данных происходит из специального начального элемента алгоритмом обхода СД с отношением «следует за». Для реализации СД с отношением «следует за» вводится ссылочный тип данных. ![]() Структура элемента ссылочного типа представляет собой запись с полями, где кроме полей, содержащих информацию, есть поля, служащие для организации СД.
xij – ссылка (указатель) на имя следующего элемента xij задаёт отношение «следует за». Конечные структуры задаются совокупностью пар элементов. Пример 1. Список авиатрасс, связывающих города. Отношение «следует за» суть отношение «из города x можно долететь до города y». type Москва= record информация о трассе; Казань; Новгород end type Казань=record информация о трассе; Новгород; Казань; end type Новгород=record информация о трассе; Москва Далее ссылочные структуры будут иллюстрироваться бинарным графом, вершины которого суть имена элементов данных, а направленные дуги соответствуют отношению «следует за». Для примера 1 граф ссылочной СД будет иметь следующий вид, показанный на рис.1 ![]() Рис.1 Ссылочный граф для СД «авиатрассы». ^ (LL) LL линейный список или очередь имеет ссылочный граф вида ![]() где 0, 1, и т.д. имена элементов данных, построенных в цепочку. Бесконечные LL задаются рекурсивным описанием type T = record key: integer; next: T end где поле «next» содержит ссылку (указатель) на следующий элемент данных. Операция удаления элемента из любого места списка определяет разрыв графа ссылок и новые ссылки для организации необходимого отношения «следует за». Поиск любого элемента списка всегда начинается с начального элемента. ^ Цепочки LL могут быть связаны между собой либо введением специальных элементов связей, либо полей ссылок. Обычно под связными списками понимают деревья, например, показанные на рис. 2. Записи, которые не содержат ссылок (пустые ссылочные поля) называются терминальными. Для рис. 2 это записи с номерами 5, 7, 8, 11, 12, 13. Начало древовидного списка называется корнем, или корневой записью. Как видно из ссылочной СД древовидного списка, некоторые ссылочные поля являются пустыми. Древовидный список может быть записан в виде скобочной формулы, где знак «» соответствует отношению «следует за», а знак «,» соответствует отношению «и». Например, формула х (y, z) читается так: «за «х» следует и «y» u «z».» Для древовидного списка на рис. 2 скобочная формула будет иметь вид: 01(2(6(9(12,10(11,13)))),3(7,4(8,5))) Язык ЛИСП фактически использует древовидные списки в виде скобочных формул. Операции включения и исключения элементов древовидного списка проводятся изменением значений ссылочных полей. ![]() ![]() ![]() Рис. 2. Древовидный список ^ Последовательность обхода (алгоритм обхода) при поиске заданного элемента должна гарантировать доступ к всем элементам древовидной СД для проведения процедуры идентификации (распознавания). Существует три вида процедур такого обхода.
![]()
![]()
Представляет собой алгоритм Тезея обхода лабиринта. Для этих целей дерево лабиринта снабжает специальной вершиной, входом (выходом) в корень дерева (она обозначена «». Последовательность обхода Тезеем дерева (по правилу «правой руки») на рис.2 будет следующим: , 0, 1, 2, 6, 9, 12, 9, 10, 13, 11, 10, 9, 6, 2, 3, 7, 3, 4, 8, 4, 5, 4, 3, 1, 0.
Для выполнения алгоритмов над произвольными бинарными отношениями служат произвольные ссылочные структуры. На рис.3 показано описание буквы «А» в виде отношения связей между элементами ![]() Число операций, которые необходимо затратить на поиск элемента в древовидных ссылочных структурах либо произвольных ссылочных структурах, состоящих из «n» элементов, равно в среднем n/2, а в худшем случае «n». ![]() ![]() г) последовательность обхода ссылочного графа для поиска элемента (Т-код) , T1, S1, S2, T2, S2, P, S1, P, S2, S1, T1, Рис. 3. Произвольная ссылочная структура, задающая описание буквы «А» ^ В этих СД поле имени в записи элемента данных содержится значение «ключа». Переменная ключ- k обладает следующими свойствами.
![]() ![]() ^ называется дерево, вершины которого размечены значениями ключей так, что они образуют отношение строго частичного порядка. На рис.4а показано такое дерево. Рёбра дерева суть отношение порядка обозначено «<<». На рис.4 и далее УБД представлены, расположенными по направлениям < ![]() ![]() ![]() Рис. 4 Упорядоченное бинарное дерево ^
На рис.4в вершины 6, 8, 9, 10 – терминальные.
На рис.4в путь из вершины 2 в 9 – ![]() ![]()
![]() Если ![]() Если ![]() На рис.4в дерево размечено значениями дисбалансов каждой вершины. ^ Процедура порождения УБД определена для последовательности данных. Процедура представляет собой автомат, работающий с лентой данных. В момент t автомат «отрывает» от ленты ровно одну ячейку, где расположен элемент данных ![]() ![]()
На рис.5 показана последовательность шагов порождения УБД. ^ Последовательности называются эквивалентными, если они порождают одинаковые деревья. На рис.5б и рис.6 показаны деревья для эквивалентных и не эквивалентных последовательностей, содержащих одни и те же значения ключей. ![]() ![]() Рис. 5. Порождение УБД ![]() ![]() ![]() Рис. 6. Эквивалентность последовательностей ^ Назовём узлом УБД вершину вместе с её связями с другими вершинами УБД. Заметим, что число связей для каждой вершины не более трёх. Процедура удаления состоит из двух операций
Далее необходимо ввести операцию восстановления УБД из «осколков» так, чтобы не нарушить отношение порядка в исходном УБД.
Замечание. Для построения УБД без удалённой вершины «Z» можно воспользоваться процедурой порождения дерева для последовательности, в которой вычеркнут элемент «Z». При этом может возникнуть УБД совершенно «не похожее» на исходное дерево. Введённые выше операции «удаления» и «связывания» гарантируют минимальную перестройку УБД при удалении вершины. На рис.8 и 9 приведены примеры удаления узлов из УБД. ![]() ![]() б) Связывание деревьев yD, xD с узлом v, операция связывания обозначена «+». Два варианта операции связывания. I. 1) v +хD – связывание v и хD. 2) min xL + yD – связывание xD и yD. ![]() II. 1) v +yD – связывание v и yD. 2) min yR + xD – связывание yD и xD. ![]() Рис. 7. Операции восстановления УБД при удалении узла (вершины) а) удаление узла 17 ![]() ![]() Рис. 8. Пример удаления узла и восстановления УБД а) Удаление узла 12 ![]() б) Два варианта связывания 1) операция ![]() ![]() ![]() 2) операция ![]() ![]() ![]() Рис.9. Пример удаления узла и восстановления УБД ^ УБД называется сбалансированным, если для каждого его узла «х» значение дисбаланса ![]() ^ УБД Идеально сбалансировано, если для каждого его узла «х» количество узлов в xDL и xDR различаются не более, чем на 1. ![]() На рис.9а показанное идеально сбалансированное дерево. Для идеально сбалансированного дерева из «n» узлов количество операций поиска любого узла не более, чем ![]() Порождение идеально сбалансированного дерева на каждом шаге состоит из двух операций:
а) Идеально сбалансированное дерево П1=8, 12, 14, 16, 15, 10, 11, 9, 4, 6, 7, 5, 2, 3, 1 ![]() б) АВЛ – дерево П2=8, 12, 14, 16, 15, 10, 11, 9, 7, 6, 5, 3, 4, 1, 2 ![]() в) П=11, 12, 14, 15, 16, 6, 7, 8, 9, 10, 1, 2, 3, 4, 5 ![]() Рис.10 Сбалансированные деревья. 6. АВЛ-деревья Алгоритм идеального балансирования является достаточно сложным и здесь не приводится. Более простые алгоритмы балансировки получаются, если ввести следующее определение: УБД является АВЛ-деревом тогда и только тогда, когда для каждого его узла «х» высота его правого поддерева ![]() ![]() АВЛ – дерево получило своё название по фамилиям математиков, предложивших и исследовавших такие типы сбалансированных деревьев – Адельсон – Вельский (Россия), Ландис (США). На рис. 10б показано АВЛ-дерево, а на рис. 10в разбалансированное дерево на тех же данных. Очевидно, что количество операций поиска любого узла АВЛ – дерева не более ![]() Операция балансировки АВЛ-дерева показана на рис.11. Здесь уместно привести аналогию с подтягиванием гирь в часах. Если в каком-то узле Ш существует дисбаланс, например ![]() На рис. 12, 13 приводятся примеры АВЛ-балансировки, которые выполняются за несколько шагов. При многошаговой балансировке в случае, когда имеются несколько «самых нижних» разбалансированных узлов алгоритм может привести к «зацикливанию». Свойства АВЛ-деревьев, для которых алгоритм балансирования не приводит к зацикливанию, до сих пор неизвестны.
АВЛ-отношений а) для ![]() Ш = panties ![]() ![]() Операция «lower» не нарушила отношения порядка между узлами. Отношение ![]() б) для ![]() ![]() ![]() Операция «rise» не нарушила отношений порядка между узлами. Ш>u до возвращения и после сохраняется. Рис.11 Операция балансировки АВЛ – дерева. а) Исходное УБД 1) Поднять Ш1 ![]() 2) Опустить Ш2 ![]() Рис.12 Пример двухшаговой балансировки, приводящей к АВЛ – дереву. а) Исходное УБД с разметкой дисбаланса узлов ![]() 1) Поднять Ш1 2) Опустить Ш2 ![]()
![]() Рис.13. Пример 3-х шаговой балансировки ^ Хэш – функция или функция расстановки задаёт отображение ![]() ![]() ![]() ![]() ![]() ![]() ![]() Например, множество возможных 8-символьных слов из 2 – буквенного алфавита составляет 268 – 1 млрд. слов. Таким образом, если каждое слово из 8 букв является ключом, то ![]() ![]() ^ Пусть ![]() ![]() Например, пусть ![]() Н – таблица П(k)=0, 1, 3, 15, 4, 6, 2, 4.
Рис.14. Н – таблица с прямой адресацией k=i. ^ . ![]() В этом случае для n<m ![]() На рис. 15 приведен пример заполнения Н – таблицы длиной m=4 последовательностью ключей длиной n=8. При этом возникают коллизии, на одну и ту же ячейку с одним и тем же индексом претендуют несколько элементов данных, как это показано на рис.14а. На ячейку «0» претендуют 5 и 20, на ячейку 1–36, 6, 31, на ячейку 2–17, 2. Наиболее простым способом разрешения коллизий является алгоритм размещения в свободную правую ячейку. При этом «правая часть» таблицы всегда может быть увеличена на одну ячейку. Работа по этому правилу требует проведения операции идентификации занятости, такая операция называется пробой. Пример такого размещения в Н-таблице показан на рис. 15б. Поиск элементов с ключом k=i требует очевидно такое же число проб, как и его размещение. Удаление элемента с ключом k=i оставляет соответствующую ячейку свободной. Число операций при размещении элемента в наихудшем случае, когда уже размещено «n» элементов равно также «n». Число операций поиска (проб) при таблице длиной ![]() ![]() а) Н – таблица для П(k)=17, 2, 36, 5, 20, 6, 31, 3 Пример коллизии ![]() б) Заполнение Н – таблицы при открытой адресации. ![]() ![]() ![]() ![]() 1 ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() размещение 17 размещение 2 размещение 36 1 проба 2 пробы 1 проба ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() 4 ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() размещение 5 размещение 20 размещение 6 1 проба 5 проб 5 проб ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() 7 ![]() ![]() i 0 1 2 3 4 5 6 i 0 1 2 3 4 5 6 7 ![]() ![]() размещение 6 размещение 3 6 проб 5 проб Рис.15. Н – таблица с открытой адресацией и Н – функцией ![]() ^ В этом случае Н – таблица сворачивается в кольцо и Н – функция рекурсивно зависит от числа проб j. На рис. 16 показано размещение 8 записей в 8 ячейках, организованных в кольцо. m=8 П(k)=21, 22, 13, 15, 29, 30, 37, 39 для ![]() ![]() Коллизия разрешается сдвигом на один шаг по «кольцу» и повторной пробой до тех пор, пока не будет найдена свободная ячейка. Можно построить Н – функцию, которая сдвигает пробу на несколько шагов, например, на 2. ![]() Оказывается, такая функция приводит к неразрешимой коллизии (см. рис. 17). Кольцевые Н – функции уменьшают количество проб и увеличивают плотность заполнения ячеек таблицы, но как показано на примере рис. 17 могут привести к неразрешимым коллизиям. Выбор той или иной кольцевой Н – функции, дающее минимальное число проб и максимальную плотность заполнения, является нерешённой проблемой. Хорошей кольцевой Н – функцией считается функция расстановки с квадратичным шагом. ![]() П(k)= 21, 22, 13, 15, 29, 30, 37, 39 m=8 h(k)= 5, 6, 5, 7, 5, 5, 6, 5, 7 n=8 ![]()
Рис.16 Кольцевая Н – таблица с – функцией, зависящей от номера пробы 1 – шаговым сдвигом. ![]() П(k)= 21, 22, 13, 15, 29, 30, 37, 39 m=8 h(k)= 5, 6, 5, 7, 5, 5, 6, 5, 7 n=8 ![]() ![]()
Запись с ключом 37 не может разместиться Рис. 17. Пример Н – функции с неразрешимой коллизией. Кольцевая Н – таблица с Н – функцией, зависящая от номера пробы с 2 – шаговым сдвигом ![]() 7.4. Н – таблица с цепочками Другой способ разрешения коллизий – организация Н – таблицы с цепочками (линейными списками). В этом случае значение ![]() ![]() Например, для ![]()
класс i = 0 класс i = 1 класс i = 2 класс i = 3 Рис. 18. Н – таблица с цепочками
|