Скачать презентацию Тема Связанные списки Лекция 11 13 г 1 Скачать презентацию Тема Связанные списки Лекция 11 13 г 1

Лекция-16.ppt

  • Количество слайдов: 23

Тема Связанные списки Лекция 11. 13 г. 1 Тема Связанные списки Лекция 11. 13 г. 1

Общие сведения Связанный список (linked list) — это структура данных, элементы которой расположены в Общие сведения Связанный список (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 Однонаправленные списки (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 При необходимости включения в однонаправленный список нового элемента Включение элемента в список 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 При извлечения элемента из однонаправленного списка также Извлечение (удаление) элемента из списка 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 #include "List 1. h" int main() { 1 List 1 *l = init_L 1(); Print_L 1(l); Insert_L 1(l, 14. 0); 7 Insert_L 1(l->next, 10. 0); 8 2 Insert_L 1(l, 16. 0); Print_L 1(l); 3 Insert_L 1(l->next, 20. 0); Print_L 1(l); Insert_L 1(l->next->next, 50. 0); Print_L 1(l); 4 double x; 5 Extract_L 1(l->next, &x); Print_L 1(l); 6 Extract_L 1(l->next->next, &x); Print_L 1(l); 7 Extract_L 1(l, &x); Print_L 1(l); Extract_L 1(l->next, &x); Print_L 1(l); 8 system("PAUSE"); return 0; } Лекция 11. 13 г. 2 3 4 5 6 6

Задача обращения однонаправленного списка Построим алгоритм решения одной простой задачи для однонаправленного списка: перестроить Задача обращения однонаправленного списка Построим алгоритм решения одной простой задачи для однонаправленного списка: перестроить список так , чтобы хвост стал головой и наоборот – голова стала хвостом (т. е. выполнить реверсирование списка). На рисунке показан один цикл обращения. Слева от пунктира – перестроенный список, справа – исходный. 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 <stdlib. h> #include Тестирование функции reverse_L 1 #include #include "List 1. h" int main() { List 1 *l = init_L 1(); Print_L 1(l); Insert_L 1(l, 14. 0); Insert_L 1(l->next, 10. 0); Insert_L 1(l, 16. 0); Print_L 1(l); Insert_L 1(l->next, 20. 0); Print_L 1(l); Insert_L 1(l->next->next, 50. 0); Print_L 1(l); reverse_L 1(l); Print_L 1(l); system("PAUSE"); return 0; } Лекция 11. 13 г. 8

Задача сортировки однонаправленного списка Рассмотрим еще одну задачу – сортировка списка вставками. q исходный Задача сортировки однонаправленного списка Рассмотрим еще одну задачу – сортировка списка вставками. 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 == Исходный код функции 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 <stdlib. h> #include Тестирование функции sort_L 1 #include #include "List 1. h" int main() { List 1 *l = init_L 1(); Print_L 1(l); Insert_L 1(l, 14. 0); Insert_L 1(l->next, 10. 0); Insert_L 1(l, 16. 0); Print_L 1(l); Insert_L 1(l->next, 20. 0); Print_L 1(l); Insert_L 1(l->next->next, 50. 0); Print_L 1(l); reverse_L 1(l); Print_L 1(l); sort_L 1(l); Print_L 1(l); reverse_L 1(l); Print_L 1(l); system("PAUSE"); return 0; } Лекция 11. 13 г. 13

Тестирование на случайных числах #include <stdlib. h> #include <math. h> #include Тестирование на случайных числах #include #include #include "List 1. h" int main() { List 1 *l = init_L 1(); Print_L 1(l); for(int i = 0; i<100; i++) Insert_L 1(l, (double)(rand() % 1000)); Print_L 1(l); sort_L 1(l); Print_L 1(l); system("PAUSE"); return 0; } Лекция 11. 13 г. 14

Обход списка Вернемся к задаче вывода на печать однонаправленного связанного списка. Эта задача была Обход списка Вернемся к задаче вывода на печать однонаправленного связанного списка. Эта задача была решена путем прохождения в цикле всех узлов списка от «головы» к «хвосту» и распечатки (вывода на экран) содержимого каждого узла. Заметим, что эта задача является частным случаем задачи обхода списка – посещения всех его узлов и выполнения некоторых действий в каждом узле и может быть решена не только с помощью циклического алгоритма, но и с помощью рекурсии. Во втором случае появляются две основные возможности: üобработать узел, а затем следовать связи (в этом случае узлы посещаются по порядку), или üследовать связи, а затем обработать узел (в этом случае узлы посещаются в обратном порядке). Лекция 11. 13 г. 15

Обход списка Модернизируем функцию печати списка, введя две вспомогательных функции: visit, которая выполняет «полезную Обход списка Модернизируем функцию печати списка, введя две вспомогательных функции: 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) Обход списка Продемонстрируем работу функции 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) Каждый узел двунаправленного списка содержит два указателя, Двунаправленные (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 <stdlib. h> #include Тестирование кольцевого списка #include #include "List 2. h" int main() { List 2 *l = init_L 2(); Print_L 2(l, 1); 1 Insert_L 2(l, 14. 0, 1); Insert_L 2(l, 10. 0, -1); Insert_L 2(l, 16. 0, 1); Print_L 2(l, 1); 2 Insert_L 2(l->next, 20. 0, 1); Print_L 2(l, 1); 3 Insert_L 2(l->prev, 40. 0, -1); Print_L 2(l, 1); 4 Insert_L 2(l->next, 50. 0, 1); Print_L 2(l, 1); 5 Print_L 2(l, -1); 6 system("PAUSE"); return 0; } Лекция 11. 13 г. 1 2 3 4 5 6 23