Лекция № 6
Тема: «Линейные структуры данных. Нелинейные структуры данных»
План лекции
1. Линейные структуры данных
1.1. Линейные списки
1.1.1. Линейный однонаправленный список
1.1.2. Линейный двунаправленный список
1.2. Стек.
1.3. Очередь.
1.4. Дек
2. Нелинейные структуры данных
2.2. Мультисписки
2.3. Слоеные списки
1. Линейные структуры данных
1.1. Линейные списки
Список – это структура данных, представляющая собой логически связанную последовательность элементов списка.
Иногда бывают ситуации, когда невозможно на этапе разработки алгоритма определить диапазон значений переменной. В этом случае применяют динамические структуры данных.
Динамическая структура данных – это структура данных, определяющие характеристики которой могут изменяться на протяжении ее существования.
Обеспечиваемая такими структурами способность к адаптации часто достигается меньшей эффективностью доступа к их элементам.
Динамические структуры данных отличаются от статических двумя основными свойствами:
1) в них нельзя обеспечить хранение в заголовке всей информации о структуре, поскольку каждый элемент должен содержать информацию, логически связывающую его с другими элементами структуры;
2) для них зачастую не удобно использовать единый массив смежных элементов памяти, поэтому необходимо предусматривать ту или иную схему динамического управления памятью.
Для обращения к динамическим данным применяют указатели.
Созданием динамических данных должна заниматься сама программа во время своего исполнения. В языке программирования Паскаль для этого существует специальная процедура:
New(Current);
После выполнения данной процедуры в оперативной памяти ЭВМ создается динамическая переменная, тип которой определяется типом указателя Current.
После использования динамического данного и при отсутствии необходимости его дальнейшего использования необходимо освободить оперативную память ЭВМ от этого данного с помощью соответствующей процедуры:
Dispose(Current);
Наиболее простой способ организовать структуру данных, состоящее из некоторого множества элементов – это организовать линейный список. При такой организации элементы некоторого типа образуют цепочку. Для связывания элементов в списке используют систему указателей, и в зависимости от их количества в элементах различают однонаправленные и двунаправленные линейные списки.
Область применения линейных списков достаточно широка:
-в операционных системах (очередь задач, готовых к выполнению, очередь документов к принтеру; стек состояний прерванных процессов (задач);
-в системах имитационного моделирования (очередь заявок на обслуживание какой-либо системой массового обслуживания);
-в научном и исследовательском ПО и т. д.
Линейные списки находят широкое применение в приложениях, где непредсказуемы требования на размер памяти, необходимой для хранения данных; большое число сложных операций над данными, особенно включений и исключений. На базе линейных списков могут строиться стеки, очереди и деки. Представление очереди с помощью линейного списка позволяет достаточно просто обеспечить любые желаемые дисциплины обслуживания очереди. Особенно это удобно, когда число элементов в очереди трудно предсказуемо.
Основными достоинствами линейных списков являются:
- лёгкость добавления и удаления элементов;
- размер ограничен только объёмом памяти компьютера и разрядностью указателей;
- динамическое добавление и удаление элементов.
1.1.1. Линейный однонаправленный список
В этом списке любой элемент имеет один указатель, который указывает на следующий элемент в списке или является пустым указателем у последнего элемента.

Основные операции, осуществляемые с линейным однонаправленным списком:
– вставка элемента;
– просмотр;
– поиск;
– удаление элемента.
Следует обратить особое внимание на то, что при выполнении любых операций с линейным однонаправленным списком необходимо обеспечивать позиционирование какого-либо указателя на первый элемент. В противном случае часть или весь список будет недоступен.
Линейный однонаправленный список имеет только один указатель в элементах. Это позволяет минимизировать расход памяти на организацию такого списка. Одновременно, это позволяет осуществлять переходы между элементами только в одном направлении, что зачастую увеличивает время, затрачиваемое на обработку списка. Например, для перехода к предыдущему элементу необходимо осуществить просмотр списка с начала до элемента, указатель которого установлен на текущий элемент.
Для ускорения подобных операций целесообразно применять переходы между элементами списка в обоих направлениях. Это реализуется с помощью линейных двунаправленных списков.
1.1.2 Линейный двунаправленный список
В этом линейном списке любой элемент имеет два указателя, один из которых указывает на следующий элемент в списке или является пустым указателем у последнего элемента, а второй – на предыдущий элемент в списке или является пустым указателем у первого элемента.

Основные операции, осуществляемые с линейным двунаправленным списком те же, что и с однонаправленным линейным списком:
– вставка элемента;
– просмотр;
– поиск;
– удаление элемента.
Следует обратить внимание на то, что в отличие от однонаправленного списка здесь нет необходимости обеспечивать позиционирование какого-либо указателя именно на первый элемент списка, так как благодаря двум указателям в элементах можно получить доступ к любому элементу списка из любого другого элемента, осуществляя переходы в прямом или обратном направлении. Однако часто бывает полезно иметь указатель на заголовок списка.
1.2. Стек
Стек – это структура данных, в которой новый элемент всегда записывается в ее начало (вершину) и очередной читаемый элемент также всегда выбирается из ее начала. Здесь используется принцип «последним пришел – первым вышел» (LIFO: Last Input – First Output).
Стек можно реализовывать как статическую структуру данных в виде одномерного массива, а можно как динамическую структуру – в виде линейного списка.
При реализации стека в виде статического массива необходимо резервировать массив, длина которого равна максимально возможной глубине стека, что приводит к неэффективному использованию памяти. Однако работать с такой реализацией проще и быстрее. При такой реализации дно стека будет располагаться в первом элементе массива, а рост стека будет осуществляться в сторону увеличения индексов. Одновременно необходимо отдельно хранить значение индекса элемента массива, являющегося вершиной стека. Можно обойтись без отдельного хранения индекса, если в качестве вершины стека всегда использовать первый элемент массива, но в этом случае, при записи или чтении из стека, необходимо будет осуществлять сдвиг всех остальных элементов, что приводит к дополнительным затратам вычислительных ресурсов.

Стек как динамическую структуру данных легко организовать на основе линейного списка. Поскольку работа всегда идет с заголовком стека, т. е. не требуется осуществлять просмотр элементов, удаление и вставку элементов в середину или конец списка, то достаточно использовать экономичный по памяти линейный однонаправленный список.
Для такого списка достаточно хранить указатель вершины стека, который указывает на первый элемент списка. Если стек пуст, то списка не существует и указатель принимает значение nil.
Поскольку стек, по своей сути, является структурой с изменяемым количеством элементов, то основное внимание уделим динамической реализации стека. Как уже говорилось выше, для такой реализации целесообразно использовать линейный однонаправленный список.
Описание элементов стека аналогично описанию элементов линейного однонаправленного списка, где DataType является типом элементов стека. Поэтому здесь приводить его не будем.
Основные операции, производимые со стеком:
– записать (положить в стек);
– прочитать (снять со стека);
– очистить стек;
– проверка пустоты стека.
Примеры использования стека. Стопка тарелок, лоток принтера с бумагой, игра “Ханойские башни” — простые примеры стека.

Более изощрённые примеры применения стека — любая рекурсивная задача (“так, старую итерацию пока отложу в стопку, а сейчас надо обрабатывать новую итерацию!“), например, перебор маршрутов исследовательского робота в пещере неизвестной конфигурации.
1.3. Очередь
Очередь – это структура данных, представляющая собой последовательность элементов, образованная в порядке их поступления. Каждый новый элемент размещается в конце очереди; элемент, стоящий в начале очереди, выбирается из нее первым. Здесь используется принцип «первым пришел – первым вышел» (FIFO: First Input – First Output).
Очередь можно реализовывать как статическую структуру данных в виде одномерного массива, а можно как динамическую структуру – в виде линейного списка (рис. 4).
При реализации очереди в виде статического массива необходимо резервировать массив, длина которого равна максимально возможной длине очереди, что приводит к неэффективному использованию памяти.
При такой реализации начало очереди будет располагаться в первом элементе массива, а рост очереди будет осуществляться в сторону увеличения индексов. Однако, поскольку добавление элементов происходит в один конец, а выборка – из другого конца очереди, то с течением времени будет происходить миграция элементов очереди из начала массива в сторону его конца. Это может привести к быстрому исчерпанию массива и невозможности добавлении новых элементов в очередь при наличии свободных мест в начале массива. Предотвратить это можно двумя способами:
– после извлечения очередного элемента из начала очереди осуществлять сдвиг всей очереди на один элемент к началу массива. При этом необходимо отдельно хранить значение индекса элемента массива, являющегося концом очереди (начало очереди всегда в первом элементе массива);
– представить массив в виде циклической структуры, где первый элемент массива следует за последним. Элементы очереди располагаются в «круге» элементов массива в последовательных позициях, конец очереди находится по часовой стрелке на некотором расстоянии от начала. При этом необходимо отдельно хранить значение индекса элемента массива, являющегося началом очереди, и значение индекса элемента массива, являющегося концом очереди. Когда происходит добавление в конец или извлечение из начала очереди, осуществляется смещение значений этих двух индексов по часовой стрелке.
С точки зрения экономии вычислительных ресурсов более предпочтителен второй способ. Однако здесь усложняется проверка на пустоту очереди и контроль переполнения очереди – индекс конца очереди не должен «набегать» на индекс начала.
Очередь как динамическую структуру данных легко организовать на основе линейного списка. Поскольку работа идет с обоими концами очереди, то предпочтительно будет использовать линейный двунаправленный список. Хотя, как уже говорилось при описании этого списка, для работы с ним достаточно иметь один указатель на любой элемент списка, здесь целесообразно хранить два указателя – один на начало списка (откуда извлекаем элементы) и один на конец списка (куда добавляем элементы). Если очередь пуста, то списка не существует и указатели принимают значение nil.

Примеры использования очереди. Очереди часто используются в программах для реализации буфера, в который можно положить элемент для последующей обработки, сохраняя порядок поступления. Очереди используются для моделирования реальных ситуаций ожидания клиентов в банке, вылета самолетов или передачи данных по Интернету. В очереди печати — документы ждут освобождения принтера. Данные вводимые с клавиатуры, также хранятся в очереди.
1.4. Дек
Дек – это структура данных, представляющая собой последовательность элементов, в которой можно добавлять и удалять в произвольном порядке элементы с двух сторон. Первый и последний элементы дека соответствуют входу и выходу дека.
Выделяют ограниченные деки:
– дек с ограниченным входом – из конца дека можно только извлекать элементы;
– дек с ограниченным выходом – в конец дека можно только добавлять элементы.
Данная структура является наиболее универсальной из рассмотренных выше линейных структур. Накладывая дополнительные ограничения на операции с началом и/или концом дека, можно осуществлять моделирование стека и очереди.
Дек также можно реализовывать как статическую структуру данных в виде одномерного массива, а можно как динамическую структуру – в виде линейного списка.
Поскольку в деке, как и в очереди, осуществляется работа с обоими концами структуры, то целесообразно использовать те же подходы к организации дека, что применялись и для очереди.

2. Нелинейные структуры данных
2.1. Мультисписки

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

Поиск в мультисписке происходит аналогично поиску в линейном списке, но при этом используется только один указатель, соответствующий списку, в котором осуществляется поиск.
Добавление элемента здесь сложнее. Добавление элемента, принадлежащего только одному из списков, осуществляется аналогично добавлению в линейный список, за исключением того, что поля указателей, относящиеся к другим спискам, устанавливаются в nil. При добавлении элемента, принадлежащего сразу нескольким спискам, необходимо аккуратно осуществлять определение значений соответствующих указателей. Алгоритм выполнения такой операции сильно зависит от количества списков и места вставки нового элемента.
Применение мультисписков. В программных системах, обрабатывающих объекты сложной структуры, могут решаться разные подзадачи, каждая из которых требует, возможно, обработки не всего множества объектов, а лишь какого-то его подмножества. Так, например, в автоматизированной системе учета лиц, пострадавших вследствие аварии на ЧАЭС, каждая запись об одном пострадавшем содержит более 50 полей в своей информационной части. Решаемые же автоматизированной системой задачи могут потребовать выборки, например:
- участников ликвидации аварии;
- переселенцев из зараженной зоны;
- лиц, состоящих на квартирном учете;
- лиц с заболеваниями щитовидной железы;
- и т.д., и т.п.
Для того, чтобы при выборке каждого подмножества не выполнять полный просмотр с отсеиванием записей, к требуемому подмножеству не относящихся, в каждую запись включаются дополнительные поля ссылок, каждое из которых связывает в линейный список элементы соответствующего подмножества. В результате получается многосвязный список или мультисписок, каждый элемент которого может входить одновременно в несколько односвязных списков.
К достоинствам мультисписков помимо экономии памяти (при множестве списков информационная часть существует в единственном экземпляре) следует отнести также целостность данных - в том смысле, что все подзадачи работают с одной и той же версией информационной части и изменения в данных, сделанные одной подзадачей немедленно становятся доступными для другой подзадачи.
Каждая подзадача работает со своим подмножеством как с линейным списком, используя для этого определенное поле связок. Специфика мультисписка проявляется только в операции исключения элемента из списка. Исключение элемента из какого-либо одного списка еще не означает необходимости удаления элемента из памяти, так как элемент может оставаться в составе других списков. Память должна освобождаться только в том случае, когда элемент уже не входит ни в один из частных списков мультисписка. Обычно задача удаления упрощается тем, что один из частных списков является главным - в него обязательно входят все имеющиеся элементы. Тогда исключение элемента из любого неглавного списка состоит только в переопределении указателей, но не в освобождении памяти. Исключение же из главного списка требует не только освобождения памяти, но и переопределения указателей как в главном списке, так и во всех неглавных списках, в которые удаляемый элемент входил.
2.2. Слоеные списки
Слоеные (skip), или разделенные, списки – это связные списки, которые позволяют перескакивать через некоторое количество элементов (рис. 2). Это позволяет преодолеть ограничения последовательного поиска, являющейся основной причиной низкой эффективности поиска в линейных списках. В то же время вставка и удаление остаются сравнительно эффективными.
Идея, лежащая в основе слоеных списков, очень напоминает метод, используемый при поиске имен в адресной книжке. Чтобы найти имя, ищут страницу, помеченную буквой, с которой начинается имя, а затем поиск осуществляют в пределах этой страницы.
В отличие от элементов обычных линейных списков, элементы этих списков имеют дополнительный указатель. Все элементы списка группируются по определенному признаку, и первый элемент каждой группы содержит указатель на первый элемент следующей группы. Если следующая группа отсутствует или элемент не является первым в группе, то этот дополнительный указатель принимает значение nil.
Эта простая идея может быть расширена – можно добавлять нужное число дополнительных указателей, группируя группы элементов и т. д. на нужном количестве уровней.
Если реализован только один уровень, то это, фактически, обычный список и время поиска пропорционально O(n). Однако если имеется достаточное число уровней, то разделенный список можно считать деревом с корнем на высшем уровне, а для дерева время поиска, как будет показано ниже, пропорционально O(log n).

Контрольные
вопросы.
1.
Понятие списка
2.
Виды списков
3.
Реализация линейного
однонаправленного списка
4.
Реализация линейного
двунаправленного списка
5.
Стек и его организация
6.
Очередь и его организация
7.
Дек и его организация
8.
Что такое мультисписок?
9.
Где применяются
мультисписки? Реализация мультисписков.
10.
Что такое слоеные списки?
11.
Где применяются
слоеные списки? Реализация слоеных списков.