Двухсвязные линейные списки
Как мы уже говорили, линейные списки могут быть односвязными и двухсвязными.
Каждый элемент односвязного списка, кроме собственно данных, содержит поле с адресом следующего элемента. Работу с такими списками мы только что рассмотрели.
В двухсвязном списке каждый элемент имеет поля с данными и два указателя: один указатель хранит адрес предшествующего элемента списка, второй — адрес последующего элемента. Вполне естественно для работы с двухсвязным списком использовать два указателя, хранящие адреса начала и конца такого списка. На рисунке ниже даётся графическое представление двухсвязного списка.
Рассмотрим особенности работы с двухсвязным списком.
1. Как и в случае с односвязным списком, создадим две структуры. Описание этих структур расположим в начале программы (до main() и других функций). Вот возможная реализация структур:
struct Data
{
int a; // данные
};
struct DList
{
Data d;
DList *prev; // указатель на предшествующий элемент
DList *next; // указатель на последующий элемент
};
2. Затем создаём два указателя:
DList *Begin = NULL; // Начало списка
DList *End = NULL; // Конец списка
3. Теперь можно формировать какой-то начальный список. Делаем это по аналогии с тем, как делали при работе с односвязными списками. Начнём формировать список из трёх чисел 3, 5 и 1:
// Создаём первый элемент
DList *t = new DList;
t->d.a = 3;
t->prev = NULL;
t->next = NULL;
// Настроим на него оба указателя
Begin = t;
End = t;
//++++++++++++++++++++++++следующая пара+++++++++++++++
// Создаём второй элемент
t->next = new DList;
DList *p = t;
t = t->next;
t->prev = p;
t->d.a = 5;
t->next = NULL;
End = t;
// Создаём третий элемент
t->next = new DList;
p = t;
t = t->next;
t->prev = p;
t->d.a = 1;
t->next = NULL;
End = t;
Всё, список из трёх чисел создан. Приведём хотя бы некоторые действия с этим списком.
1.Обход списка в прямом направлении и его вывод на экран монитора:
void print_forward(DList *Begin)
{
DList *p = Begin;
cout << "Spisok (forward):" << endl;
while(p)
{
cout << p->d.a << endl;
p = p->next;
}
}
Возможный вызов функции:
print_forward(Begin);
Как видим, полученная реализация по сути является копией функции print() для печати односвязного списка.
2.Обход списка в обратном направлении и его вывод на экран монитора:
void print_back(DList *End)
{
DList *p = End;
cout << "Spisok (back):" << endl;
while(p)
{
cout << p->d.a << endl;
p = p->prev;
}
}
Возможный вызов функции:
print_back(End);
И здесь сходство большое. Важно обратить внимание на оператор, за счет которого выполняется движение назад:
p = p->prev;
На других операциях с двухсвязными списками останавливаться не будем, так как их можно разработать по аналогии с операциями для односвязных списков.
Кольцевой список
Кольцевой список — это список, у которого последний элемент связан с первым. Кольцевой список можно сделать как односвязным, так и двухсвязным. Рассмотрим вкратце односвязный кольцевой список.
Схема кольцевого списка представлена на рисунке ниже (используем те же данные, что и для ранее рассмотренного односвязного списка, т.е. список из чисел 3, 5, 1, 9):
Используем те же структуры (Data и List), что применяли для односвязного списка. Точно так же определяем указатель на начало будущего списка:
List *u = NULL;
и так же делаем начальное заполнение списка. Но в конце, после того, как для нашего примера мы занесли число 9 в список, требуется «замкнуть» список на начало:
x->next = u;
В итоге будет получен кольцевой список.
Операции для кольцевого списка можно выполнять те же, что и для обычного списка, но здесь будет присутствовать одна особенность: список замкнут, поэтому проверка того факта, что достигнут конец цикла, выполняется по другому. Покажем это на примере распечатки кольцевого cписка:
void printC(List *u)
{
if(u != NULL)
{
List *p = u;
cout << "Kolcevoj Spisok:" << endl;
cout << p->d.a << endl;
p = p->next;
while(p != u)
{
cout << p->d.a << endl;
p = p->next;
}
}
else
cout << "Spisok pust." << endl;
}
Возможный вызов функции:
printC(u);
Как видно из примера, после печати первого элемента, мы печатаем не до конца списка, а до нахождения начала.
Другие операции для кольцевого списка разработайте самостоятельно.
Стеки
Определение стека
Стек — динамическая структура данных, для которой выполняется правило: последним пришёл — первым ушёл. Таким образом, и дополнение новых данных, и извлечение их из стека всегда выполняется с одной стороны.
Вершина стека — эта та его часть, через которую ведётся вся работа. На вершину стека добавляются новые элементы, и с вершины стека снимаются (удаляются) элементы. В общем, стек — это односвязный список, для которого определены только две операции: добавление и удаление из начала списка. Примером стека может служить коробка, в которую сверху укладывают книги. Извлекать книги также приходится сверху. |
Операции со стеком
Рассмотрим основные и дополнительные действия со стеком. Для работы используем структуры Data и Stack:
struct Data
{
int a;
};
struct Stack
{
Data d;
Stack *next;
};
В программе определяем указатель на начало будущего стека:
Stack *u = NULL;
После этого можно начать работать со стеком.
1. Добавление в стек. Функцию добавления назовём Push() — по аналогии с командой ассемблера push, которая заносит целое число в программный стек.
void Push(Stack **u, Data &x)
{
Stack *t = new Stack; // Память под новый элемент
t->d.a = x.a; // Заполнение полей
t->next = *u; // Подключаем новый элемент к имеющимся
*u = t; // Перенастройка вершины
}
Обратиться к функции можно так:
Push(&u,x);
где x — объект типа Data.
2. Извлечение из стека. Здесь снова аналогия с ассемблером (команда pop выталкивает целое число из стека).
bool Pop(Stack **u, Data &x)
{
if(*u == NULL)
{
cout << "Pustoj Stack" << endl;
return false;
}
Stack *t = *u;
x.a = t->d.a; // Получаем значения полей на вершине стека
*u = t->next; // Перенастройка вершины
delete t; // Удаление ненужного элемента
return true;
}
Пример вызова функции:
Data y;
if(Pop(&u, x))
{
y = x;
cout << "y=" << y.a << endl;
}
Дополнительные действия со стеком — распечатка стека (можно взять алгоритм для работы с односвязным списком) и чтение с вершины стека. Чтение напоминает извлечение, но при этом данные из стека не удаляются. Вот возможный вариант реализации:
bool Read(Stack **u, Data &x)
{
if(*u == NULL)
{
cout << "Pustoj Stack" << endl;
return false;
}
Stack *t = *u;
x.a = t->d.a; // Заполнение полей
return true;
}
Использование функции Read() может быть аналогичным использованию Pop().
Очереди
Определение очереди
Очередь — это линейная динамическая структура данных, для которой выполняется правило: добавление новых данных возможно только в конец этой структуры, а удаление (извлечение) — только с начала. В англоязычной литературе этот принцип называется FIFO (First Input — First Output, т.е. первый пришёл — первый ушёл).
Примером из реальной жизни может быть очередь из покупателей к кассе в магазине.
Как не трудно понять, очередь — это линейный список, для которого определены всего две основные операции: добавление в конец и извлечение с начала. Значит, удобно иметь два указателя: на начало и конец этой динамической структуры. Но списки бывают односвязные и двухсвязные. Какой использовать? Подойдёт только двухсвязный список. В этом можно будет убедиться при рассмотрении основных алгоритмов для работы с очередью.
На рисунке ниже показано графическое представление очереди. Как и в предыдущих темах, очередь будем строить из целых чисел, например: 3, 5, 1.
Для работы используем структуры Data (данные) и Queue (очередь):
struct Data
{
int a;
};
struct Queue
{
Data d;
Queue *prev; // указатель на предшествующий элемент
Queue *next; // указатель на последующий элемент
};
Затем в программе определяем два указателя:
Queue *Begin = NULL; // Начало очереди
Queue *End = NULL; // Конец очереди
После этого можно начать работать с очередью.