МИНИСТЕРСТВО ОБРАЗОВАНИЯ МОСКОВСКОЙ ОБЛАСТИ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ СРЕДНЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ МОСКОВСКОЙ ОБЛАСТИ ДМИТРОВСКИЙ ГОСУДАРСТВЕННЫЙ ПОЛИТЕХНИЧЕСКИЙ КОЛЛЕДЖ МЕТОДИЧЕСКОЕ ПОСОБИЕ ПО ТЕОРИИ АЛГОРИТМОВ СТРУКТУРЫ ДАННЫХ специальность 09.02.03 программирование в компьютерных системах Дмитров 2014 Основные понятия Обработка на персональных электронных вычислительных машинах (ПЭВМ) данных реального мира требует, чтобы их структура была определена и точно представлена в ПЭВМ. Структура данных определяет семантику данных, а также способы организации и управления данными. Информация, представленная в виде последовательности символов и предназначенная для обработки на ПЭВМ, называется данными. Организация данных для обработки является важным этапом разработки программ. Для реализации многих приложений выбор структуры данных – единственное важное решение: когда выбор сделан, разработка алгоритмов не вызывает затруднений. Для одних и тех же данных различные структуры будут занимать неодинаковое дисковое пространство. В основе работы компьютера лежит умение оперировать только с одним видом данных – с отдельными битами, или двоичными цифрами. Все обрабатываемые компьютером данные, в конечном счете, разбиваются на отдельные биты. Задачи, которые решаются с помощью компьютера, редко выражаются на языке битов. Как правило, данные имеют форму чисел, литер, текстов, символов и более сложных структур. Структура данных относится по существу к пространственным понятиям: ее можно свести к схеме организации информации в памяти компьютера. Однако, структура данных не является пассивным объектом: необходимо принимать во внимание выполняемые с ней операции (и алгоритмы, используемые для этих операций). Структуры данных, применяемые в алгоритмах, могут быть чрезвычайно сложными. В результате выбор правильного представления данных часто служит ключом к удачному программированию и может в большей степени сказываться на производительности программы, чем детали используемого алгоритма. 2 Независимо от содержания и сложности любые данные в памяти ЭВМ представляются последовательностью двоичных разрядов, или битов, а их значениями являются соответствующие двоичные числа. Данные, рассматриваемые в виде последовательности битов, имеют очень простую организацию или, другими словами, слабо структурированы. Для человека описывать и исследовать сложные данные в терминах последовательностей битов весьма неудобно. Вообще говоря, под структурой понимается способ представления информации, посредством которого совокупность отдельно взятых элементов образует нечто единое, обусловленное их взаимосвязью друг с другом. В общем же случае, структура данных – это множество элементов данных и множество связей между ними. Такое определение охватывает все возможные подходы к структуризации данных, но в каждой конкретной задаче используются те или иные его аспекты. Поэтому вводится дополнительная классификация структур данных, направления которой соответствуют различным аспектам их рассмотрения. Прежде чем приступать к изучению конкретных структур данных, дадим их общую классификацию по нескольким признакам. Понятие физическая структура данных отражает способ физического представления данных в памяти машины и называется еще структурой хранения, внутренней структурой или структурой памяти. Рассмотрение структуры данных без учета ее представления в машинной памяти называется абстрактной или логической структурой. В общем случае между логической и соответствующей ей физической структурами существует различие, которое зависит от самой структуры и особенностей той среды, в которой она должна быть отражена. Вследствие этого различия существуют процедуры, осуществляющие отображение логической структуры в физическую и, наоборот, физической структуры в логическую. Эти процедуры обеспечивают, кроме того, доступ к физическим структурам и выполнение над ними различных операций, причем каждая операция 3 рассматривается применительно к логической или физической структуре данных. Все данные подразделяются на несколько типов, причем понятие тип связывается не только с представлением данных в адресном пространстве, но и со способом их обработки. Иначе говоря, тип данных – это множество значений и набор операций с ними. Классификация структуры данных Различают простые (базовые, примитивные) структуры (типы) данных и интегрированные (структурированные, композитные, сложные). Простыми называются такие структуры данных, которые не могут быть расчленены на составные части, большие, чем биты. С точки зрения физической структуры важным является то обстоятельство, что в данной машинной архитектуре, в данной системе программирования всегда можно заранее сказать, каков будет размер выбранного простого типа и какова структура его размещения в памяти. С логической точки зрения простые данные являются неделимыми единицами. В подавляющем большинстве случаев, простые структуры представляются в виде переменных. Переменная – поименованная область памяти, адрес которой можно использовать для осуществления доступа к данным. Данные, находящиеся в переменной (т.е. по данному адресу памяти), называются значением этой переменной. Иначе говоря, это небольшая область в оперативной памяти компьютера, куда во время работы программы можно занести и хранить некоторое значение, которое в дальнейшем можно использовать или изменять. Более простым языком, переменная – это некий объект, которому дано имя и который может принимать различные значения. Простые структуры (типы) данных включают целые и вещественные числа, логические и символьные значения. Таблица 1. Базовые типы данных в Pascal и C++. Тип данных Pascal C++ 4 Целое число Вещественное число Логическое значение Символьное значение integer real/double boolean char int float/double bool char К простым типам данных также относится указатель. Указатель – переменная, диапазон значений которой состоит из адресов ячеек памяти или специального значения – нулевого адреса. Интегрированными называются такие структуры данных, составными частями которых являются другие структуры данных – простые или в свою очередь интегрированные. Интегрированные структуры данных конструируются программистом с использованием средств интеграции данных, предоставляемых языками программирования. Между отдельными элементами структур могут наличествовать или отсутствовать явно заданные связи. В зависимости от этого следует различает несвязные структуры (векторы, массивы, строки, стеки, очереди) и связные структуры (связные списки). По признаку изменчивости различают структуры статические, полустатические и динамические. Под изменчивостью понимают изменение числа элементов структуры или связей между этими элементами. Статические структуры представляют собой структурированное множество примитивных, базовых, структур. Изменчивость несвойственна статическим структурам, т.е. размер памяти компьютера, отводимый для таких данных, постоянен и выделяется на этапе компиляции или выполнения программы. К таким структурам относят: массивы (array), записи или структуры (record or struct), векторы (vector or one-dimensional array), множества, таблицы (two-dimensional array). Прежде чем рассматривать полустатические структуры, дадим вспомогательное определение линейного списка. Линейный список – это структура данных, состоящая из элементов одного типа, связанных между собой последовательно посредством указателей. Каждый элемент списка 5 имеет указатель на следующий элемент. Проще говоря, линейный список – это линейная последовательность элементов, каждый из которых содержит указатель (ссылается) на соседа, следующего за ним. Полустатические структуры – это последовательные линейные списки с переменной длиной, ограниченной фиксированной максимальной величиной и с ограниченным доступом. Если полустатическую структуру рассматривать на логическом уровне, то о ней можно сказать, что это последовательность данных, связанная отношениями линейного списка. Доступ к элементу может осуществляться по его порядковому номеру. Физическое представление полустатических структур в памяти – это обычно последовательность слотов в памяти, где каждый следующий элемент расположен в памяти в следующем слоте. К таким структурам относятся стеки (stack), очереди (queue), деки (deque) и строки (string). Динамические структуры не имеют постоянного размера, поэтому память под отдельные элементы таких структур выделяется в момент, когда они создаются в процессе выполнения программы, а не во время трансляции. Когда в элементе структуры больше нет необходимости, занимаемая им память освобождается (элемент «разрушается»). Поскольку элементы динамической структуры располагаются в памяти не по порядку и даже не в одной области, адрес элемента такой структуры не может быть вычислен из адреса начального или предыдущего элемента. Связь между элементами динамической структуры устанавливается через указатели, содержащие адреса элементов в памяти. К таким структурам относят: связные списки (linked list), графы (graph), деревья (tree). По признаку упорядоченности элементов структуры можно делить на линейные и нелинейные. Примерами линейных структур являются – векторы, строки, массивы, стеки, очереди, односвязные и двусвязные списки. К нелинейным структурам относят – многосвязные списки, деревья и графы. 6 Базовые структуры данных, статические, полустатические и динамические характерны для оперативной памяти и часто называются оперативными структурами. Файловые структуры соответствуют структурам данных для внешней памяти, где в качестве единицы хранения данных принят объект переменной длины, называемый файлом. Файл – это последовательность произвольного числа байтов, обладающая уникальным собственным именем. Различают последовательные файлы, файлы, организованные разделами и файлы прямого доступа. Виды структур данных Массив Наиболее широко известной структурой данных является массив. Массив – это последовательность элементов одного типа, называемого базовым, которая хранится в виде непрерывного ряда, т.е. элементы расположены непосредственно друг за другом. Поскольку массив состоит из компонент одного типа, то его структура является однородной. Массивы относят к так называемым структурам со случайным доступом. Доступ к элементам массива осуществляется по индексу. Индекс – это значение специального типа, которое указывает на конкретный элемент массива, обычно является неотрицательным целым числом. Каждый элемент массива имеет уникальный набор значений индексов. Другим понятием, связанным с массивами, является размерность массива. Размерность массива – это количество индексов, необходимое для однозначного доступа к элементу массива. Массивы бывают одномерными (вектор), двумерными (матрица) и т.д. В общем случае массив может быть многомерным или n-мерным. Таблица 2. Создание массивов в Pascal и C++. Размерность массива Одномерный Pascal C++ имя_массива = array тип_массива имя_массива [список_индексов] of тип_массива [количество_элементов] mas = array[1..10] of integer; int mas[10]; 7 Двумерный имя_массива = array [список_индексов] of array [список_индексов] of тип_массива или имя_массива = array [список_индексов, список_индексов] of тип_массива mas = array[1..5] of array[1..5] of integer; или mas = array[1..5, 1..5] of integer; тип_массива имя_массива [количество_элементов] [количество_элементов] int mas[5][5]; Важнейшая операция над массивом – доступ к заданному элементу. Обращение к элементу массива выполняется по имени массива и значениям индексов данного элемента. Как только реализован доступ к элементу, над ним может быть выполнена любая операция, имеющая смысл для того типа данных, которому соответствует элемент. Таблица 3. Доступ к элементу массива в Pascal и C++. Размерность массива Одномерный Двумерный Pascal mas[5]; //доступ к 5-ому элементу массива mas[2,3]; //доступ к элементу во 2-ой строке и 3ем столбце C++1 mas[4]; //доступ к 5-ому элементу массива mas[1,2]; //доступ к элементу во 2-ой строке и 3ем столбце В случае двумерных массивов существует соглашение, по которому первый индекс идентифицирует строку элемента, а второй – столбец. Виды массивов 1. Статический (был рассмотрен выше); 2. Динамический – это массив, размер которого может меняться во время исполнения программы; 3. Гетерогенный – это массив, в разные элементы которого могут быть непосредственно записаны значения, относящиеся к различным типам данных. Достоинства 1 В C++ индексация массивов начинается с нуля и только с нуля. 8 Легкость вычисления адреса элемента по его индексу (поскольку элементы массива располагаются один за другим); Одинаковое время доступа ко всем элементам; Малый размер элементов. Недостатки Отсутствие динамики, невозможность удаления или добавления элемента без сдвига других (для статического массива). Множество Множество – это структура данных, которая представляет собой набор неповторяющихся данных одного и того же типа. Множество в памяти хранится как массив битов, в котором каждый бит указывает, является ли элемент принадлежащим объявленному множеству или нет. Операции над множествами: 1. Объединение. Объединением множеств A и B называется множество, состоящее из всех тех элементов, которые принадлежат хотя бы одному из множеств A или B. 2. Пересечение. Пересечением множеств A и B называется множество, состоящее из всех тех и только тех элементов, которые принадлежат и A, и B. 3. Разность. Разностью множеств A и B называется множество всех тех и только тех элементов A, которые не содержатся в B. 4. Вхождение. Операция вхождения – это операция, устанавливающая связь между множеством и скалярной величиной. Таблица 4. Создание множества в Pascal и C++. Множество Pascal2 имя_множества: тип_множества st: set of char; set C++ (STL) of set<тип_множества> имя_множества set<int> st; В Pascal базовым типом множества могут быть только те типы, которые не превышают 256 возможных значений. К таким типам относятся byte, char и производные от них типы. 2 9 Запись/структура Наиболее общий метод получения составных типов заключается в объединении элементов произвольных типов. Причем сами эти элементы могут быть в свою очередь составными. Рассмотрим пример из обработки данных: человек описывается с помощью нескольких подходящих характеристик вроде имени, фамилии, даты рождения, пола и семейного положения. К данным такой природы стало широко применяться слово «запись». Запись – это конечно упорядоченное множество полей, характеризующихся различным типом данных. Записи являются удобным средством для представления программных моделей реальных объектов предметной области, ибо, как правило, каждый такой объект обладает набором свойств, характеризуемых данными различных типов. В памяти эта структура записи может быть представлена в одном из двух видов: 1. в виде последовательности полей, занимающих непрерывную область памяти. При такой организации достаточно иметь один указатель на начало области и смещения относительно начала. 2. в виде связного списка с указателями на значения полей записи. При такой организации имеет место быстрое обращение к элементам, но очень неэкономичный расход памяти для хранения. Полем записи может быть в свою очередь интегрированная структура данных – вектор, массив или другая запись (но ни в коем случае не такая же). Понятие структура является синонимом к понятию «запись». Структура – это совокупность нескольких переменных, часто различных типов, сгруппированных под единым именем для удобства обращения. Таблица 5. Создание записи/структуры в Pascal и C++. Pascal (запись) C++ (структура) 10 Запись/структура type имя_записи = record поле1: тип; поле2: тип; … полеN: тип; end; struct имя_структуры { тип поле1; тип поле2; … тип полеN; }; type Person = record first_name:char[25]; last_name: char[25]; date_of_birth: char[8]; sex: char; marital_status: char[16]; end; struct Person { char[25] first_name; char[25] last_name; char[8] date_of_birth; char sex; char[16] marital_status; }; Важнейшей операцией для записи является операция доступа к выбранному полю записи (тоже самое аналогично для структур). Практически во всех языках программирования обозначение этой операции имеет вид: <имя_переменной_записи>.<имя_поля> Над выбранным полем записи возможны любые операции, допустимые для типа этого поля. Большинство языков программирования поддерживает некоторые операции, работающие с записью, как с единым целым, а не с отдельными ее полями. Это операция присваивания одной записи значения другой однотипной записи. Тоже самое справедливо и для структур. Стек Стек – это структура данных, представляющая собой список элементов, для которой добавление и удаление элементов выполняются только с одной стороны списка, называемого вершиной стека. Данный принцип носит название LIFO (last in – first out) – «последним пришел – первым вышел». Чаще всего принцип работы стека сравнивают со стопкой тарелок: чтобы взять вторую сверху, нужно снять верхнюю. Другим примером стека является магазин в огнестрельном оружии: стрельба начнется с патрона, заряженного последним. 11 Основные операции над стеком – включение (добавление) нового элемента – push, и исключение (удаление) элемента из стека – pop. Операция push помещает новый элемент на вершину стека, который теперь указывает на элемент, бывший до этого вершиной стека. Операция pop удаляет верхний элемент стека, а вершиной стека становится тот, на который указывал удаленный из стека элемент. На практике список операций может быть дополнен проверкой стека на пустоту (isEmpty) и получением размера стека (size). Рассмотрим пример, демонстрирующий принцип включения элементов в стек и исключения элементов из стека. На рисунке ниже изображены состояния стека: а) пустого; б-г) после последовательного включения в него элементов с именами ‘A’, ‘B’, ‘C’; д-е) после последовательного исключения из стека элементов ‘C’ и ‘B’; ж) после включения в стек элемента ‘D’. Рисунок 1. Принцип работы стека. Зачастую стек реализуется в виде однонаправленного списка (каждый элемент списка указывает только на следующий). Но в таком случае невозможно применить операцию обхода элементов. Кроме того можно организовать стек на обыкновенном массиве. 12 Реализации Для стека с n элементами требуется O(n) памяти, так как она нужна лишь для хранения самих элементов. На массиве Перед реализацией стека выделим ключевые поля: s[1..n] – массив, с помощью которого реализуется стек, способный вместить не более n элементов; s.top – индекс последнего помещенного в стек элемента. Стек состоит из элементов s[1..s.top], где s[1] – элемент на дне стека, а s[s.top] – элемент на его вершине. Если s.top = 0, то стек не содержит ни одного элемента и является пустым (empty). Если элемент снимается с пустого стека, говорят, что он опустошается (underflow), что обычно приводит к ошибке. Если значение s.top больше n, то стек переполняется (overflow). Реализация стека S с помощью массива. Элементы стека находят только в тех позициях массива, которые отмечены светло-серым цветом. (а)Стек S состоит из четырех элементов. На вершине стека находится элемент 9. (б)Стек S после вызовов Push(S, 17) и Push(S, 3). (в)Стек S после вызова Pop(S), которая возвращает помещенное в стек последним значение 3. Несмотря на то, что элемент 3 все еще находится в массиве, он больше не принадлежит стеку; теперь на вершине стека располагается элемент 17. В данном случае, все операции со стеком выполняются за O(1). На списке Стек можно реализовать и на списке. Для этого необходимо создать список и операции работы стека на созданном списке. Рассмотрим пример реализации стека на односвязном списке. Стек будем «держать» за голову. Добавляться новые элементы посредством операции push будут перед головой, сами при этом становясь новой головой, а элементом для изъятия из 13 стека с помощью pop будет текущая голова. После вызова push текущая голова уже станет старой и будет являться следующим элементом за добавленным, то есть ссылка на следующий элемент нового элемента будет указывать на старую голову. После вызова pop будет получена и возвращена информация, хранящаяся в текущей голове. Сама голова будет изъята из стека, а новой головой станет элемент, который следовал за изъятой головой. Ключевыми полями будут: head.data – значение в верхушке стека; head.next – значение следующее за верхушкой стека. В данной реализации стека, кроме самих данных, хранятся указатели на следующие элементы, которых столько же, сколько и элементов, то есть, так же n. Стоит заметить, что потребуется O(n) дополнительной памяти на указатели. Очередь Очередью называется такой последовательный список с переменной длиной, в котором включение (добавление) элементов выполняется только с одной стороны списка (эту сторону часто называют концом или хвостом очереди), а исключение – с другой стороны (называемой началом или головой очереди). Данный принцип носит название FIFO (first in – first out) – «первым пришел – первым вышел». Типичным примером очереди FIFO являют самые обычные очереди в магазинах. Основные операции над очередью те же, что и над стеком. Добавления элемента (enqueue – поставить в очередь) возможно лишь в конец очереди, выборка (dequeue – убрать из очереди) – только из начала очереди, при этом выбранный элемент из очереди удаляется. Аналогично стеку, список операций можно расширить проверкой на пустоту (isEmpty) и получением размера(size). 14 Рисунок 2. Принцип LIFO. Реализации На массиве Очередь, способную вместить не более n элементов, можно реализовать с помощью массива elements[0..n-1]. Она будет обладать следующими полями: head – голова очереди; tail – хвост очереди. Рисунок 3. Реализация очереди на массиве. Реализация очереди с помощью массива Q[1..12]. элементы очереди содержатся только в светло-серых ячейках. (а)Очередь содержит пять элементов в позициях Q[7..11]. (б)Конфигурация очереди после вызовов 15 Enqueue(Q, 17), Enqueue(Q, 3) и Enqueue(Q, 5). (в)Конфигурация очереди после того, как вызов Dequeue(Q) вернул ключевое значение 15, ранее находившееся в голове очереди. Новый элемент в голове очереди имеет ключ 6. Из-за того, что не нужно перевыделять память, каждая операция выполняется за O(1) времени. Плюсы: простота в разработке; по сравнению с реализацией на списке есть незначительная экономия памяти. Минусы: количество элементов в очереди ограничено размером массива (исправляется написанием функции расширения массива); при переполнении очереди требуется перевыделение памяти и копирование всех элементов в новый массив. На списке Для данной реализации очереди необходимо создать список list и операции работы на созданном списке. Данная очередь будет обладать следующими полями: x.value – поле, в котором хранится значение элемента; x.next – указатель на следующий элемент очереди. Плюсы: каждая операция выполняется за время O(1). Минусы: Память фрагментируется гораздо сильнее и последовательная итерация по такой очереди может быть ощутимо медленнее, нежели итерация по очереди реализованной на массиве. Дек (двусвязная очередь) Дек – это структура данных, в которой добавление новых и удаление существующих элементов допустимо с любого конца. Такие образом реализуются принципы FIFO и LIFO. 16 Иначе дек называют «двусторонней очередью»: deque – double ended queue. Поскольку дек, по своей структуре, является очередью, то над ним определены те же самые операции: включение (добавление) в конец очереди (pushBack), включение (добавление) в начало очереди (pushFront), исключение (удаление) из конца очереди (popBack), исключение (удаление) из начала очереди (popFront). Данный набор операций можно расширить проверкой на пустоту (isEmpty) и получением размера (size). Рисунок 4. Принципы LIFO и FIFO в деке. На рисунке ниже приведен пример последовательности состояний дека при включении и удалении пяти элементов. На каждом этапе стрелка указывает с какой стороны дека осуществляется включение элемента. 17 Рисунок 5. Принцип работы дека. Как и в случае с очередью, дек может быть реализован на массиве или на списке. Строка Строка – это линейно упорядоченная последовательность символов, принадлежащих конечному множеству символов, называемому алфавитом. Строки обладают следующими важными свойствами: длина, как правило, переменна, хотя алфавит фиксирован; обращение к символам строки идет с какого-нибудь одного конца последовательности, т.е. важна упорядоченность этой последовательности, а не ее индексация; чаще всего целью доступа к строке является не отдельный ее элемент (хотя это тоже не исключается), а некоторая цепочка символов в строке. Таблица 6. Создание строки в Pascal и C++. Строка Pascal имя_строки: string[n]3 или имя_строки: string C++ string имя_строки str: string[10]; или str: string; string str; Базовыми операциями над строками являются: получение символа по индексу; определение длины строки; присваивание строк; сравнение строк. Операция сравнения строк производится по следующим правилам. Сравниваются первые символы двух строк. Если символы не равны, то строка, содержащая символ, место которого в алфавите ближе к началу, считается меньшей. Если символы равны, n – максимально возможная длина строки – целое число в диапазоне 1..255. Если этот параметр опущен, то по умолчанию он принимается равным 255. 3 18 сравниваются вторые, третьи и т.д. символы. При достижении конца одной из строк, строка меньшей длины считается меньшей. При равенстве длин строк и попарном равенстве всех символов в них строки считаются равными. конкатенация (сцепление) строк. Результатом операции сцепления двух строк является строка, длина которой равна суммарной длине строкоперандов, а значение соответствует значению первого операнда, за которым непосредственно следует значение второго операнда. выделение подстроки. Операция выделения подстроки выделяет из исходной строки последовательность символов, начиная с заданной позиции n, с заданной длиной l. поиск вхождения. Операция поиска вхождения находит место первого вхождения подстроки-эталона в исходную строку. Результатом операции может быть номер позиции в исходной строке, с которой начинается вхождения эталона или указатель на начало вхождения. В случае отсутствия вхождения результатом операции должно быть специальное значение, например, нулевой номер позиции или пустой указатель. Представление строк Некоторые языки программирования накладывают ограничения на максимальную длину строки, но в большинстве языков подобные ограничения отсутствуют. Для представления строк в памяти компьютера существуют два принципиально разных подхода. Представление массивом символов В этом подходе строки представляются массивом символов; при этом размер массива хранится в отдельной (служебной) области. Впервые данный метод был реализован в языке Pascal, поэтому данный метод получил название Pascal strings. Преимущества 19 программа в каждый момент времени «знает» о размере строки, и операции добавления символов конец, копирования и получения размера строки выполняются достаточно быстро; строка может содержать любые данные; возможно на программном уровне следить за выходом за границы строки при ее обработке; возможно быстрое выполнение операции вида «взятие N-ого символа с конца строки». Недостатки проблемы с хранением и обработкой символов произвольной длины; увеличение затрат на хранение строк – значение «длина строки» также занимает место и в случае большого количества строк маленького размера может существенно увеличить требования алгоритма к оперативной памяти; ограничение максимального размера строки. В современных языках программирования это ограничение скорее теоретическое, так как обычно размер строки хранится в 32-битовом поле, что дает максимальный размер строки в 4 гигабайта. Метод «завершающего байта» Второй метод заключается в использовании «завершающего байта». Одно из возможных значений символов алфавита (как правило, это символ с кодом 0) выбирается в качестве признака конца строки, и строка хранится как последовательность байтов от начала до конца. Метод имеет три названия – ASCIIZ (символы в кодировке ASCII с нулевым завершающим байтом), C-strings (наибольшее распространение метод получил именно в языке Си) и метод нуль-терминированных строк. Преимущества отсутствие дополнительной служебной информации о строке (кроме завершающего байта); 20 возможность представления строки без создания отдельного типа данных; отсутствие ограничения на максимальный размер строки; экономное использование памяти; простота передачи строк в функции (передается указатель на первый символ); возможность использовать алфавит с переменным размером символа (UTF-8). Недостатки долгое выполнение операций получения длины и конкатенации строк; отсутствие средств контроля за выходом за пределы строки, в случае повреждения завершающего байта возможность повреждения больших областей памяти, что может привести к непредсказуемым последствиям – потере данных, краху программы и даже все системы; невозможность использовать символ завершающего байта в качестве элемента строки. В стандартном Pascal строка выглядит как массив из 256 байтов; первый байт хранит длину строки, в остальных хранится ее тело. Таким образом, длина строки не может превышать 255 символов. Связный список Списком называется упорядоченное множество, состоящее из переменного числа элементов, к которым применимы операции включения и исключения. Список, отражающий отношения соседства между элементами, называется линейным. Логические списки мы уже рассматривали, но там речь шла о полустатических структурах данных и на размер списка накладывались ограничения. Если ограничения на длину списка не допускаются, то список является динамической структурой данных. 21 Связный список – это динамическая структура данных, состоящая из узлов, каждый из которых содержит как собственно данные, так и одну или две ссылки («связки») на следующий и/или предыдущий узел списка. Односвязный список Ссылка в каждом узле указывает на следующий узел в списке. В односвязном списке можно передвигаться только в сторону конца списка. Узнать адрес предыдущего элемента, опираясь на содержимое текущего узла, невозможно. На рисунке ниже приведена структура односвязного списка. Отметим, что в поле указателя последнего элемента списка находится константа NULL, свидетельствующая о конце списка. Рисунок 6. Односвязный список. Двусвязный список Обработка односвязного списка не всегда удобна, так как отсутствует возможность продвижения в противоположную сторону. Такую возможность обеспечивает двусвязный список. Здесь ссылки в каждом узле указывают на предыдущий и на последующий узел в списке. По двусвязному списку можно передвигаться в любом направлении – как к началу, так и к концу. В этом списке проще производить удаление и перестановку элементов, так как всегда известны адреса тех элементов списка, указатели которых направлены на изменяемый элемент. Ниже представлена структура двусвязного списка. Также как и в случае односвязного списка, в полях указателей крайних элементов находится константа NULL. 22 Рисунок 7. Двусвязный список. Операции над списком: поиск элемента в списке – search(S,k); вставка элемента в список: o вставка элемента в конец списка – pushBack(S, x); o вставка элемента в начало списка – pushFront(S, x); o вставка элемента в произвольную позицию – insert(S, i, x); удаление элемента из списка: o удаление последнего элемента – popBack(S); o удаление первого элемента – popFront(S); o удаление элемента из произвольной позиции – erase(S, i); проверка списка на пустоту – empty(S); получение размера списка – size(S); слияние двух списков (для односвязного списка) – merge(S1, S2). Операция слияния заключается в формировании из двух списков одного. Последний элемент первого списка содержит пустой указатель на следующий элемент, этот указатель служит признаком конца списка. Вместо этого пустого указателя в последний элемент первого списка заносится указатель на начало второго списка. Таким образом, второй список становится продолжением первого. Линейные списки находят широкое применение в приложениях, где непредсказуемы требования на размер памяти, необходимой для хранения данных; большое число сложных операций надо данными, особенно 23 включений и исключений. На базе линейных списков могут строится стеки, очереди и деки. Преимущества легкость добавления и удаления элементов; размер ограничен только объемом памяти компьютера и разрядностью указателей; динамическое добавление и удаление элементов. Недостатки сложность определения адреса элемента по его индексу в списке; на поля-указатели (указатели на следующий и предыдущий элемент) расходуется дополнительная память; работа со списком медленнее, чем с массивами, так как к любому элементу списка можно обратиться, только пройдя все предшествующие ему элементы; элементы списка могут быть расположены в памяти разреженно, что окажет негативный эффект на кэширование процессора4. Граф Граф – это сложная нелинейная многосвязная динамическая структура, отображающая свойства и связи сложного объекта. *Граф 𝐺 = (𝑉, 𝐸) состоит из множества вершин 𝑉 и множества ребер 𝐸. В узлах графа содержится информация об элементах объекта. Связи между узлами задаются ребрами графа. Ребра графа могут иметь направленность, показываемую стрелками, тогда они называются ориентированными, ребра без стрелок – неориентированные. Кэш – промежуточный буфер с быстрым доступом, содержащий информацию, которая может быть запрошена с наибольшей вероятностью. Кэш микропроцессора – кэш (сверхоперативная память), используемый микропроцессором компьютера для уменьшения среднего времени доступа к компьютерной памяти. Кэш использует небольшую, очень быструю память, которая хранит копии часто используемых данных из основной памяти. 4 24 Граф, все ориентированным связи которого графом или ориентированные, орграфом; граф называется со всеми неориентированными связями – неориентированным графом; граф со связями обоих типов – смешанным графом. Многосвязная структура обладает следующими свойствами: на каждый элемент (узел, вершину) может быть произвольное количество ссылок; каждый элемент может иметь связь с любым количеством других элементов; каждая связка (ребро, дуга) может иметь направление и вес. Для ориентированного графа число ребер, входящих в узел, называется полустепенью захода узла, выходящих из узла – полустепенью исхода. Количество входящих и выходящих ребер может быть любым, в том числе и нулевым. Если ребрам графа соответствуют некоторые значения, то граф и ребра называются взвешенными. Мультиграфом называется граф, имеющий параллельные (соединяющий одни и те же вершины) ребра, в противном случае граф называется простым. Путь в графе – это последовательность узлов, связанных ребрами; элементарным называется путь, в котором все ребра различны, простым называется путь, в котором все вершины различны. Путь от узла к самому себе называется циклом. Узел называется инцидентным к ребру, если он является его вершиной, т.е. ребро направлено к этому узлу. Существует два способа логического представления графа – матрица смежности и матрица инцидентности. Матрица инцидентности – это матрица, строки которой обозначены вершинами графа, а столбцы обозначены ребрами графа. Элемент матрицы инцидентности равен 1, если вершина инцидентна ребру, и равен 0 в противном случае. 25 Рисунок 8. Граф и его матрица инцидентности. Матрицы инцидентности не имеют большого значения при рассмотрении ориентированных графов, т.к. они не содержат информации о том, как ребро ориентировано. Поэтому, используя матрицу инцидентности, нельзя восстановить ориентированный граф. Матрица смежности – это матрица, строки которой обозначены вершинами графа и столбцы обозначены теми же вершинами в том же самом порядке. Элемент матрицы смежности равен 1, если имеется ребро между двумя вершинами, и равен 0 в противном случае. Рисунок 9. Граф и его матрица смежности. Существует два основных метода представления графов в памяти компьютера: матричный, т.е. массивами, и связными нелинейными списками. С графами связано множество алгоритмов. Среди них алгоритм нахождения кратчайшего пути (алгоритм Дейкстры и алгоритм ФлойдаУоршолла). Дерево Дерево – это совокупность элементов, называемых узлами (один из которых определен как корень), и отношений, образующих иерархическую структуру узлов. Дерево – это граф без циклов. Дерево – это граф, который характеризуется следующими свойствами: существует единственный элемент (узел или вершина), на который не ссылается никакой другой элемент и который называется корнем; 26 начиная с корня и следуя по определенной цепочке указателей, содержащихся в элементах, можно осуществить доступ к любому элементу структуры; на каждый элемент, кроме корня, имеется единственная ссылка, т.е. каждый элемент адресуется единственным указателем. Рисунок 10. Дерево. Деревья нужны для описания любой структуры с иерархией. Традиционные примеры таких структур: генеалогические деревья, иерархия должностей в организации, алгебраическое выражение, включающее операции, для которых предписаны определенные правила приоритета. Ориентированное дерево – это такой ациклический ориентированный граф, у которого одна вершина, называемая корнем, имеет полустепень захода, равную 0, остальные вершины имеют полустепени захода, равные 1. Введем еще некоторые понятия, связанные с деревьями. На рисунке ниже показано дерево: Узел X называется предком (или отцом), а узлы Y и Z называются наследниками (или сыновьями) – между собой их называют братьями. Причем левый сын является старшим сыном, а правый – младшим. Если наибольшая из степеней исхода для вершины дерева равна m, тогда дерево называется m-арным деревом. В частном случае, когда m = 2, дерево называется бинарным деревом. 27 Деревья можно представлять с помощью массивов и связных списков. Представление деревьев с помощью массивов Пусть T – дерево с узлами 1, 2, …, n. Самым простым представлением дерева T будет линейный массив A, где каждый элемент A[i] является указателем (или курсором) на родителя узла i. Корень дерева T отличается от других узлов тем, что имеет нулевой указатель или указатель на самого себя как на родителя. *В языке Pascal указатели на элементы массива недопустимы, поэтому мы будем использовать схему с курсорами, тогда A[i] = j, если узел j является родителем узла i, и A[i] = 0, если узел i является корнем. Рисунок 11. Дерево и его представление линейный массивом. Данное представление использует то свойство деревьев, что каждый узел, отличный от корня, имеет только одного родителя. Используя это представление, родителя любого узла можно найти за фиксированное время. Прохождение по любому пути, т.е. переход по узлам от родителя к родителю, можно выполнить за время, пропорциональное количеству узлов пути. Представление деревьев с использованием списком сыновей Важный и полезный способ представления деревьев состоит в формировании для каждого узла списка его сыновей. Так как число сыновей у разных узлов может быть разное, чаще всего для этих целей применяются связные списки. На рисунке ниже показано, как таким способом представить дерево. 28 Рисунок 12. Дерево и его представление списком сыновей. Здесь есть массив ячеек заголовков, индексированный номерами (они же имена) узлов. Каждый заголовок (header) указывает на связный список, состоящий из «элементов»-узлов. Элементы списка header[i] являются сыновьями узла i, например узлы 9 и 10 – сыновья узла 3. Основные операции над деревьями Поиск узла с заданным ключом; Добавление нового узла; Удаление узла (поддерева); Обход дерева в определенном порядке: o Нисходящий обход (Preorder); o Смешанный обход (Inorder); o Восходящий обход (Postorder). Файловые структуры данных Последовательные файлы Последовательный неупорядоченный файл является простейшей структурой данных на внешней памяти. Очевидно, что при последовательном доступе к такому файлу достигаются минимальные потери производительности. Однако, при необходимости выбирать записи в произвольном порядке рассмотренные выше затраты времени относятся к 29 каждой выбираемой записи. Поиск в неупорядоченном последовательном файле возможен только линейный, что совершенно неприемлемо для большинства приложений. Если последовательный файл будет упорядочен, то в нем можно применять дихотомический поиск. Однако, сортировка последовательного файла представляет собой достаточно сложную и затратную задачу. Файлы прямого доступа Файл прямого доступа, как правило состоит из двух частей: индекса и пространства данных. Индекс и является, собственно таблицей прямого доступа, каждая запись которой содержит только ключ, адрес соответствующей записи в области данных и адрес следующей записи в возможной цепочке переполнений (для разрешения коллизий используются раздельные цепочки переполнений). Область данных – неупорядоченный последовательный файл. Обычно индекс или его часть считывается в оперативную память и поиск по ключу ведется, по возможности, в оперативной памяти. Если запись файла прямого доступа содержит несколько полей, которые могут использоваться в качестве ключа, для каждого ключа строится свой индекс при одной общей области данных. Существенным недостатком файла прямого доступа является чрезвычайная затратность операций, требующих обработки всех хранящихся в файле записей в определенном порядке. Поэтому область применения таких файлов ограничивается теми приложениями, которые сводятся к поиску записей и почти никогда не требуют последовательной обработки. Сущности, связанные с работой с файлом: хэндлер файла, или дескриптор (описатель). При открытии файла (в случае, если это возможно), операционная система возвращает число (или указатель на структуру), с помощью которого выполняются все остальные файловые операции. По их завершению файл закрывается, а хэндлер теряет смысл. 30 файловый указатель. Число, являющееся смещением относительно нулевого байта в файле. Обычно по этому адресу осуществляется чтение/запись, в случае, если вызов операции чтения/записи не предусматривает указание адреса. При выполнении операций чтения/записи файловый указатель смещается на число прочитанных (записанных) байт. Последовательный вызов операций чтения таким образом позволяет прочитать весь файл, не заботясь о его размере. файловый буфер. Операционная система (и/или библиотека языка программирования) осуществляет кэширование файловых операций в специальном буфере (участке памяти). При закрытии файла буфер сбрасывается. режим доступа. В зависимости от потребностей программы, файл может быть открыт на чтение и/или запись. Кроме того, некоторые операционные системы (и/или библиотеки) предусматривают режим работы с текстовыми файлами. Режим обычно указывается при открытии файла. Операции над файлами Открытие файла (обычно в качестве параметров передается имя файла, режим доступа и режим совместного доступа, а в качестве значения выступает файловый хэндлер или дескриптор); Закрытие файла. В качестве аргумента выступает значение, полученное при открытии файла. Запись — в файл помещаются данные. Чтение — данные из файла помещаются в область памяти. Перемещение указателя — указатель перемещается на указанное число байт вперёд/назад или перемещается по указанному смещению относительно начала/конца. Не все файлы позволяют выполнение этой операции (например, файл на ленточном накопителе может не «уметь» перематываться назад). 31 Сброс буферов — содержимое файловых буферов с не записанной в файл информацией записывается. Используется обычно для указания на завершение записи логического блока (для сохранения данных в файле на случай сбоя). Получение текущего значения файлового указателя. 32 Список литературы 1. Кормен, Томас Х. и др. Алгоритмы: построение и анализ, 3-е изд.: Пер. с англ. – М.: ООО «И. Д. Вильямс», 2013. – 1328 с.: ил. 2. Далека В.Д., Деревянко А.С., Кравец О.Г., Тимановская Л.Е. Модели и структуры данных: Учебное пособие. – Харьков: ХГПУ, 2000. – 412 с. 3. https://ru.wikipedia.org 33