Сравнение линейных структур
Сводная таблица структур
Соберём все линейные структуры в одну таблицу, чтобы выбирать осознанно. Сложность указана для худшего случая; «память» — накладные расходы сверх самих данных.

| Структура | Доступ по индексу | Вставка/удаление в начало | Вставка/удаление в конец | Вставка/удаление в середину | Поиск | Доп. память |
|---|---|---|---|---|---|---|
| Массив | O(1) | O(N) (сдвиг) | O(1) (если есть место) / O(N) (рост) | O(N) | O(N) | нет |
| Односвязный список | O(N) | O(1) | вставка O(1) с tail, иначе O(N); удаление O(N) | O(N) (нужен пред.) | O(N) | next на узел |
| Двусвязный список | O(N) | O(1) | O(1) | O(1) (если узел известен) | O(N) | prev + next |
| Кольцевой список | O(N) | O(1) | вставка O(1) с tail; удаление O(1), если известен пред. | O(N) (нужен пред.) | O(N) | next на узел |
| Стек | — | O(1) (вершина) | — | — | — | next на узел |
| Очередь | — | O(1) (из начала) | O(1) (в конец) | — | — | next на узел |
| Дек | — | O(1) | O(1) | — | — | prev + next |
Пояснения к таблице:
- Массив выигрывает по доступу к элементу по номеру и по памяти (данные лежат непрерывно), но вставка и удаление в начало или середину требуют сдвига всех следующих элементов.
- Односвязный список даёт O(1) вставку/удаление в начало; вставка в конец — O(1), если
хранить
tail, а удаление хвоста всё равно требует предыдущего узла (O(N)). В середину — O(N), потому что до узла нужно дойти от головы. - Двусвязный список даёт O(1) вставку/удаление в любом месте, если узел уже известен
(есть
prev). Поиск самого узла по значению всё равно O(N). - Кольцевой список — тот же односвязный, но без
NULL-терминатора: удобен, когда нужно «ходить по кругу» (сдвиг, задача Иосифа, циклический обход). Вставка в конец сtail— O(1); для удаления хвоста всё равно нужен предыдущий узел, поэтому без него это O(N). - Стек, очередь, дек — это не отдельные структуры хранения, а ограниченные интерфейсы поверх списка или массива: стек — операции только с одного конца, очередь — в один конец и из другого, дек — с обоих концов.
Как выбирать структуру под задачу
Общее правило выбора:
- Нужен доступ по номеру (
a[i], бинарный поиск, сортировка «на месте») — массив. - Размер заранее неизвестен и данные часто меняются (вставки/удаления) — список.
- Много вставок/удалений в начало — односвязный список с
head. - Нужны операции с обоих концов — дек (двусвязный список) или кольцевой буфер.
- Нужен циклический обход или «круг» — кольцевой список.
- Нужно ходить в обе стороны от узла — двусвязный список.
- Важен только порядок «последний пришёл — первый ушёл» — стек; «первый пришёл — первый ушёл» — очередь.
Выбор под варианты ЛР 3
В лабораторной №3 структура уже задана вариантом; вопрос в том, какие операции вам придётся реализовать и на что обратить внимание.
Основные варианты (1–20) — односвязный список Student. Здесь нужны: стабильная
перестановка (разделение на группы без изменения порядка внутри группы), удаление по
условию, слияние двух списков, сдвиг, разворот, работа с половинами.
- Стабильная перестановка (варианты 1–3, 5, 8, 14) — строите два (или три) списка-группы,
цепляя узлы к их хвостам, затем сшиваете. Односвязный список с
tailздесь идеален: вставка в конец группы за O(1). - Удаление (варианты 4, 6, 7, 20) — нужен указатель на предыдущий узел, чтобы перецепить
его
nextчерез удаляемый; удаляйте узел только после перецепки. - Сдвиг на K (вариант 11) — на односвязном списке найдите узел
Kс конца (илиN-Kс начала), разорвите список и сшейте в новом порядке; не потеряйтеheadи новый хвост. - Разворот и половины (варианты 9, 10, 12, 13, 15) — разворот за один проход с тремя
указателями
prev/cur/next; для половин найдите середину быстрым/медленным указателем. - Слияние (варианты 17–19) — аккуратно сшивайте
tailпервого со вторым; при дедупликации сравнивайтеid.
Повышенные варианты (П1–П5) — двусвязный список Student. Здесь добавляется работа с
prev. Разворот (П1) — это обмен prev и next у каждого узла. Разделение по порогу (П4)
и слияние (П5) требуют обновлять оба направления при каждой перецепке, иначе обратные
связи порвутся.
Высокие варианты (В1–В5) — кольцевой список Value. Всё вертится вокруг инварианта
tail->next == head. Сдвиг (В1) — перенос головы; обмен половин (В2) — разрезание кольца
и слияние частей в обратном порядке; разделение по типу (В3) — разрезание на два кольца;
слияние (В4) — соединение хвостов; разворот (В5) — изменение направления с замыканием.
Подробно см. статью «Циклический список».
Чек-лист отладки указателей
Работа со списками ломается почти всегда из-за указателей. Вот порядок действий при отладке.
- Нарисуйте схему на бумаге. Узлы — квадратики, ссылки — стрелки. Перед каждой перецепкой рисуйте «до» и «после»: это ловит потерю узла до того, как вы её закодируете.
- Проверяйте инварианты. Для очереди/дека —
head == NULL <=> tail == NULL; для дека — ещёhead->prev == NULL,tail->next == NULLи взаимныеprev/next; для кольца —tail->next == head. После каждой операции структура обязана снова удовлетворять инварианту. - Отдельно проверьте N = 0, 1, 2. Пустой список (ничего не делаем), один узел
(
head == tail,next == NULLилиnext == head), два узла (соседние ссылки). Больше всего ошибок — именно на единице и двойке. - Проверьте границы K и T.
K = 0,K = N,K = 1, а такжеT, при котором порог совпадает с реальным значением (запись ровно сT— где она должна остаться?). - Проверьте дубликаты
idи пустые блоки. Слияние с совпадающимиid, разделение, где одна из групп пуста, — типичные места «съедания» узлов. - Компилируйте строго и гоняйте под санитайзерами:
gcc -std=c23 -Wall -Wextra -Werror -fsanitize=address,undefined main.c -o main
AddressSanitizer ловит чтение после free и утечки, UndefinedBehaviorSanitizer —
разыменование NULL и чтение не того члена union. Под Linux также работает valgrind:
valgrind --leak-check=full ./main
- После
freeне трогайте узел. Сначала сохраните все нужные указатели (next,prev), потом освобождайте.
План тестирования
Прогоните программу на этих случаях, сверяя output.txt с ожидаемым:
| Тест | Что проверяет |
|---|---|
| Пустой список (N = 0) | не падаем, ничего не выводим или пустой блок |
| Один элемент (N = 1) | head == tail, корректный вывод, нет «лишних» ссылок |
| Два элемента (N = 2) | обмен, сдвиг, разворот пары — соседние ссылки не рвутся |
K = 0 и K = N для сдвига; K = N для удаления | сдвиг без изменения порядка; удаление только последнего узла |
| K = 1 | граничный шаг, соседние узлы |
| T на границе значения | элемент ровно с порогом остаётся/уходит по условию |
Дубликаты id | слияние с совпадениями, дедупликация |
| Второй список пуст | слияние/чередование без обращения к NULL |
| Кольцо из одного узла | head->next == head, удаление единственного узла |
Много одинаковых score | стабильность перестановки (порядок внутри группы) |
Проверяйте не только сам вывод, но и отсутствие утечек: запустите под ASan/valgrind и убедитесь, что все узлы освобождены.
Что запомнить
- Массив — быстрый доступ по номеру, дорогая вставка/удаление; список — наоборот.
- Односвязный список дешевле по памяти, но ходит только вперёд; двусвязный — в обе
стороны ценой указателя
prev. - Кольцевой список — односвязный без
NULL-терминатора, удобен для «круговых» задач. - Стек, очередь и дек — ограниченные интерфейсы поверх списка или массива.
- Храните
tail, если нужна O(1) вставка в конец. - Перед перецепкой рисуйте схему; проверяйте инварианты и случаи N = 0/1/2.
free— только после перецепки и сохранения нужных указателей.- Гоняйте код под
-fsanitize=address,undefinedи valgrind.