Линейный список - это конечная последовательность однотипных элементов (узлов), возможно, с повторениями. Количество элементов в последовательности называется длиной списка, причем длина в процессе работы программы может изменяться.
Линейный список F, состоящий из элементов D1,D2,...,Dn, записывают в виде
последовательности значений заключенной в угловые скобки F= Например, F1=< 2,3,1>,F2=< 7,7,7,2,1,12 >, F3=< >. Длина списков F1, F2, F3
равна соответственно 3,6,0.
При работе со списками на практике чаще всего приходится выполнять следующие
операции:
- найти элемент с заданным свойством;
- определить первый элемент в линейном списке;
- вставить дополнительный элемент до или после указанного узла;
- исключить определенный элемент из списка;
- упорядочить узлы линейного списка в определенном порядке.
В реальных языках программирования нет какой-либо структуры данных для
представления линейного списка так, чтобы все указанные операции над ним
выполнялись в одинаковой степени эффективно. Поэтому при работе с линейными
списками важным является представление используемых в программе линейных
списков таким образом, чтобы была обеспечена максимальная эффективность и по
времени выполнения программы, и по объему требуемой памяти.
Методы хранения линейных списков разделяются на методы последовательного и
связанного хранения. Рассмотрим простейшие варианты этих методов для списка с
целыми значениями F=<7,10>.
При последовательном хранении элементы линейного списка размещаются в
массиве d фиксированных размеров, например, 100, и длина списка указывается в
переменной l, т.е. в программе необходимо иметь объявления вида
Размер массива 100 ограничивает максимальные размеры линейного списка.
Список F в массиве d формируется так:
Полученный список хранится в памяти согласно схеме на рис.13.
При связанном хранении в качестве элементов хранения используются структуры,
связанные по одной из компонент в цепочку, на начало которой (первую структуру)
указывает указатель dl. Структура образующая элемент хранения, должна кроме
соответствующего элемента списка содержать и указатель на соседний элемент
хранения.
Описание структуры и указателя в этом случае может имееть вид:
Для выделения памяти под элементы хранения необходимо пользоваться функцией
malloc(sizeof(DL)) или calloc(l,sizeof(DL)). Формирование списка в связанном
хранении может осуществляется операторами:
В последнем элементе хранения (конец списка) указатель на соседний элемент
имеет значение NULL. Получаемый список изображен на рис.14.
D1
а
D2
а
D3
а
...
а
Dn
Рис.12. Изображение линейного списка.
float d[100]; int l;
d[0]=7; d[1]=10; l=2;
l:
2
d:
7
10
...
[0]
[1]
[2]
[3]
[98]
[99]
Рис.13. Последовательное хранение линейного списка.
typedef struct snd /* структура элемента хранения */
{ float val; /* элемент списка */
struct snd *n ; /* указатель на элемент хранения */
} DL;
DL *p; /* указатель текущего элемента */
DL *dl; /* указатель на начало списка */
p=malloc(sizeof(DL));
p->val=10; p->n=NULL;
dl=malloc(sizeof(DL));
dl->val=7; dl->n=p;
Рис.14. Связное хранение линейного списка.
При выборе метода хранения линейного списка следует учитывать, какие операции будут выполняться и с какой частотой, время их выполнения и объем памяти, требуемый для хранения списка.
Пусть имеется линейный список с целыми значениями и для его хранения
используется массив d (с числом элементов 100), а количество элементов в списке
указывается переменной l. Реализация указанных ранее операций над списком
представляется следующими фрагментами программ которые используют объявления:
float d[100];
int i,j,l;
1) печать значения первого элемента (узла)
if (i<0 || i>l) printf("\n нет элемента");
else printf("d[%d]=%f ",i,d[i]);
2) удаление элемента, следующего за i-тым узлом
if (i>=l) printf("\n нет следующего ");
l--;
for (j=i+1;j
Схема движения индексов i,j,t и значения aux=d[i] при выполнении приведенного фрагмента программы приведена на рис.15.
Количество действий Q, требуемых для выполнения приведенных операций над списком, определяется соотношениями: для операций 1 и 2 - Q=1; для операций 3,4 - Q=l; для операции 5 - Q=l*l.
Заметим, что вообще операцию 5 можно выполнить при количестве действий порядка l, а операции 3 и 4 для включения и исключения элементов в конце списка, часто встречающиеся при работе со стеками, - при количестве действий 1.
Более сложная организация операций требуется при размещении в массиве d нескольких списков, или при размещении списка без привязки его начала к первому элементу массива.
При простом связанном хранении каждый элемент списка представляет собой
структуру nd, состоящую из двух элементов: val - предназначен для хранения
элемента списка, n - для указателя на структуру, содержащую следующий элемент
списка. На первый элемент списка указывает указатель dl. Для всех операций над
списком используется описание:
typedef struct nd
{ float val;
struct nd * n; } ND;
int i,j;
ND * dl, * r, * p;
Для реализации операций могут использоваться следующие фрагменты программ:
1) печать значения i-го элемента
r=dl;j=1;
while(r!=NULL && j++n ;
if (r==NULL) printf("\n нет узла %d ",i);
else printf("\n элемент %d равен %f ",i,r->val);
2) печать обоих соседей узла(элемента), определяемого указателем p (см. рис.16)
if((r=p->n)==NULL) printf("\n нет соседа справа");
else printf("\n сосед справа %f", r->val);
if(dl==p) printf("\n нет соседа слева" );
else { r=dl;
while( r->n!=p ) r=r->n;
printf("\n левый сосед %f", r->val);
}
3) удаление элемента, следующего за узлом, на который указывает р (см. рис.17)
if ((r=p->n)==NULL) printf("\n нет следующего");
p->n=r->n; free(r->n);
4) вставка нового узла со значением new за элементом, определенным указателем р (см. рис.18)
r=malloc(1,sizeof(ND));
r->n=p->n; r->val=new; p->n=r;
5) частичное упорядочение списка
Рис.19. Схема частичного упорядочения списка.
ND *v;
float k1;
k1=dl->val;
r=dl;
while( r->n!=NULL )
{ v=r->n;
if (v->valn=v->n;
v->n=dl;
dl=v;
}
else r=v;
}
Количество действий, требуемых для выполнения указанных операций над списком в связанном хранении, оценивается соотношениями: для операций 1 и 2 - Q=l; для операций 3 и 4 - Q=1; для операции 5 - Q=l.
Связанное хранение линейного списка называется списком с двумя связями или двусвязным списком, если каждый элемент хранения имеет два компонента указателя (ссылки на предыдущий и последующий элементы линейного списка).
В программе двусвязный список можно реализовать с помощью описаний:
typedef struct ndd
{ float val; /* значение элемента */
struct ndd * n; /* указатель на следующий элемент */
struct ndd * m; /* указатель на предыдующий элемент */
} NDD;
NDD * dl, * p, * r;
Графическая интерпретация метода связанного хранения списка F=< 2,5,7,1 > как списка с двумя связями приведена на рис.20.
Вставка нового узла со значением new за элементом, определяемым указателем p,
осуществляется при помощи операторов:
r=malloc(NDD);
r->val=new;
r->n=p->n;
(p->n)->m=r;
p->=r;
Удаление элемента, следующего за узлом, на который указывает p
p->n=r;
p->n=(p->n)->n;
( (p->n)->n )->m=p;
free(r);
Связанное хранение линейного списка называется циклическим списком, если его последний указывает на первый элемент, а указатель dl - на последний элемент списка.
Схема циклического хранение списка F=< 2,5,7,1 > приведена на рис.21.
При решении конкретных задач могут возникать разные виды связанного хранения.
Пусть на входе задана последовательность целых чисел B1,B2,...,Bn из интервала от 1 до 9999, и пусть Fi (1 по возрастанию. Составить процедуру для формирования Fn в связанном хранении и возвращения указателя на его начало.
При решении задачи в каждый момент времени имеем упорядоченный список Fi и при вводе элемента Bi+1 вставляем его в нужное место списка Fi, получая упорядоченный список Fi+1. Здесь возможны три варианта: в списке нет элементов; число вставляется в начало списка; число вставляется в конец списка. Чтобы унифицировать все возможные варианты, начальный список организуем как связанный список из двух элементов <0,1000>.
Рассмотрим программу решения поставленной задачи, в которой указатели dl,
r, p, v имеют следующее значение: dl указывает начало списка; p, v - два
соседних узла; r фиксирует узел, содержащий очередное введенное значение in.
#include
В зависимости от метода доступа к элементам линейного списка различают разновидности линейных списков называемые стеком, очередью и двусторонней очередью.
Стек - это конечная последовательность некоторых однотипных элементов -
скалярных переменных, массивов, структур или объединений, среди которых могут
быть и одинаковые. Стек обозначается в виде: S= Допустимыми операциями над стеком являются:
- проверка стека на пустоту S=< >,
- добавление нового элемента Sn+1 в конец стека - преобразование
< S1,...,Sn> в < S1,...,Sn+1>;
- изъятие последнего элемента из стека - преобразование < S1,...,Sn-1,Sn>
в < S1,...,Sn-1>;
- доступ к его последнему элементу Sn, если стек не пуст.
Таким образом, операции добавления и удаления элемента, а также доступа к
элементу выполняются только в конце списка. Стек можно представить как стопку
книг на столе, где добавление или взятие новой книги возможно только сверху.
Очередь - это линейный список, где элементы удаляются из начала списка, а
добавляются в конце списка (как обыкновенная очередь в магазине).
Двусторонняя очередь - это линейный список, у которого операции добавления
и удаления элементов и доступа к элементам возможны как вначале так и в конце
списка. Такую очередь можно представить как последовательность книг стоящих на
полке, так что доступ к ним возможен с обоих концов.
Реализация стеков и очередей в программе может быть выполнена в виде
последовательного или связанного хранения. Рассмотрим примеры организации
стека этими способами.
Одной из форм представления выражений является польская инверсная запись,
задающая выражение так, что операции в нем записываются в порядке выполнения,
а операнды находятся непосредственно перед операцией.
Например, выражение
в польской инверсной записи имеет вид
Особенность такой записи состоит в том, что значение выражения можно
вычислить за один просмотр записи слева направо, используя стек, который до
этого должен быть пуст. Каждое новое число заносится в стек, а операции
выполняются над верхними элементами стека, заменяя эти элементы результатом
операции. Для приведенного выражения динамика изменения стека будет иметь вид
Ниже приведена функция eval, которая вычисляет значение выражения, заданного
в массиве m в форме польской инверсной записи, причем m[i]>0 означает
неотрицательное число, а значения m[i]<0 - операции. В качестве кодировки
операций сложения, вычитания, умножения и деления выбраны отрицательные числа
-1, -2, -3, -4. Для организации последовательного хранения стека используется
внутренний массив stack. Параметрами функции являются входной массив a и его
длина l.
Рассмотрим другую задачу. Пусть требуется ввести некоторую последовательность
символов, заканчивающуюся точкой, и напечатать ее в обратном порядке (т.е. если
на входе будет "ABcEr-1." то на выходе должно быть "1-rEcBA"). Представленная
ниже программа сначала вводит все символы последовательности, записывая их в
стек, а затем содержимое стека печатается в обратном порядке. Это основная
особенность стека - чем позже элемент занесен в стек, тем раньше он будет
извлечен из стека. Реализация стека выполнена в связанном хранении при помощи
указателей p и q на тип, именованный именем STACK.
При хранении больших объемов информации в форме линейных списков
нежелательно хранить элементы с одинаковым значением, поэтому используют
различные методы сжатия списков.
Сжатое хранение. Пусть в списке B= Достоинство сжатого хранения списка при большом числе элементов со значением
V заключается в возможности уменьшения объема памяти для его хранения.
Поиск i-го элемента в связанном сжатом хранении осуществляется методом
полного просмотра, при последовательном хранении - методом бинарного поиска.
Преимущества и недостатки последовательного сжатого и связанного сжатого
хранений аналогичны преимуществам и недостаткам последовательного и связанного
хранений.
Рассмотрим следующую задачу. На входе заданы две последовательности целых
чисел M=< M1,M2,...,M10000 >, N=< N1,N2,...,N10000 >, причем 92% элементов
последовательности М равны нулю. Составить программу для вычисления суммы
произведений Mi * Ni, i=1,2,...,10000.
Предположим, что список М хранится последовательно сжато в массиве структур
m с объявлением:
Для определения конца списка добавим еще один элемент с порядковым номером
m[j].nm=10001, который называется стоппером (stopper) и располагается за
последним элементом сжатого хранения списка в массиве m.
Программа для нахождения искомой суммы имеет вид:
Индексное хранение используется для уменьшения времени поиска нужного
элемента в списке и заключается в следующем. Исходный список B =
< K1,K2, ...,Kn > разбивается на несколько подсписков В1,В2, ...,Вm таким
образом, что каждый элемент списка В попадает только в один из подсписков, и
дополнительно используется индексный список с М элементами, указывающими на
начало списков В1,В2, ...,Вm.
Считается, что список хранится индексно с помощью подсписков B1,B2, ...,Bm
и индексного спискa X = < ADG1,ADG2,... ADGm >, где ADGj - адрес начала
подсписка Bj, j=1,M.
При индексном хранении элемент К подсписка Bj имеет индекс j. Для получения
индексного хранения исходный список В часто преобразуется в список В' путем
включения в каждый узел еще и его порядкового номера в исходном списке В, а в
j-ый элемент индексного списка Х, кроме ADGj, может включаться некоторая
дополнительная информация о подсписке Bj. Разбиение списка В на подсписки
осуществляется так, чтобы все элементы В, обладающие определенным свойством Рj,
попадали в один подсписок Bj.
Достоинством индексного хранения является то, что для нахождения элемента К
с заданным свойством Pj достаточно просмотреть только элементы подсписка Bj; его
начало находится по индексному списку Х, так как для любого К, принадлежащего
Bi, при i не равном j свойство Pj не выполняется.
В разбиении В часто используется индексная функция G(K), вычисляющая по
элементу К его индекс j, т.е. G(K)=j. Функция G обычно зависит от позиции К,
обозначаемой поз.K, в подсписке В или от значения определенной части компоненты
К - ее ключа.
Рассмотрим список B=< K1,K2, ...,K9 > с элементами
Если для разбиения этого списка на подсписки в качестве индексной функции
взять Ga(K)=1+(поз.K-1)/3, то список разделится на три подсписка:
Добавляя всюду еще и начальную позицию элемента в списке, получаем:
Если в качестве индексной функции выбрать другую функцию Gb(K)=1+(поз.K-1)%3,
то получим списки:
Теперь для нахождения узла K6 достаточно просмотреть только одну из трех
последовательностей (списков). При использовании функции Ga(K) это список B2a',
а при функции Gb(K) список B3b".
Для индексной функции Gc(K)=1+K1/100, где K1 - первая компонента элемента К,
находим:
Чтобы найти здесь узел К с первым компонентом-ключом К1=77, достаточно
просмотреть список B2.
При реализации индексного хранения применяется методика А для хранения
индексного списка Х (функция Ga(X) ) и методика C для хранения подсписков
B1,B2,...,Bm (функция Gc(Bi)), т.е. используется, так называемое, A-C индексное
хранение.
В практике часто используется последовательно-связанное индексное хранение.
Так как обычно длина списка индексов известна, то его удобно хранить
последовательно, обеспечивая прямой доступ к любому элементу списка индексов.
Подсписки B1,B2,...,Bm хранятся связанно, что упрощает вставку и удаление
узлов(элементов). В частности, подобный метод хранения используется в ЕС ЭВМ
для организации, так называемых, индексно-последовательных наборов данных, в
которых доступ к отдельным записям возможен как последовательно, так и при
помощи ключа.
Последовательно-связанное индексное хранение для приведенного примера
изображено на рис.24, где X= Рассмотрим еще одну задачу. На входе задана последовательность целых
положительных чисел, заканчивающаяся нулем. Составить процедуру для ввода этой
последовательности и организации ее последовательно-связанного индексного
хранения таким образом, чтобы числа, совпадающие в двух последних цифрах,
помещались в один подсписок.
Выберем в качестве индексной функции G(K)=K%100+1, а в качестве индексного
списка Х - массив из 100 элементов. Следующая функция решает поставленную
задачу:
Возвращаемым значением функции index будет число обработанных элементов
списка.
Для индексного списка также может использоваться индексное хранение. Пусть,
например, имеется список B= Требуется разделить его на семь подсписков, т.е. X=
(6+8)*5-6/2
6 8 + 5 * 6 2 / -
S = < >; <6>; <6,8>; <14>; <14,5>; <70>;
<70,6>; <70,6,2>; <70,3>; <67>.
float eval (float *m, int l)
{ int p,n,i;
float stack[50],c;
for(i=0; i < l ;i++)
if ((n=m[i])<0)
{ c=st[p--];
switch(n)
{ case -1: stack[p]+=c; break;
case -2: stack[p]-=c; break;
case -3: stack[p]*=c; break;
case -4: stack[p]/=c;
}
}
else stack[++p]=n;
return(stack[p]);
}
#include
2.1.6. Сжатое и индексное хранение линейных списков
1,C
3,Y
6,S
7,H
9,T
Рис.22. Последовательное сжатое хранение списка.
Рис.23. Связное сжатое хранение списка.
struct
{ int nm;
float val; } m[10000];
# include
К1=(17,Y), K2=(23,H), K3=(60,I), K4=(90,S), K5=(66,T),
K6=(77,T), K7=(50,U), K8=(88,W), K9=(30,S).
B1a=<(17,Y),(23,H),(60,I)>,
B2a=<(90,S),(66,T),(77,T)>,
B3a=<(50,U),(88,W),(30,S)>.
B1a'=<(1,17,Y),(2,23,H),(3,60,I)>,
B2a'=<(4,90,S),(5,66,T),(6,77,T)>,
B3a'=<(7,50,U),(8,88,W),(9,30,S)>.
B1b"=<(1,17,Y),(4,90,S),(7,50,U)>,
B2b"=<(2,23,H),(5,66,T),(8,88,U)>,
B3b"=<(3,60,I),(6,77,T),(9,30,S)>.
B1=<(17,Y),(23,H),(60,I),(90,S)>,
B2=<(66,T),(77,T)>,
B3=<(50,U),(88,W)>,
B4=<(30,S)>.
Рис.24. Последовательно-связанное индексное хранение списка.
#include
K1=(338,Z), K2=(145,A), K3=(136,H), K4=(214,I), K5 =(146,C),
K6=(334,Y), K7=(333,P), K8=(127,G), K9=(310,O), K10=(322,X).
Рис.25. Связанно-связанное связанное индексное хранение списка.
[ Назад | Оглавление | Вперед ]