Learning Platform
Глоссарий Troubleshooting
Урок 07.01 · 25 мин
Начальный
linked-listsingly-linkednodeheadpointer

Что такое связный список

Динамический массив (list, ArrayList, vector) — это один большой блок памяти. Связный список — это много маленьких узлов, каждый знает только своего соседа. Узлы могут лежать в памяти где угодно: указатель связывает их в цепочку.

Базовая структура узла singly linked list:

class Node:
    __slots__ = ('value', 'next')

    def __init__(self, value, next_node=None):
        self.value = value
        self.next = next_node
  • value — данные узла (что угодно).
  • next — указатель на следующий узел, или None если это конец.

Сам список держит только указатель на head — первый узел. Дальше — по next.

Singly linked list: head -> N1 -> N2 -> N3 -> None

Каждый узел знает только своего следующего. Цепочка обрывается на None.

head*Nodeуказатель списка на первый узел
N1value=10, next=*N2первый узел, value=10, next ведёт ко второму
N2value=20, next=*N3второй узел
N3value=30, next=Noneпоследний узел; next равен None — это и есть конец списка

Реализация на Python class

Контейнер LinkedList:

class LinkedList:
    __slots__ = ('head',)

    def __init__(self):
        self.head = None

    def push_front(self, value):
        """Вставка в начало — O(1)."""
        self.head = Node(value, next_node=self.head)

    def pop_front(self):
        """Удаление с начала — O(1)."""
        if self.head is None:
            raise IndexError("pop from empty list")
        node = self.head
        self.head = node.next
        return node.value

    def find(self, value):
        """Поиск по значению — O(n)."""
        node = self.head
        while node is not None:
            if node.value == value:
                return node
            node = node.next
        return None

    def __iter__(self):
        node = self.head
        while node is not None:
            yield node.value
            node = node.next

    def __len__(self):
        # O(n) если не кэшировать
        count = 0
        node = self.head
        while node is not None:
            count += 1
            node = node.next
        return count

Использование:

ll = LinkedList()
ll.push_front(30)
ll.push_front(20)
ll.push_front(10)

print(list(ll))   # [10, 20, 30]
print(ll.pop_front())  # 10
print(list(ll))   # [20, 30]

Сложности операций

Главное про singly linked list — в чём он быстрее и в чём медленнее массива:

Сложности singly linked vs dynamic array

Связный список лучше в начале, массив — в произвольной точке. Поиск везде линейный.

push_frontlinked O(1), list.insert(0) O(n)у linked — просто создать узел и переставить head; у list — сдвинуть все элементы
pop_frontlinked O(1), list.pop(0) O(n)у linked — изменить head; у list — сдвинуть все
push_backlinked O(n) без tail, list.append O(1)без указателя на tail singly linked обходит весь список до конца
доступ по индексу xs[i]linked O(n), list O(1)нет арифметики адресов — надо пройти i узлов
search by valuelinked O(n), list O(n)одинаковый линейный поиск
insert после данного узлаlinked O(1), list O(n)если у тебя есть указатель на узел — у linked это перестановка указателей; у list — сдвиг

Ключевая черта: linked list даёт O(1) на insert/delete, если у вас уже есть указатель на узел. Это полезно для LRU cache, ring buffer с замещением, парсеров. Платим за это потерей O(1) доступа по индексу и плохой cache locality (об этом — в уроке 03 этого модуля).

Зачем нужен указатель на tail

В нашей реализации push_back — это O(n), потому что без указателя на последний узел приходится пройти весь список. Можно добавить tail:

class LinkedListWithTail:
    __slots__ = ('head', 'tail', 'size')

    def __init__(self):
        self.head = None
        self.tail = None
        self.size = 0

    def push_back(self, value):
        """O(1) с указателем на tail."""
        node = Node(value)
        if self.tail is None:
            self.head = self.tail = node
        else:
            self.tail.next = node
            self.tail = node
        self.size += 1

    def push_front(self, value):
        node = Node(value, next_node=self.head)
        if self.head is None:
            self.tail = node
        self.head = node
        self.size += 1

