# Связные списки для самоваров
## 1. Списки
Список — это абстрактная структура данных. Структура данных — значит, набор какой-то инфы, как-то организованной. Вот список хранит **последовательность** элементов. И мы можем получить энный элемент с начала.
Кроме того, в список можно добавить новый элемент, удалить ненужный. А ещё по нему можно проходиться.
Здесь я тупо описал, что список должен делать. Но я не даю никакой реализации — кода, алгоритмов. Поэтому и называется это абстрактной структурой данных.
Как реализовать список? Можно хранить элементы последовательно в непрерывном куске памяти. Такой тип данных (уже не абстрактный) называется **вектором** или **массивом**. Ну, в C его уже по-любому юзали: `int[] array` какой-нибудь там.
Другой способ — сделать **связный список**. Вот о нём и поговорим.
## 2. Связные списки
Вот код.
```c
struct list {
int value;
struct list *next;
}
```
Здесь мы объявили структуру `struct list`, в который запихали 2 поля:
- `value` — это значение.
- `next` — указатель на следующий элемент. Если элемент последний в списке, то сюда пишем `NULL`.
Последняя хрень творит магию: мы из одного элемента можем получить следующий! А затем следующий за ним! И идти так до конца — пока `next` не `NULL`. Такая реализация называется **связным списком**, потому что элементы связаны через указатель.
Проверим, что это вообще список.
- [x] Храним последовательность элементов.
- [x] Можем получить энный элемент.
```c
size_t i = 0;
for (struct list *l = start; l != NULL; l = l->next) {
if (i == n) {
printf("Нашли энный элемент: %d\n", l->value);
break;
}
i++;
}
```
- [x] Можно добавить новый элемент.
```c
// Добавить `value` на место под номером `pos` в списке `l`.
void insert(struct list **l, size_t pos, int value) {
if (pos == 0 || *l == NULL) {
// Добавляем элемент в начало списка:
// - Создаём новый стракт
struct list *head = malloc(sizeof(struct list));
// - Сетим поля, следующим элементом будет *l
head->value = value;
head->next = *l;
// - А то, что было на месте *l, мы заменяем нашим новым первым элементом
*l = head;
} else if ((*l)->next != NULL) {
// Отрежем голову списка и уйдём в рекурсию.
insert(&(*l)->next, pos - 1, value);
} else {
// У нас pos больше или равен длине списка.
// Добавляем в конец и не мучаемся.
struct list *tail = malloc(sizeof(struct list));
tail->value = value;
tail->next = NULL;
(*l)->next = tail;
}
}
```
Здесь я заюзал рекурсию. Лучше это делать циклом, конечно. Но мне лень.
- [x] Можно удалить элемент какой-то.
```c
// Удалить элемент под номером `pos` из списка `l`.
void remove(struct list **l, size_t pos) {
if (*l == NULL) {
// Список пуст. Бывает и такое.
return;
}
if (pos == 0) {
// Удаляем первый элемент списка:
// - Выдёргиваем следующий элемент
struct list *next = (*l)->next;
// - Освобождаем память из-под удалённого элемента
free(*l);
// - Ставим `next` в главу списка
*l = next;
// - Профит
} else if ((*l)->next != NULL) {
// Ну, тут тоже рекурсией смотрим следующий элемент.
remove(&(*l)->next, pos - 1);
} else {
// Мы дошли до конца списка, но pos всё ещё не 0!
// Чутка зашкалил, значит, pos.
// Ничего не делаем.
return;
}
}
```
- [x] И можно проходиться по всем элементам.
```c
size_t i = 0;
for (struct list *l; l != NULL; l = l->next) {
printf("Элемент номер %zu: %d\n", i, l->value);
i++;
}
```
Все галочки помечены, то есть связный список — это список. Как неожиданно.
## 3. Зачем
Ну да, действительно, зачем вообще такая дрянь, когда есть обычный массив?
Плюсы:
- Легко добавить элемент в начало списка. В массиве вот нам придётся двигать все элементы на один вправо, чтобы освободить место под новый. Чтобы удалить, тоже двигать надо в массиве. А в связаном списке таких проблем нет.
Но есть другие!
- **Фрагментация памяти.**
Каждый элемент хранится в отдельном куске памяти. В массиве процессор может загрузить их памяти сразу дюжину элементов в кэш себе, а оттуда тырить что-либо — это наносекундное дело. В списке же связном, чтобы пройтись по всем элементам, надо кучу раз дёргать память. Это долго. Кэш бесполезен.
- **Добавление и удаление элементов** на самом деле — **долгая операция**.
Как минимум, пока у нас в списке не десятки тысяч значений. Потому что мы юзаем `malloc`. Нужно тыкать в операционную систему, чтобы она нам скачала ещё немного памяти. А тыкать — это долго и больно.
Массив, если он сделан добротно хорошим программистом, просит большие куски памяти, сразу на несколько элементов. Поэтому `malloc` юзается реже. А двигать память — это дело десятое, делается быстро.
Получается так, что на практике массивы почти всегда быстрее связных списков.
- **Взять энный элемент — это медленно и неудобно.**
Нужно проходиться по всему списку в худшем случае.
В массиве же просто множим индекс на размер элемента и получаем адрес в памяти. Быстро, удобно, меньше кода писать.
- **На `next` расходуется память.**
Ладно там, если значение занимает байт сотню. Восемь байт цеплять дополнительно — мелочь. Но инты так хранить — это уже в 2 раза больше памяти требуется, чем массиву. А чары какие-нибудь — вообще в 9.
Короче говоря, незачем. Связные списки можно отправить в топку. Почти всегда массивы справляются лучше или так же.
## P. S. Про двусвязные списки
Есть такие штуки — двусвязные списки. Это значит, что они тратят память ещё и на указатель на предыдущий элемент. С его помощью можно бегать против всех, в сторону начала, по списочку. Может быть полезно.