Реализация операций над связными линейными списками
В разделе рассматриваются некоторые операции над линейными списками. Выполнение операций иллюстрируется рисунками со схемами изменения связей и программными примерами. На всех рисунках сплошными линиями показаны связи, имевшиеся до выполнения и сохранившиеся после выполнения операции, а значком 'x' отмечены связи, разорванные при выполнении операции.
Во всех операциях важна последовательность коррекции указателей, которая обеспечивает корректное изменение списка, не затрагивающее другие элементы. При неправильном порядке коррекции легко потерять часть списка. Поэтому рядом с устанавливаемыми связями в скобках показаны шаги, на которых эти связи устанавливаются. Во всех примерах считаются определенными следующие типы данных:
структура информационного поля списка:
type
data = ...;
элемент односвязного списка (sll - single linked list):
sllptr = ^slltype; { указатель в односвязном списке }
slltype = record { элемент односвязного списка }
inf : data; { информационная часть }
next : sllptr; { указатель на следующий элемент }
end;
элемент двухсвязного списка (dll - double linked list):
dllptr = ^dlltype; { указатель в двухсвязном списке }
dlltype = record { элемент односвязного списка }
inf : data; { информационная часть }
next : sllptr; { указатель на следующий элемент (вперед) }
prev : sllptr; { указатель на предыдущий элемент (назад) }
end;
Перебор элементов списка. Операция перебора, возможно, чаще других выполняется над линейными списками. При ее выполнении осуществляется последовательный доступ к элементам списка – ко всем элементам до конца списка или до нахождения искомого элемента.
{ Перебор 1-связного списка }
procedure LookSll(head: sllptr);
{ head - указатель на начало списка }
var cur: sllptr; { адрес текущего элемента }
Begin
cur:=head; { первый элемент списка назначается текущим }
while cur <> nil do
Begin
< обработка c^.inf >
{ обрабатывается информационная часть
эл-та, на который указывает cur.
Обработка может состоять в:
- печати содержимого инф. части;
- модификации полей инф. части;
- сравнения полей инф. части с образцом при поиске по ключу; }
cur:=cur^.next;
{ из текущего элемента выбирается указатель
на следующий элемент и для следующей итерации
следующий элемент становится текущим; если текущий
элемент был последний, то его поле next содержит
пустой указатель и в cur запишется nil, что приведет
к выходу из цикла при проверке условия while }
end;
end;
В двухсвязном списке возможен перебор как в прямом направлении (выглядит так же, как и перебор в односвязном списке), так и в обратном. В последнем случае параметром процедуры должен быть указатель на конец списка, и переход к следующему элементу должен осуществляться по указателю назад:
cur:=cur^.prev;
В кольцевом списке окончание перебора должно происходить не по признаку последнего элемента – такой признак отсутствует, а по достижению элемента, с которого начался перебор.
Вставка элемента в список. Вставка элемента в середину односвязного списка показана на рис. 6.4.
Рис. 6.4. Вставка элемента в середину 1-связного списка.
Следующий пример демонстрирует реализацию этой операции.
{ Вставка элемента в середину 1-связного списка }
procedure InsertSll(prev: sllptr; inf: data);
{ prev - адрес предыдущего эл-та; inf - данные нового элемента }
var cur: sllptr; { адрес нового элемента }
Begin
{ Создание нового элемента – выделение
памяти для него и запись инф. части }
New(cur);
cur^.inf:=inf;
{ элемент, следовавший за предыдущим
теперь будет следовать за новым }
cur^.next:=prev^.next;
{ новый элемент следует за предыдущим }
prev^.next:=cur;
end;
Рисунок 6.5 представляет вставку в двухсвязный список.
Рис. 6.5. Вставка элемента в середину 2-связного списка.
Приведенные примеры обеспечивают вставку в середину списка, но не могут быть применены для вставки в начало списка. В этом случае должен модифицироваться указатель на начало списка, как показано на рис. 6.6.
Рис. 6.6. Вставка элемента в начало 1-связного списка.
В примере процедура выполняет вставку элемента в любое место односвязного списка.
{ Вставка элемента в любое место 1-связного списка }
procedure InsertSll(
Var
{ указатель на начало списка, может измениться в
процедуре, если head=nil - список пустой }
head: sllptr;
{ указатель на элемент, после которого делается вставка,
если prev=nil - вставка перед первым элементом }
prev: sllptr;
inf: data { данные нового элемента }
cur: sllptr); { адрес нового элемента }
Begin
{ выделение памяти для нового элемента и запись его инф. части }
New(cur);
cur^.inf:=inf;
{ если есть предыдущий элемент - вставка в середину списка }
if prev <> nil then
Begin
cur^.next:=prev^.next;
prev^.next:=cur;
End else
{ вставка в начало списка }
Begin
{ новый элемент указывает на бывший первый элемент списка;
если head = nil, то новый элемент будет и последним элементом списка }
cur^.next:=head;
{ новый элемент становится первым в списке,
указатель на начало теперь указывает на него }
head:=cur;
end;
end;
Удаление элемента из списка. Удаление элемента из односвязного списка показано на рис. 6.7. Процедуру удаления легко выполнить, если известен адрес элемента, предшествующего удаляемому (prev на рис. 6.7. а). Однако на рис. 6.7 и в примере приводится процедура для случая, когда удаляемый элемент задается своим адресом del.
а).
б).
Рис. 6.7. Удаление элемента из 1-связного списка из середины списка (а) и из начала (б).
Процедура обеспечивает удаление из середины и из начала списка.
{ Удаление элемента из любого места 1-связного списка }
procedure DeleteSll(
var head: sllptr; { указатель на начало списка, может измениться в процедуре }
del: sllptr); { указатель на элемент, который удаляется }
var prev: sllptr; { адрес предыдущего элемента }
Begin
{ попытка удаления из пустого списка расценивается как ошибка
(в последующих примерах этот случай учитываться не будет) }
if head = nil then
Begin
Writeln('Ошибка!');
Halt;
end;
{ если удаляемый элемент - первый в списке,
то следующий за ним становится первым }
if del = head then
head:=del^.next else
{ удаление из середины списка }
Begin
{ приходится искать элемент, предшествующий удаляемому; поиск производится перебором списка с самого его начала, пока не будет найдет элемент, поле next которого совпадает с адресом удаляемого элемента }
prev:=head^.next;
while (prev^.next <> del) and (prev^.next <> nil) do
prev:=prev^.next;
if prev^.next = nil then
Begin
{ это случай, когда перебран весь список, но элемент не найден,
он отсутствует в списке; расценивается как ошибка }
Writeln('Ошибка!');
Halt;
end;
{ предыдущий элемент теперь указывает на следующий за удаляемым }
prev^.next:=del^.next;
end;
{ элемент исключен из списка, теперь можно освободить занимаемую им память }
Dispose(del);
end;
Удаление элемента из двухсвязного списка требует коррекции большего числа указателей, как показано на рис. 6.8. Процедура удаления, чем для односвязного списка, так как в ней не нужен поиск предыдущего элемента (выбирается по указателю назад).
Рис. 6.8. Удаление элемента из 2-связного списка.
Перестановка элементов списка. Изменчивость динамических структур предполагает не только изменения размера структуры, но и изменения связей между элементами. В качестве примера приведем перестановку двух соседних элементов списка (рис. 6.9). В алгоритме предполагается известным адрес элемента, предшествующего паре, в которой производится перестановка. В алгоритме также не учитывается случай перестановки первого и второго элементов.
Рис. 6.9. Перестановка соседних элементов 1-связного списка.
{ Перестановка двух соседних элементов в 1-связном списке }
procedure ExchangeSll(
prev: sllptr { указатель на элемент, предшествующий переставляемой паре });
var p1, p2: sllptr); { указатели на элементы пары }
Begin
p1:=prev^.next; { указатель на 1-й элемент пары }
p2:=p1^.next; { указатель на 2-й элемент пары }
p1^.next:=p2^.next; { 1-й элемент пары указывает на следующий за парой }
p2^.next:=p1; { 1-й элемент пары теперь следует за 2-ым }
prev^.next:=p2; { 2-й элемент пары теперь становится 1-ым }
end;
В процедуре перестановки для двухсвязного списка нетрудно учесть и перестановку в начале и конце списка (рис. 6.10).
Рис. 6.10. Перестановка соседних элементов 2-связного списка.
Копирование части списка. При копировании исходный список сохраняется в памяти, и создается новый список. Информационные поля элементов нового списка содержат те же данные, что и в элементах старого списка, но поля связок в новом списке совершенно другие, поскольку элементы нового списка расположены по другим адресам в памяти. Существенно, что операция копирования предполагает дублирование данных в памяти.
{ Копирование части 1-связного списка; head - указатель на начало копируемой
части; num - число элементов. Функция возвращает указатель на список-копию }
function CopySll(head: sllptr; num: integer): sllptr;
var cur, head2, cur2, prev2: sllptr;
Begin
{ исходный список пуст - копия пуста }
if head = nil then
CopySll:=nil else
Begin
cur:=head;
prev2:=nil;
{ перебор исходного списка до конца или по счетчику num }
while (num > 0) and (cur <> nil) do
Begin
{ выделение памяти для элемента выходного списка и
запись в него информационной части }
New(cur2);
cur2^.inf:=cur^.inf;
{ если 1-й эл-т выходного списка - запоминается указатель на
начало, иначе - записывается указатель в предыдущий элемент }
if prev2 <> nil then
prev2^.next:=cur2 else head2:=cur2;
prev2:=cur2; { текущий элемент становится предыдущим }
cur:=cur^.next; { продвижение по исходному списку }
num:=num-1; { подсчет элементов }
end;
cur2^.next:=nil; { пустой указатель - в последний элемент выходного списка }
CopySll:=head2; { вернуть указатель на начало выходного списка }
end;
end;
Слияние двух списков. Операция слияния заключается в формировании из двух списков одного и аналогична операции сцепления строк. В случае односвязного списка слияние выполняется просто. Последний элемент первого списка содержит пустой указатель на следующий элемент, этот указатель служит признаком конца списка. Вместо пустого указателя в последний элемент первого списка заносится указатель на начало второго списка. Таким образом, второй список становится продолжением первого.
{ Слияние двух списков. head1 и head2 - указатели на начала
списков. На результирующий список указывает head1 }
procedure UniteSll(var head1, head2: sllptr);
var cur: sllptr;
begin { если 2-й список пустой - нечего делать }
if head2 <> nil then
Begin
{ если 1-й список пустой, выходным списком будет 2-й }
if head1 = nil then
head1:=head2 else
{ перебор 1-го списка до последнего его элемента }
Begin
cur:=head1;
while cur^.next <> nil do
cur:=cur^.next;
{ последний элемент 1-го списка указывает на начало 2-го }
cur^.next:=head2;
end;
head2:=nil; { 2-й список аннулируется }
end;
end;
Дата добавления: 2021-12-14; просмотров: 302;