Свързан списък
Linked List
В тази тема ще разгледаме какво е свързан списък, какви операции поддържа и с каква сложност е всяка от тях. Ще видим как се имплементира двусвързан списък и ще дадем цялостна имплементация като шаблонен клас, както и код, с който да тествате ваша такава.
Вече видяхме един начин, по който можем да пазим неизвестен брой елементи. Съществува и друга, много известна структура данни, която върши това.
Списък
Структурата данни "Свързан списък" представлява поредица от елементи, като всеки от тях съдържа не само информацията, която бихме пазили в масив, ами и информация кой е следващият елемент в поредицата. В най-стандартната имплементация, тази информация представлява указател към следващия елемент. Така лесно можем да обходим всички елементи от структурата - почвайки от първия, следваме неговия указател към следващия, от там към по-следващия и т.н. Допълнително ще пазим два указателя - към първия и към последния елемент на списъка - което ни позволява лесно да добавяме елемент както в началото, така и в края на списъка.Предимства
- Можем бързо да добавяме елемент (с
O(1)
) както в началото, така и в края на списъка. При масивите добавянето в края беше също константна операция, но в началото отнемаше целиO(N)
стъпки. - С
O(1)
можем да добавим елемент и на произволна позиция по средата на списъка, стига вече да имаме указател, сочещ към елемента преди или след тази позиция. - Можем да поддържаме паметта точно колкото ни трябва (без "резервни" клетки) като сложността на добавяне си остава
O(1)
.
Недостатъци
- Най-големият недостатък в сравнение с масивите е, че губим възможността за произволен достъп. Няма начин, по който да индексираме k-тия елемент, без да минем през всички k-1 предходни. Това прави операции като сортиране, двоично търсене и т.н. невъзможни, или поне неприемливо бавни.
- Тъй като всяка клетка от списъка съдържа указател към следващата клетка, то всеки елемент заема повече памет, отколкото би заемала клетката в масив. Когато информацията във всяка клетка е относително много, това не е проблем, но ако просто пазим по един
int
, то ние ще се нуждаем от поне двойно повече памет. Нашата имплементация ще е двусвързан списък, тоест ще имаме и още един указател към предходния елемент в списъка, което прави нещата дори по-зле в това отношение. - Заделяме памет при всяко добавяне на нов елемент, което може да бъде сравнително бавно.
- Копирането е учудващо сложно.
Кога бихме ползвали списък пред масив?
Сравнително рядко, но все пак има случаи, в които списъкът е по-удачен избор от динамичен масив. Един вариант е когато наистина ни трябва да пестим паметта възможно най-много и все пак да имаме бързо добавяне на елементи.В състезателното програмиране списъците се ползват доста рядко, като на този сайт ще ги ползваме само в един от вариантите на представяне на графи. Даже там ще ползваме хибрид между свързан списък и динамичен масив - ще заделяме по много клетки наведнъж, както правихме при динамичния масив, за да избегнем бавнотата на заделянето на памет.
Имплементация
Следва да покажем примерна имплементация на двусвързан списък, с основните операции за добавяне в началото и края, търсене и взимане на k-тия елемент.Единична клетка
Освен информацията (която отново ще е темплейтен типDataType
), всяка клетка ще съдържа указател към предходната и следващата клетка в списъка. Ще енкапсулираме тези данни в структура Element
.
typedef int DataType;
struct Element {
DataType data;
Element *prev, *next;
Element(const DataType& value) {
data = value;
prev = next = NULL;
}
};
Данни
Ще ни трябват един указател към първата клетка (наричана глава или head), един указател към последната клетка (наричана опашка или tail, и единint
, който да пази общия брой клетки. Забележете, че този брояч реално не ни трябва, но ще го пазим за да можем да изпълняваме метода size()
с O(1)
).
struct List {
int size;
Element *head, *tail;
};
Инициализация
В началото броят елементи в списъка е нула, а двата указателя (за начало и край) не сочат никъде.void init(List& list) {
list.size = 0;
list.head = list.tail = NULL;
}
size()
Връща колко елемента има в списъка. Тук допълнителната променлива size ни помага да извършваме тази операция сO(1)
. Всъщност не се нуждаем от функция за това (поне при тази имплементация), можем директно да ползваме list.size
в кода си.
int size(List& list) {
return list.size;
}
empty()
Показва дали списъкът е празен. Отново, можем да минем и без тази функция.bool empty(List& list) {
return !list.size;
}
front()
Връща първия елемент от списъка. Спира програмата ако списъкът е празен (тогава тази операция е невалидна).DataType& front(List& list) {
assert(list.size > 0);
return list.head->data;
}
back()
Връща последния елемент от списъка. Спира програмата ако списъкът е празен (тогава тази операция е невалидна).DataType& back(List& list) {
assert(list.size > 0);
return list.tail->data;
}
at()
Връща елемента на позиция index. Спира програмата ако индексът е по-малък от нула или по-голям или равен на броя елементи на списъка (тогава тази операция е невалидна).DataType& at(List& list, int index) {
assert(index >= 0 && index < list.size);
Element* link = list.head;
for (int i = 0; i < index; i++)
link = link->next;
return link->data;
}
push_front()
Добавя елемент в началото на списъка.void push_front(List& list, const DataType& value) {
Element* element = new Element(value);
element->next = list.head;
if (list.size == 0)
list.head = list.tail = element;
else
list.head = list.head->prev = element;
list.size++;
}
push_back()
Добавя елемент в края на списъка.void push_back(List& list, const DataType& value) {
Element* element = new Element(value);
element->prev = list.tail;
if (list.size == 0)
list.head = list.tail = element;
else
list.tail = list.tail->next = element;
list.size++;
}
insert()
Добавя елемент на позиция index или в началото/края на списъка, ако позицията е преди/след първия/последния елемент.void insert(List& list, int index, const DataType& value) {
if (index <= 0) {
push_front(list, value);
}
else if (index >= list.size) {
push_back(list, value);
}
else {
Element* link = list.head;
for (int i = 0; i < index - 1; i++)
link = link->next;
Element* element = new Element(value);
element->prev = link;
element->next = link->next;
element->prev->next = element;
element->next->prev = element;
list.size++;
}
}
pop_front()
Премахва първия елемент от списъка, ако такъв съществува.void pop_front(List& list) {
if (list.size == 0)
return;
if (list.size == 1) {
delete list.head;
list.head = list.tail = NULL;
}
else {
Element* link = list.head;
list.head = link->next;
list.head->prev = NULL;
delete link;
}
list.size--;
}
pop_back()
Премахва последния елемент от списъка, ако такъв съществува.void pop_back(List& list) {
if (list.size == 0)
return;
if (list.size == 1) {
delete list.head;
list.head = list.tail = NULL;
}
else {
Element* link = list.tail;
list.tail = link->prev;
list.tail->next = NULL;
delete link;
}
list.size--;
}
erase()
Изтрива даден елемент или интервал от елементи, зададени чрез индекс към първия и един след последния.void erase(List& list, int first, int last) {
first = (first < 0 ? 0 : first);
int count = (last == -1 ? 1 : last - first);
if (first >= list.size || count <= 0)
return;
Element* link = list.head;
for (int i = 0; i < first; i++)
link = link->next;
for (int i = 0; i < count && link != NULL; i++) {
if (list.head == link)
list.head = link->next;
if (list.tail == link)
list.tail = link->prev;
if (link->prev != NULL)
link->prev->next = link->next;
if (link->next != NULL)
link->next->prev = link->prev;
Element* next = link->next;
delete link;
link = next;
list.size--;
}
}
clear()
Изтрива всички елементи от списъка. Това е еквивалентно даerase()
-нем от първия до последния елемент, което и всъщност ще направим за да не пишем кода наново.
void clear(List& list) {
while (list.head != NULL) {
Element* element = list.head;
list.head = list.head->next;
delete element;
}
list.size = 0;
list.head = list.tail = NULL;
}
Цялостна имплементация
Цялостна имплементация и тестове: List.h | ListTester.cppХибриден лист
Една интересна модификация на стандартния свързан списък, донякъде известна в българските състезателни среди като "структура на Велин" (тъй като Велин Цанов я преподаваше по време на подготовката на националите за IOI) пази множество листове в един динамичен масив. Нея ще разгледаме малко по-нататък, в тази тема.Допълнителни материали
- Информация за абстрактния тип Лист (en.wikipedia.org)
- Структурата данни List в STL (www.cplusplus.com)
- Опашка (www.informatika.bg)
За да предложите корекция, селектирайте думата или текста, който искате да бъде променен,
натиснете Enter и изпратете Вашето предложение.
натиснете Enter и изпратете Вашето предложение.
Страницата е посетена 19201 пъти.