Значения всех элементов списка можно хранить в одном массиве, а ссылки на следующий элемент - в другом массиве. Массивов может потребоваться и больше: по количеству полей, если значение элемента списка состоит из нескольких полей. Разные поля каждого элемента располагаются в разных массивах с одним и тем же индексом. Количество элементов в этих массивах одинаково. Такие массивы на рисунках удобно изображать рядом ("параллельно"), и их называют параллельными.
В качестве ссылок используются индексы. Роль пустой ссылки может играть 0, -1 (чтобы можно было использовать нулевые элементы) или любое другое значение, не являющееся индексом. Одни и те же массивы образуют область памяти для хранения нескольких списков с одинаковым строением элементов - так называемую кучу.
Предположим, что значением элемента списка является один символ. На рис. 2.12 показан такой список, содержащий текст "СЛОН" и расположенный в параллельных массивах zn и uk (имена взяты по аналогии с рис. 2.11).
Рис. 2.12. Организация списков с помощью параллельных массивов (со списком свободной памяти)
Для организации таких списков можно, например, по аналогии с приведенными обозначениями для ссылочных переменных (пример 2.2) поместить в программу описания из примера 2.3.
Пример 2.3. Описание данных для списков с использованием параллельных массивов
#define N 1000 /* Максимальное кол-во элементов списков */
#define NOL 0 /* Пустой указатель */
typedef int ukaz; /* Тип указателя */
charzn[N+l]; /* Значения элементов (информация) */
ukazuk[N+l]; /* Указатели следующего элемента */
ukaz p; /* Указатель списка */
ukaz i,j; /* Указатели элементов списка */
char sim;
3.1. Очередь
Очередь - это упорядоченная последовательность элементов, в когорой выполняются операции включения и исключения элемента по принципу FIFO (First-In-First-Out) - "первым пришел - первым ушел", т.е. исключение всегда происходит в начале очереди, а включение - в конце (рис. 3.1).
Q[i] | Q[2] | ... | Q[n] |
Исключение Начало
элементов
Рис. 3.1. Очередь
Элементы данных помещаются в очередь, когда скорость поступления данных временно превышает скорость их обработки. Примером очереди является буфер ввода-вывода данных - область памяти для ввода-вывода записей файла параллельно с их обработкой. В операционных системах возникают многочисленные очереди программ: на загрузку в память, на выполнение к процессору, на обработку файлов и т.д. Очередь можно рассматривать как механизм запоминания подзадач, возникающих при решении некоторой задачи, с последующим решением подзадач в порядке их возникновения. Пример такого применения очереди приведен в алгоритме 4.3 обхода дерева в ширину.
Типовые операции над очередью:
1. Инициализация очереди (создание, подготовка к работе).
2. Включение элемента в очередь.
3. Исключение элемента из очереди.
4. Проверка пустоты очереди.
5. Проверка переполнения очереди.
6. Доступ к началу и концу (получение/изменение значения мерного/последнего элемента).
Представление очереди в виде вектора Очередь удобно представлять в виде циклического (кольцевого) нектара (в котором за последним элементом следует первый) с указателями начали и конца (рис. 3.2).
а) Индекс начала меньше индекса конца б) Индекс начала больше индекса конца
Рис. 3.2. Хранение очереди в виде циклического вектора (показаны два состояния)
Пример 3.1. Описание данных для представления очереди циклическим вектором:
#defmeN 100 /* Максимальная длина очереди */
Тип-элемента q[N+l]; /* Отображающий вектор очереди */
int un; /* Указатель начала (индекс первого элемента) */
int uk; /* Указатель конца (индекс первого свободного элемента в конце очереди) */
Пример 3.2. Инициализация (создание) пустой очереди: un = uk= 0;
Пример 3.3. Исключение из очереди элемента и присваивание его величине х (обозначим эту операцию х<==Очередь):
Тип-элемента х; /* Значение исключенного элемента */
if (un!= uk) // В очереди есть элементы
{ х = q[un]; // Запоминание значения
if (un < N) un++; else un=0; /* Исключение элемента
}
else... /* Очередь пустая
Пример 3.4. Включение в очередь элемента, равного х (обозначим эту операцию Очередь<== х):
Тип-элемента х; /* Включаемое значение
int i; / */ */
if (uk < N) i=uk+l; else i=0; /* Новое значение uk */
if (i!= un) /* Есть место в очереди */
{ q[uk] = х; /* Включение в очередь значения х */
uk = i;
else … /* Переполнение очереди */
Представление в виде списка удобно для элементов переменного размера, очереди труднопредсказуемого размера и для очереди с приоритетами.
В очереди с приоритетами исключение элементов происходит не в порядке поступления, а по приоритету (старшинству). Для этого элементы упорядочиваются в очереди по убыванию приоритета (т.е. включаются не обязательно в конец), что удобно делать в списке.
Рассмотрим описание данных и алгоритмы типовых операций для представления очереди списком, как на рис. 3.4.
Пример 3.1 а. Описание данных для представления очереди в виде списка:
struct el_oeh /* Тип элемента списка */
{ Тип-элемепта inf; /* Информация */
struct cl och *sled; /* Ссылка на следующий элемент */
};
struct el_och f; /* Фиктивный начальный элемент */
struct el_och *uk; /* Указатель конца очереди */
Пример 3.2 а. Инициализация (создание) пустой очереди:
f'.slеd = NULL; /* Указатель начала очереди */
uk = &f; /* Указатель конца очереди */
Пример 3.3 а. Исключение из очереди элемента и присваивание его переменной х (обозначим эту операцию х<==Очередь):
Тип-элемента х; /* Значение исключенного элемента */
struct el_och *i; /* Указатель исключенного элемента */
if (f.sled!= NULL) /* В очереди есть элементы */
{ х = f.sled->inf; /* Запоминание значения */
i = f.sled;
f.sled = f.sled->sled; /* Исключение элемента */
free (i); /* Освобождение памяти */
if (f.sled == NULL) uk = &f; /* Очередь стала пустой */
}
else... /* Очередь пуста */
Пример 3.4 а. Включение в очередь элемента, равного х (обозначим эту операцию Очередь <== х):
Тип-элемента х; /* Включаемое значение */
struct el_och *i; /* Указатель включаемого элемента */
i = malloc (sizeof(struct el_och)); /* Получение памяти */
if(i!=NULL) /* Есть место' */
{ i->inf = х; /* Запись информации */
i->sled = NULL; /* Запись ссылки */
uk.slcd = i; /* Соединить *i с концом очереди */
uk = i; /* Новый указатель конца */
} else... /* Переполнение очереди */
Стек
Стек (stack) - это упорядоченная последовательность элементов, в которой выполняются операции включения и исключения элемента по принципу LIFO (Last-In-First-Out) - "последним пришел - первым ушел", т.е. включение и исключение всегда происходят в одном конце (рис. 3.5). Этот конец называют верхом, противоположный - дном стека. Другие названия стека: магазин (но аналогии с магазином пистолета или автомата), очередь типа LIFO.
Рис. 3.5. Стек
Пример стека - стопка подносов в столовой, где подносы берутся и кладутся только сверху.
Типовые операции над стеком:
1. Инициализация (создание, подготовка к работе);
2. Вталкивание (включение) элемента - PUSH;
3. Выталкивание (исключение) элемента - POP;
4. Проверка пустоты стека;
5. Проверка переполнения стека;
6. Доступ к вершине (получение/изменение значения последнего поступившего элемента).
Представление стека в виде вектора
Чаще всего стек представляется в виде вектора с указателем (рис. 3.6). Часть вектора занимает стек, часть - свободное пространство. Указателем стека служит индекс последнего из поступивших элементов (либо индекс первой свободной позиции).
Рис. 3.6. Хранение стека в виде вектора
Указатель стека увеличивается на единицу при вталкивании элемента и уменьшается при выталкивании (или наоборот, если стек расположен в конце вектора).
Два стека удобно хранить в одном векторе, заполняя их навстречу друг Другу (рис. 3.7).
Рис. 3.7. Хранение двух стеков в одном векторе
Этот способ позволяет расти любому из двух стеков, пока есть хотя бы одна свободная ячейка. Максимальные размеры каждого стека и их суммы равны длине вектора. Для большего количества стеков подобного метода не существует.
Пример 3.5.Описание данных для представления стека вектором
tfdefme N100 /* Максимальная длина стека */
Тип-элемента st[N]; /* Отображающий вектор стека */
int ist; /* Указатель стека (индекс последнего элемента) */
Пример 3.6. Инициализация (создание) пустого стека:
ist = -1;
Пример 3.7.Выталкивание из стека элемента и присваивание его величине х (обозначим эту операцию х <== Стек; без запоминания элемента: <== Стек):
Тип-элемента х; /* Значение вытолкнутого элемента */
if(ist>=0) /* В стеке есть элементы */
{ х = st[ist]; /* Получение значения */
ist—; /* Выталкивание элемента */
}
else... /* Стек пуст */
Пример 3.8. Вталкивание в стек элемента, равного х (обозначим эту операцию Стек <== х):
Тип-элемента х; /* Вталкиваемое значение */
if(ist<N-l) /* Есть место в стеке
{ ist++; /* Увеличение стека
st[ist] = х; /* Запись х в вершину стека
}
else.../* Стек переполнен
Представление стека в виде списка
В виде списка удобно хранить стек с элементами переменной длины или труднопредсказуемым количеством элементов. Вершина стека размещается в начале списка, где очень просто выполняются операции включения и исключения элемента (рис. 3.8).
Рис. 3.8. Стек в виде списка
СМ ЛЕКЦИИ ЖЕНИ
Дек
Дек (deque - double-ended queue: двусторонняя очередь) - это упорядоченная последовательность элементов, в которой включение и исключение элемента могут выполняться в обоих концах (рис. 3.9). Дек является обобщением очереди и стека.
Рис. 3.9. Дек
Назначение дека - переупорядочивание поступающих данных перед обработкой. Стек и дек часто сравнивают с железнодорожными разъездами для перестановки вагонов поезда. Возможности дека по перестановкам больше, чем у стека.
Операции с деком аналогичны операциям над очередью и стеком. Хранится дек подобно очереди, в виде циклического вектора или списка с двумя указателями.