Тема Связанные списки Лекция 11. 13 г. 1
Общие сведения Связанный список (linked list) — это структура данных, элементы которой расположены в линейном порядке. Однако, в отличие от массива, в котором этот порядок определяется индексами, порядок в связанном списке поддерживается с помощью указателей. Связанные списки обеспечивают простое и гибкое представление динамических множеств и поддерживают все операции, рассмотренные ранее для массивов. Списки могут быть разных видов. Простейшим из списков является однократно связанный (однонаправленный - singly linked) список. Будем обозначать вид таких списков L 1. узлы списка … next elem NULL хвост голова (ограничитель) typedef struct List 1 { double elem; struct List 1 *next; } List 1; Лекция 11. 13 г. 2
Однонаправленные списки (L 1) Ниже приведена иллюстрация пустого однонаправленного списка и текст функции init_L 1 , инициализирующей такой список. q … NULL List 1* init_L 1() { List 1 *q = (List 1*)calloc(1, sizeof(List 1)); q->next = NULL; return q; } В дальнейшем полезной будет функция печати элементов списка Print_L 1. void Print_L 1(List 1 *q) { List 1 *p = q->next; if (p == NULL) printf("List is empty"); while (p) { printf("%5. 1 f ", p->elem); p = p->next; } printf("n--------n"); } Лекция 11. 13 г. 3
Включение элемента в список L 1 При необходимости включения в однонаправленный список нового элемента обычно задают (с помощью указателя) элемент списка, после которого должен разместиться новый элемент: q – заданный узел … next elem next голова elem NULL хвост elem next новый Ниже представлен текст функции, реализующей включение нового элемента после заданного элемента Insert_L 1. void Insert_L 1(List 1 *q, double z) { List 1* p = (List 1*)calloc(1, sizeof(List 1)); p->elem = z; p->next = q->next; q->next = p; } Лекция 11. 13 г. 4
Извлечение (удаление) элемента из списка L 1 При извлечения элемента из однонаправленного списка также обычно задают элемент списка, после которого должен быть удален элемент: голова … next q – заданный узел elem next хвост elem NULL освобождение памяти int Extract_L 1(List 1 *q, double *z) { List 1 *p = q->next; if (p == NULL) return 0; *z = p->elem; q->next = p->next; free(p); } Лекция 11. 13 г. 5
Тестирование однонаправленного списка Как и для всех ранее рассмотренных структур данных, сформируем файл интерфейса List 1. h и файл реализации List 1. c для списков вида L 1 (тексты не 1 приводятся), а также проведём тестирование: #include
Задача обращения однонаправленного списка Построим алгоритм решения одной простой задачи для однонаправленного списка: перестроить список так , чтобы хвост стал головой и наоборот – голова стала хвостом (т. е. выполнить реверсирование списка). На рисунке показан один цикл обращения. Слева от пунктира – перестроенный список, справа – исходный. elem next r elem next y elem next t elem next r elem next y void reverse_L 1(List 1 *l) { List 1 *r = l->next, *y, *t; if (r == NULL) return; y = r->next; r->next = NULL; while (y) { t = y->next; y->next = r; r = y; y = t; } l->next = r; } Лекция 11. 13 г. 7
Тестирование функции reverse_L 1 #include
Задача сортировки однонаправленного списка Рассмотрим еще одну задачу – сортировка списка вставками. q исходный список 50 … 10 20 14 16 N С помощью фиктивных узлов a и b создадим два списка – уже отсортированный (список b) и еще не отсортированный (список a): a b … … 10 50 N 20 14 16 N неотсортированный список Извлекаем из списка a первый элемент и помещаем его на «подходящее» место в список b: a … 20 14 16 N неотсортированный список b … 10 50 N отсортированный список Этот процесс продолжаем до полного исчерпания списка a. В результате список b будет содержать отсортированную последовательность. Отметим, что «физического» перемещения узлов в памяти не происходит. Изменяются только указатели. Лекция 11. 13 г. 9
Задача сортировки однонаправленного списка Перейдем к программной реализации задачи сортировки. Будем считать, что список задан указателем: List 1 *q; Сначала убедимся, что в этом списке есть более одного элемента: if ((q->next == NULL) || (q->next == NULL)) return; Замечания: 1. Условие q->next == NULL означает, что исходный список пуст 2. Условие q->next == NULL означает, что список состоит из одного элемента Сформируем начальное состояние списков a и b: a b … … 10 50 N 20 14 16 N неотсортированный список List 1* a = (List 1*)calloc(1, sizeof(List 1)); List 1* b = (List 1*)calloc(1, sizeof(List 1)); b->next = q->next; a->next = b->next; b->next = NULL; Лекция 11. 13 г. 10
Задача сортировки однонаправленного списка Перейдем к реализации первой итерации алгоритма. Введем два рабочих указателя t и x для перебора элементов списков a и b соответственно: List 1 *t, *x; Установим t на начало списка a. А так как первый элемент списка a «физически» извлекается из него, скорректируем поле next фиктивного узла a: a 10 t 20 14 16 N неотсортированный список t = a->next; a->next = t->next; Найдем место элементу t в отсортированном списке. В начале цикла будет: b отсортированный список 50 N x for (x = b; ; x = x->next) // цикл по отсортированному списку if ((x->next == NULL) || (x->next->elem > t->elem)) { t->next = x->next; x->next = t; break; } После завершения цикла будет: a b 10 20 14 50 N Лекция 11. 13 г. 16 N неотсортированный список 11
Исходный код функции sort_L 1 void sort_L 1(List 1 *q) { if ((q->next == NULL) || (q->next == NULL)) return; List 1* a = (List 1*)calloc(1, sizeof(List 1)); List 1* b = (List 1*)calloc(1, sizeof(List 1)); b->next = q->next; a->next = b->next; b->next = NULL; List 1* t; while (t = a->next) { a->next = t->next; for (List 1* x = b; ; x = x->next) if ((x->next == NULL) || (x->next->elem > t->elem)) { t->next = x->next; x->next = t; break; } } q->next = b->next; } Лекция 11. 13 г. 12
Тестирование функции sort_L 1 #include
Тестирование на случайных числах #include
Обход списка Вернемся к задаче вывода на печать однонаправленного связанного списка. Эта задача была решена путем прохождения в цикле всех узлов списка от «головы» к «хвосту» и распечатки (вывода на экран) содержимого каждого узла. Заметим, что эта задача является частным случаем задачи обхода списка – посещения всех его узлов и выполнения некоторых действий в каждом узле и может быть решена не только с помощью циклического алгоритма, но и с помощью рекурсии. Во втором случае появляются две основные возможности: üобработать узел, а затем следовать связи (в этом случае узлы посещаются по порядку), или üследовать связи, а затем обработать узел (в этом случае узлы посещаются в обратном порядке). Лекция 11. 13 г. 15
Обход списка Модернизируем функцию печати списка, введя две вспомогательных функции: visit, которая выполняет «полезную работу» в узле (т. е. просто печатает его содержимое) и рекурсивную функцию traverse, которая и выполняет обход списка: void visit(List 1* l) { printf("%5. 1 f ", l->elem); } void traverse(List 1* l) { if (l == NULL) return; // visit(l); // печать списка в прямом направлении traverse(l->next); // рекурсивный вызов // visit(l); // печать списка в обратном направлении } void Print_L 1(List 1 *l) { if (l->next == NULL) printf("List is empty"); traverse(l->next); printf("n--------n"); } Лекция 11. 13 г. 16
Обход списка Продемонстрируем работу функции traverse. Сначала раскомментируем первый вызов: void traverse(List 1* l) { if (l == NULL) return; visit(l); // печать списка в прямом направлении traverse(l->next); // рекурсивный вызов // visit(l); // печать списка в обратном направлении } Теперь раскомментируем второй вызов: void traverse(List 1* l) { if (l == NULL) return; // visit(l); // печать списка в прямом направлении traverse(l->next); // рекурсивный вызов visit(l); // печать списка в обратном направлении } Лекция 11. 13 г. 17
Двунаправленные (L 2) списки (doubly linked list) Каждый узел двунаправленного списка содержит два указателя, один из которых – next указывает на «соседа» справа, а второй – prev – на «соседа» слева: узлы списка NULL … next prev elem NULL хвост голова Определение структуры для двунаправленного списка и алгоритм его инициализации выглядят так: typedef struct List 2 { double elem; struct List 2 *prev, *next; } List 2; List 2* init_L 2() { List 2 *l = (List 2*)calloc(1, sizeof(List 2)); l->prev = l->next = NULL; } Лекция 11. 13 г. 18
Циклические (кольцевые) списки Двунаправленные списки на практике часто используются в форме кольцевых списков, вид которых иллюстрируется следующим рисунком: узлы списка prev … next prev elem next голова Пустой кольцевой список: хвост prev … next Определение структуры для кольцевого списка и алгоритм его инициализации выглядят так: typedef struct List 2 { double elem; struct List 2 *prev, *next; } List 2; List 2* init_L 2() { List 2 *l = (List 2*)calloc(1, sizeof(List 2)); l->prev = l->next = l; } Лекция 11. 13 г. 19
Печать кольцевого списка Наличие двух указателей в узлах кольцевого списка позволяет обрабатывать этот список как в прямом, так и в обратном направлении, без решения специальной задачи реверсирования списка. Рассмотрим это положение на примере функции печати кольцевого списка Print_L 2, которая «умеет» распечатывать такой список как в прямом, так и в обратном направлении. Для выбора направления эта функция имеет дополнительный параметр direction: если он <0, то печать в обратном направлении, иначе – в прямом. void Print_L 2(List 2 *l, int direction) { List 2 *p = (direction < 0) ? l->prev : l->next; if (p == l) printf("List is empty"); while (p != l) { printf("%5. 1 f ", p->elem); p = (direction < 0) ? p->prev : p->next; } printf("n--------n"); } Лекция 11. 13 г. 20
Включение элемента в кольцевой список Использование фиктивного узла – ограничителя и наличие двух указателей в каждом узле позволяет существенно упростить задачу включения нового узла в кольцевой список и даже немного расширить возможности включения. Ниже представлен текст функции Insert_L 2, которая выполняет включение нового узла как слева, так и справа от заданного узла, причем в качестве заданного узла может выступать и ограничитель. Необходимость в специальных функциях «включить в начало» и «включить в хвост» отпадает, т. к. их заменяют вызовы универсальной функции Insert_L 2 с параметрами «включить справа от ограничителя» и «включить слева от ограничителя» . void Insert_L 2(List 2 *l, double z, int direction) { List 2* p = (List 2*)calloc(1, sizeof(List 2)); p->elem = z; if (direction < 0) { // включение слева p->next = l; p->prev = l->prev; l->prev = p; p->prev->next = p; } else { // p->next = l->next; p->prev = l; l->next = p; p->next->prev = p; } } Лекция 11. 13 г. 21
Включение элемента в кольцевой список На рисунке ниже приведена схема, объясняющая алгоритм включения элемента в кольцевой список справа от заданного узла: p – новый узел prev elem next 3 prev … 2 prev elem next 1 4 prev elem next l – заданный узел 1 2 3 4 p->next = l->next; p->prev = l; l->next = p; p->next->prev = p; Лекция 11. 13 г. 22
Тестирование кольцевого списка #include