Лекція 26

реклама
Лекція 26. Послідовності. Однозв'язні списки.
Загальне поняття послідовності. Розвинуті структури даних. Однозв'язні списки та
варіанти їх реалізації.
Если главный интерес представляет последовательный перебор набора элементов, их
можно организовать в виде связного списка — базовой структуры данных, в которой
каждый элемент содержит информацию, необходимую для получения следующего
элемента. Основное преимущество связных списков перед массивами заключается в
возможности эффективного изменения расположения элементов. За эту гибкость
приходится жертвовать скоростью доступа к произвольному элементу списка, поскольку
единственный способ получения элемента состоит в отслеживании связей от начала
списка.
Определение. Связный список — это набор элементов, причем каждый из них является
частью узла (node), который также содержит ссылку (link) на узел.
Узлы определяются ссылками на узлы, поэтому связные списки иногда называют
самоссылочыми (self-referent) структурами. Более того, хотя узел обычно ссылается на
другой узел, возможна ссылка на самого себя, поэтому связные списки могут
представлять собой циклические (cyclic) структуры. Последствия этих двух фактов станут
ясны при рассмотрении конкретных представлений и применений связных списков.
Обычно под связным списком подразумевается реализация последовательного
расположения набора элементов. Начиная с некоторого узла, мы считаем его первым
элементом последовательности. Затем прослеживается его ссылка на другой узел,
который дает нам второй элемент последовательности и т.д. Поскольку список может
быть циклическим, последовательность иногда представляется бесконечной. Чаще всего
приходится иметь дело со списками, соответствующими простому последовательному
расположению элементов, принимая одно из следующих соглашений для ссылки
последнего узла:
 Это пустая (null) ссылка, не указывающая на какой-либо узел.
 Ссылка указывает на фиктивный узел (dummy node), который не содержит
элементов.
 Ссылка указывает на первый узел, что делает список циклическим.