Теперь push_back тоже O(1). Но: pop_back всё равно O(n) для singly linked, потому что после удаления tail нужно переставить указатель на предпоследний узел — а до него надо пройти весь список. Чтобы и pop_back был O(1), нужны указатели в обе стороны — doubly linked list (следующий урок).

Почему «singly» иногда хватает

В стандартной библиотеке Python нет встроенного linked list. Обычно его пишут сами для конкретной задачи. Singly linked хватает для:

  • Стек (push_front/pop_front как push/pop) — но в Python проще list.append/pop, который тоже O(1) сзади.
  • Persistent list в функциональном программировании (Haskell, Erlang). Каждое push_front даёт новый список, разделяющий хвост со старым. Это работает только для singly linked.
  • Hash table chaining — внутри bucket’а хранится линейная цепочка коллизий. Это singly linked, разберём в модуле 8.
  • Linked stack для парсера или DFS — когда удобно иметь immutable “prev state” указатель.

Для большинства практических задач, где нужен FIFO-доступ с обоих концов, лучше doubly linked или deque.

Память singly linked vs list

В Python каждый Node — отдельный PyObject. Размер:

import sys

class NodeSlots:
    __slots__ = ('value', 'next')
    def __init__(self, v, n):
        self.value = v
        self.next = n

n = NodeSlots(42, None)
print(f"Node (slots):     {sys.getsizeof(n)} bytes + 16 для __slots__ -> ~64 total")

Один узел в Python — 48-80 байт (с PyObject header, ob_refcnt, ob_type, ob_dict если не slots, и поля). Сравните с 8 байтами на указатель в Python list.

Linked list на 1M int = ~64 байт/узел * 1M = ~64 МБ. list на 1M int = ~36 МБ (буфер указателей + PyLongObject).

То есть singly linked в Python примерно вдвое дороже обычного list при том же содержимом. Плюс никакого spatial locality (об этом — урок 03).

WARNING

Linked list в Python — это обучающая конструкция. На практике почти всегда быстрее (и по памяти, и по CPU) использовать list или collections.deque. Linked list оправдан в C/Rust/Go, где Node — компактный struct в стеке/арене, а не PyObject в куче.

Попробуй сам

Реализуйте reverse для singly linked — переверните список in-place за один проход:

class Node:
    __slots__ = ('value', 'next')
    def __init__(self, value, next_node=None):
        self.value = value
        self.next = next_node

def build(values):
    head = None
    for v in reversed(values):
        head = Node(v, head)
    return head

def to_list(head):
    out = []
    while head is not None:
        out.append(head.value)
        head = head.next
    return out

def reverse(head):
    """In-place reverse за один проход, O(n) время, O(1) дополнительной памяти."""
    prev = None
    curr = head
    while curr is not None:
        next_node = curr.next  # сохраняем хвост
        curr.next = prev        # переворачиваем стрелку
        prev = curr
        curr = next_node
    return prev

# Проверьте
head = build([1, 2, 3, 4, 5])
print(to_list(head))           # [1, 2, 3, 4, 5]
new_head = reverse(head)
print(to_list(new_head))       # [5, 4, 3, 2, 1]

Классическое упражнение. Понимание трёх указателей prev, curr, next_node — фундамент работы со связными списками.

Stack vs heap: где живут узлы linked list
Проверка знанийKnowledge check
У вас singly linked list с указателями на head и tail. Объясни, почему push_back работает за O(1), а pop_back — за O(n).
ОтветAnswer
push_back: создаём узел, ставим tail.next = новый узел, потом tail = новый узел. Все операции — константы. pop_back: чтобы убрать последний узел, нужно сделать предыдущий узел новым tail, то есть установить его next = None. Но у нас нет указателя на предыдущий узел — singly linked знает только 'вперёд'. Чтобы найти предыдущий, нужно пройти от head до тех пор, пока node.next == tail. Это O(n) операций обхода. Если хочется O(1) с обоих концов — нужен doubly linked (каждый узел знает и prev, и next) — это следующий урок.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. Что хранит каждый узел singly linked list?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5