Открыть сервис

Односвязный список

Односвязный список — это структура данных, представляющая собой последовательность элементов (узлов), каждый из которых содержит данные и указатель (ссылку) на следующий узел в списке. Последний узел списка указывает на пустое значение (null или nil), что обозначает конец последовательности. Односвязный список является одной из фундаментальных динамических структур данных, в отличие от массива, его размер может изменяться во время выполнения программы, а элементы не обязательно расположены в памяти последовательно.

История и происхождение

Концепция связных списков была разработана в середине XX века, в период становления компьютерных наук и разработки языков программирования высокого уровня. Первое известное описание связных списков принадлежит Аллену Ньюэллу, Клиффорду Шоу и Герберту Саймону, которые в 1955–1956 годах создали язык обработки списков IPL (Information Processing Language). В IPL списки использовались для представления символьных выражений и стали основой для последующих разработок в области искусственного интеллекта.

В 1958 году Джон Маккарти, один из создателей языка Lisp, формализовал и популяризировал связные списки. В Lisp списки стали основной структурой данных, а операции с ними — ядром языка. Односвязные списки в Lisp реализованы через пары (cons cells), где каждый элемент содержит указатель на следующий. Со временем односвязные списки вошли в стандартные библиотеки многих языков программирования (C++, Java, Python, Go, Rust) и стали обязательной темой в курсах алгоритмов и структур данных.

Устройство и основные характеристики

Узел списка

Каждый узел односвязного списка состоит из двух полей:

В некоторых реализациях узел может быть представлен как класс или структура. Например, на языке C++ узел может выглядеть так: ``cpp struct Node { int data; Node* next; }; ``

Головной узел

Доступ к списку осуществляется через указатель на первый узел, который называется головой (head). Если список пуст, голова равна null. Для удобства иногда используют фиктивный (дummy) головной узел, который не содержит данных и упрощает операции вставки и удаления в начале списка.

Основные операции

Операции над односвязным списком обычно имеют следующие временные сложности:

ОперацияСредняя сложностьОписание
Доступ по индексуO(n)Требуется последовательный обход от головы до нужного узла.
Вставка в началоO(1)Новый узел становится головой, его next указывает на старую голову.
Вставка после заданного узлаO(1)При наличии указателя на узел, после которого вставляется новый элемент.
Вставка в конецO(n) (без хвостового указателя)Требуется дойти до последнего узла. При наличии указателя на хвост (tail) — O(1).
Удаление из началаO(1)Голова переназначается на следующий узел.
Удаление после заданного узлаO(1)При наличии указателя на предыдущий узел.
Поиск элементаO(n)Последовательный обход до совпадения значения.
Очистка спискаO(n)Последовательное удаление всех узлов.

Преимущества и недостатки

Преимущества:

Недостатки:

Разновидности и модификации

Односвязный список с хвостовым указателем

В этой модификации помимо головы хранится указатель на последний узел (tail). Это позволяет выполнять вставку в конец за O(1). Однако удаление последнего узла по-прежнему требует O(n), так как нужно найти предпоследний узел.

Циклический односвязный список

В циклическом варианте последний узел указывает не на null, а на голову. Такой список удобен для задач с циклическим перебором (например, очередь или кольцевой буфер). В нём нет явного конца, и обход может продолжаться бесконечно, если не задать условие остановки.

Список с фиктивным головным узлом

Используется для упрощения краевых случаев. Фиктивный узел (sentinel) не содержит данных, но его next указывает на настоящий первый элемент. Это позволяет избежать проверок на пустой список при вставке или удалении в начале.

Применение

Реализация стеков и очередей

Односвязный список лежит в основе многих реализаций стека (LIFO) и очереди (FIFO). Для стека достаточно вставки и удаления в начале (O(1)). Для очереди используют вставку в конец (при наличии хвостового указателя) и удаление из начала.

Хранение разреженных данных

В алгоритмах, где данные добавляются и удаляются часто, а размер заранее неизвестен, односвязные списки предпочтительнее массивов. Например, в реализации хеш-таблиц с цепочками коллизий каждый элемент корзины может быть односвязным списком.

Представление графов (списки смежности)

В графах для хранения рёбер каждой вершины часто используют односвязные списки. Каждый узел списка содержит номер смежной вершины и, возможно, вес ребра. Это позволяет эффективно перебирать соседей.

Управление памятью (аллокаторы)

Некоторые аллокаторы памяти (например, в ранних реализациях malloc) использовали односвязные списки свободных блоков для отслеживания доступных участков памяти.

Обработка событий и буферизация

В системах реального времени и ввода-вывода односвязные списки применяются для организации очередей событий или буферов данных, где элементы добавляются и удаляются динамически.

Пример реализации (псевдокод)

Ниже приведён простой пример реализации односвязного списка на псевдокоде с операциями вставки в начало, удаления из начала и вывода списка.

``` class Node: data next = null

class SinglyLinkedList: head = null

method insertAtBeginning(value): newNode = Node(value) newNode.next = head head = newNode

method deleteFromBeginning(): if head is null: return null removedData = head.data head = head.next return removedData

method display(): current = head while current is not null: print(current.data) current = current.next ```

Критика и альтернативы

Несмотря на широкое применение, односвязные списки имеют ограничения. Для задач, требующих частого произвольного доступа, массивы или динамические массивы (например, ArrayList в Java, vector в C++) оказываются эффективнее. Вставка и удаление в середине списка также требуют O(n) для поиска позиции, что делает их неоптимальными для таких операций.

В современных языках программирования часто предпочитают двусвязные списки (если нужен обратный обход) или кольцевые буферы (для фиксированного размера). В функциональных языках (Haskell, Clojure) односвязные списки являются основной неизменяемой структурой, но их модификации требуют копирования.

В системах с высокими требованиями к производительности (игровые движки, высоконагруженные серверы) от связных списков часто отказываются в пользу массивов или специализированных структур (например, intrusive linked lists в ядре Linux), где указатели на следующий и предыдущий узлы хранятся непосредственно в самих данных, что уменьшает накладные расходы.

Интересные факты

Источники

BFOmetr — база данных и аналитика по компаниям России.

На главную BFOmetr →