В каждом случае отслеживание ссылок от первого узла до последнего формирует
последовательное расположение элементов. Массивы также задают последовательное
расположение элементов, но оно реализуется косвенно, за счет позиции в массиве.
(Массивы также поддерживают произвольный доступ по индексу, что невозможно для
списков.)
Сначала рассмотрим узлы с единственной ссылкой. В большинстве приложений
используются одномерные списки, где все узлы, за исключением, возможно, первого и
последнего, имеют ровно по одной ссылке, указывающей на них. Это простейшая и
наиболее интересующая нас ситуация — связные списки соответствуют
последовательностям элементов. В ходе обсуждения будут исследоваться более сложные
случаи.
Связные списки являются примитивными конструкциями в некоторых языках
программирования, но не в C++. Однако базовые строительные блоки, о которых шла речь
ранее, хорошо приспособлены для реализации связных списков. Указатели для ссылок и
структуры для узлов описываются следующим образом:
struct node { Item item; node *next; };
typedef node *link;
Эта пара выражений представляет собой не более чем код C для определения. Узлы
состоят из элементов (указанного здесь типа Item) и указателей на узлы.
Указатели на узлы также называются ссылками. Они обеспечивают большую гибкость и
более эффективную реализацию определенных операций, но этот простой пример
достаточен для понимания основ обработки списков.
Распределение памяти имеет ключевое значение для эффективного использования
связных списков. Выше описана единственная структура (struct node), но будет по- лучено
множество экземпляров этой структуры, по одному для каждого узла, который придется
использовать. Как только возникает необходимость использовать но- вый узел, для него
следует зарезервировать память. При объявлении переменной типа node для нее
резервируется память во время компиляции. Однако часто приходится организовывать
вычисления, связанные с резервированием памяти во время выполнения, посредством
вызовов системных операторов управления памятью. Например, в строке кода
link х = new node;
содержится оператор new, который резервирует достаточный для узла объем памяти и
возвращает указатель на него в переменной х.
В языке C широко принято инициализировать область хранения, а не только выделять для
нее память. В связи с этим обычно каждую описываемую структуру включается
конструктор (constructor). Конструктор представляет собой функцию, которая
описывается внутри структуры и имеет такое же имя. Конструкторы подробно
обсуждаются в главе 4. Они предназначены для предоставления исходных значений
данным структуры. Для этого конструкторы автоматически вызываются при создании
экземпляра структуры. Например, если описать узел списка при помощи следующего
кода:
struct node
{ Item item; node *next;
node (Item x; node *t)
{ item = x; next = t; };
};
typedef node *link;
то оператор
link t = new node(x, t) ;
не только резервирует достаточный для узла объем памяти и возвращает указатель на него
в переменной t, но и присваивает полю item узла значение х, а указателю поля — значение
t. Конструкторы помогают избегать ошибок, связанных с инициализацией данных.
Теперь, когда узел списка создан, возникает задача осуществления ссылок на
заключаемую в нем информацию — элемент и ссылку. Мы уже ознакомились с базовыми
операциями, необходимыми для выполнения этой задачи: достаточно снять косвенность
указателя, а затем использовать имена членов структуры. Ссылка х на элемент узла (тип
Item) имеет вид (*x).item, а на ссылку (тип link) — (*x).link. Эти операции так часто
используются, что в языке C++ для них существуют сокращенные эквиваленты: x->item и
x->link. Кроме того, часто возникает необходимость в выражении: "узел, указываемый
ссылкой х", поэтому упростим его: "узел х". Ссылка служит именем узла. Соответствие
между ссылками и указателями C++ имеет большое значение, но следует учитывать, что
ссылки являются абстракцией, а указатели — конкретным представлением. Можно
разрабатывать алгоритмы, где используются узлы и ссылки, а также выбрать одну из
многочисленных реализаций этой идеи. Например, ссылки можно представлять с
индексами, что будет показано в конце раздела. Рисунки 3.3 и 3.4 иллюстрируют две
основные операции, выполняемые со связны- ми списками. Можно удалить любой
элемент связного списка, уменьшив его длину на 1; а также вставить элемент в любую
позицию списка путем увеличения длины на 1.
В этих рисунках для простоты предполагается, что списки циклические и никогда не
становятся пустыми. В разделе 3.4 рассматриваются null-ссылки, фиктивные узлы и
пустые списки. Как показано на рисун- ках, для вставки или удаления необходимо лишь
два оператора C. Для удаления узла, следующего после узла х, используются такие
операторы:
t = x->next; x->next = t->next;
или проще:
x->next = x->next; x->next = t;
Для вставки в список узла t в позицию, следующую за узлом х используется такие
операторы:
t->next = x->next; x->next = t;
Простота вставки и удаления оправдывает существование связных списков.
Соответствующие операции неестественны и неудобны для массивов, поскольку требуют
перемещения всего содержимого массива, которое следует после затрагиваемого
элемента.
Связные списки плохо приспособлены для по- иска k-того элемента (по индексу) —
операции, которая характеризует эффективность доступа к данным массивов. В массиве
для доступа к Атому элементу используется простая запись а[к], а в списке для этого
необходимо отследить к ссылок.
Другая операция, неестественная для списков с единичными ссылками, — "поиск
элемента, пред- шествующего данному". После удаления узла из связного списка посредством операции x->next = x->next->next, повторное обращение к нему окажется
невозможным. Для небольших программ, вроде рас- смотренных вначале примеров, это не
имеет большого значения, но хорошей практикой программирования обычно считается
применение оператора delete. Он служит противоположностью оператора new для любого
узла, который более не придется использовать. В частности, последовательность
операторов
t = x->next; x->next = t->next; delete t;
не только удаляет t из списка, но также информирует систему, что задействованная память
может использоваться для других целей. Оператору delete особое внимание уделяется при
наличии больших списков либо большого их количества, но пока будем его игнорировать,
чтобы сосредоточиться на оценке преимуществ связных структур.
В качестве примера рас- смотрим следующую программу решения задачи Иосифа
(Флавия), которая служит интересным контрастом решету Эратосфена.
Пример циклического списка (задача Иосифа)
Для представления людей, расставленных в круг, построим циклический связный список,
где каждый элемент (человек) содержит ссылку на соседний элемент против хода часовой
стрелки. Целое число i представляет i-того человека в круге. После создания циклического
списка из одного узла вставляются узлы от 2 до N. В результате образуется окружность с
узлами от 1 до N. При этом переменная х указывает на N. Затем пропускаем М-1 узлов,
начиная с 1-го, и устанавливаем значение ссылки (M-i)-гo узла таким образом, чтобы
пропустить М-ый узел. Продолжаем эту операцию, пока не останется один узел.
#include <iostream.h>
#include <stdlib.h>
struct node
{ int item; node* next;
node(int x, node* t)
{ item = x; next = t; }
};
typedef node *link;
int main(int argc, char *argv[])
{ int i, N = atoi(argv[lj) , M = atoi (argv[2]) ;
link t = new node(l, 0); t->next = t;
link x » t;
for (i = 2; i <= N; i++)
x = (x->next = new node (i, t)) ;
while (x != x->next)
for (i = 1; i < M; i++) x = x->next;
x->next = x->next->next;
cout « x->ite» « endl;
}
Предположим, Л" человек решило выбрать главаря. Для этого они встали в круг и стали
удалять каждого М-го человека в определенном направлении отсчета, смыкая ряды после
каждого удаления. Задача состоит в определении, кто останется последним
(потенциальный лидер с математическими способностями заранее определит
выигрышную позицию в круге).
Номер выбираемого главаря является функцией от NuМ, называемой функцией Иосифа. В
более общем случае требуется выяснить порядок удаления людей. В примере если N = 9 и
М — 5, люди удаляются в порядке 5174369 2, а 8-ой номер становится избранным
главарем. Программа считывает значения N и М, а за- тем распечатывает эту
последовательность.
Для прямой имитации процесса выбора в программе используется циклический связный
список. Сначала создается список элементов от L до N. Для этого создается циклический
список с единственным узлом для участника 1, затем вставляются узлы для участников от
2 до N с помощью операции, иллюстрируемой на рис. 3.4. Затем в списке отсчитывается
М- 1 элемент и удаляется следующий при помощи операции, проиллюстрированной на
рисунке. Этот процесс продолжается до тех пор, пока не останется только один узел
(который будет указывать на самого себя).
Решето Эратосфена и задача Иосифа хорошо иллюстрируют различие между
использованием массивов и связных списков для представления последовательно
расположенных наборов объектов. Использование связного списка вместо массива для
построения решета Эратосфена скажется на производительности, поскольку
эффективность алгоритма зависит от возможности быстрого доступа к произвольному
элементу массива. Использование массива вместо связного списка для решения задачи
Иосифа снизит быстродействие, поскольку здесь эффективность алгоритма зависит от
возможности быстрого удаления элементов. При выборе структуры данных следует
учитывать ее влияние на эффективность алгоритма обработки данных. Это
взаимодействие структур данных и алгоритмов занимает центральное место в процессе
разработки программ и является постоянной темой данных лекций.
В языке C указатели служат прямой и удобной реализацией абстрактной концепции
связных списков, но важность абстракции не зависит от конкретной реализации.
Например, рисунок демонстрирует использование массивов целых чисел с целью
реализации связного списка для задачи Иосифа.
Таким образом, можно реализовать связный список с помощью индексов массива вместо
указателей. Связные списки применялись задолго до появления конструкций указателей в
языках высокого уровня, таких как C. Даже в современных системах реализации на основе
массивов иногда оказываются удобными.
Скачать