0Pricing
Coding Interview Prep · Урок

Класс узла и построение списка

Определите dataclass Node, вручную создавайте списки, связывая узлы, и напишите вспомогательные функции insert/delete/print для визуализации изменений указателей

«Класс узла и построение списка» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.

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

Связный список — это последовательность узлов, где каждый узел хранит значение и указатель на следующий узел. В отличие от массивов, узлы разбросаны в памяти — доступа по индексу за O(1) нет. Взамен Вы получаете вставку и удаление за O(1) в любой известной позиции без сдвига элементов.

В Python каждый узел представляется небольшим классом, содержащим val и next. Соединение узлов образует список; у последнего узла next равен None, что обозначает конец списка.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# Build: 1 -> 2 -> 3 -> None
head = ListNode(1)
head.next = ListNode(2)
head.next.next = ListNode(3)

# Traverse and print
curr = head
while curr:
    print(curr.val, end=' -> ')
    curr = curr.next
print('None')

Построение списков из массивов

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

Построение связного списка из n элементов занимает время O(n) и пространство O(n). Использование фиктивного головного узла упрощает пограничные случаи, когда первый узел может измениться.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def build(arr):
    dummy = ListNode(0)
    curr = dummy
    for val in arr:
        curr.next = ListNode(val)
        curr = curr.next
    return dummy.next

def to_list(head):
    result = []
    while head:
        result.append(head.val)
        head = head.next
    return result

head = build([1, 2, 3, 4, 5])
print(to_list(head))  # [1, 2, 3, 4, 5]

Вставка в начало и конец

Вставка нового узла в начало выполняется за O(1): создайте узел, укажите в его next старую голову списка и верните новый узел как голову. Вставка в конец (tail) требует пройти до последнего узла (O(n)), а затем связать его с новым узлом.

Использование фиктивного головного узла устраняет особый случай пустого списка при обеих вставках, поскольку dummy.next всегда указывает на настоящую голову списка.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def insert_head(head, val):
    return ListNode(val, head)  # O(1)

def insert_tail(head, val):
    new_node = ListNode(val)
    if not head:
        return new_node
    curr = head
    while curr.next:
        curr = curr.next
    curr.next = new_node
    return head

head = None
for v in [1, 2, 3]:
    head = insert_tail(head, v)
head = insert_head(head, 0)

curr = head
while curr:
    print(curr.val, end=' -> ')
    curr = curr.next
print('None')  # 0 -> 1 -> 2 -> 3 -> None

Удаление узла по значению

Чтобы удалить первый узел с заданным значением, поддерживайте указатель prev на один шаг позади curr. Когда curr.val == target, установите prev.next = curr.next, чтобы обойти этот узел. Фиктивный головной узел особенно полезен здесь, поскольку устраняет особый случай удаления самой головы списка: prev всегда может начинаться с фиктивного узла.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def delete_val(head, target):
    dummy = ListNode(0)
    dummy.next = head
    prev, curr = dummy, head
    while curr:
        if curr.val == target:
            prev.next = curr.next
            break
        prev, curr = curr, curr.next
    return dummy.next

def to_list(h):
    r = []
    while h:
        r.append(h.val)
        h = h.next
    return r

head = None
for v in [1, 2, 3, 2, 4]:
    dummy2 = ListNode(v)
    dummy2.next = head
    head = dummy2  # build in reverse for speed
head = delete_val(head, 2)
print(to_list(head))

Визуализация изменений указателей

Распространённая ошибка — потерять узел при обновлении указателей. Всегда сохраняйте next перед его перезаписью: saved = curr.next, а затем переназначайте указатель. Изобразите список в виде блоков, соединённых стрелками, и перед написанием кода на бумаге выполните каждое обновление указателя. Такой визуальный подход предотвращает случайные ошибки обращения через нулевой указатель на собеседованиях.

Помните: в Python переназначение curr.next не влияет на сам curr, но если перед сохранением потерять ссылку на curr.next, продолжить обход вперёд уже невозможно.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# Demonstrate safe pointer update
def swap_first_two(head):
    if not head or not head.next:
        return head
    first  = head
    second = head.next
    # Save third before losing the reference
    third  = second.next
    # Rewire
    second.next = first
    first.next  = third
    return second

from functools import reduce
nodes = [ListNode(i) for i in range(1, 5)]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]
head = swap_first_two(nodes[0])
curr = head
while curr:
    print(curr.val, end=' ')
    curr = curr.next
# 2 1 3 4

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

Односвязный список хранит только указатель next; обход выполняется в одном направлении. Двусвязный список хранит и prev, и next, обеспечивая обход назад за O(1) и удаление за O(1) при наличии прямой ссылки на узел (цикл с отслеживанием предыдущего узла не нужен).

collections.deque в Python реализован как двусвязный список, поэтому поддерживает appendleft и popleft за O(1). На собеседованиях Вы будете реализовывать односвязные списки; двусвязные списки встречаются при проектировании кэша LRU.

class DLNode:
    def __init__(self, val=0):
        self.val  = val
        self.prev = None
        self.next = None

# Build doubly linked: 1 <-> 2 <-> 3
a, b, c = DLNode(1), DLNode(2), DLNode(3)
a.next = b; b.prev = a
b.next = c; c.prev = b

# Traverse forward
curr = a
while curr:
    print(curr.val, end=' <-> ')
    curr = curr.next
print('None')

# Traverse backward from c
curr = c
while curr:
    print(curr.val, end=' <-> ')
    curr = curr.prev
print('None')

Длина, хвост и вспомогательные функции вывода

Три вспомогательные функции, которые стоит иметь под рукой на любом собеседовании по связным спискам: length(head) подсчитывает узлы за O(n), tail(head) возвращает последний узел за O(n), а print_list(head) форматирует список для отладки. Если они готовы заранее, Вы сможете сосредоточиться на основном алгоритме, а не заново реализовывать вспомогательную логику.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def length(head):
    count = 0
    while head:
        count += 1
        head = head.next
    return count

def tail(head):
    while head and head.next:
        head = head.next
    return head

def print_list(head):
    parts = []
    while head:
        parts.append(str(head.val))
        head = head.next
    print(' -> '.join(parts) + ' -> None')

# Build and test
nodes = [ListNode(i) for i in [10, 20, 30, 40]]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]
head = nodes[0]
print('Length:', length(head))
print('Tail:', tail(head).val)
print_list(head)

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

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

Всегда явно инициализируйте оба указателя и внимательно обрабатывайте проверку завершения списка — fast and fast.next предотвращает ошибки обращения через нулевой указатель, когда быстрый указатель находится рядом с концом.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# Find middle node using slow-fast pointers
def find_middle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
    return slow   # for even length, returns second of two middle nodes

nodes = [ListNode(i) for i in range(1, 6)]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]

print(find_middle(nodes[0]).val)  # 3 (middle of 1->2->3->4->5)

Шаблон фиктивной головы

Шаблон фиктивной головы (узла-сторожа) — один из самых полезных приёмов в задачах со связными списками. Добавив в начало фиктивный узел со значением 0, Вам не придётся отдельно обрабатывать пустой список или изменение настоящей головы. Результат всегда находится в dummy.next. Этот шаблон встречается при слиянии отсортированных списков, удалении узла с конца по заданному номеру, разделении списка и во многих других задачах.

class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

# Remove all nodes with val == target (may include head)
def remove_all(head, target):
    dummy = ListNode(0)
    dummy.next = head
    curr = dummy
    while curr.next:
        if curr.next.val == target:
            curr.next = curr.next.next  # skip the node
        else:
            curr = curr.next
    return dummy.next

def to_list(h):
    r = []
    while h:
        r.append(h.val)
        h = h.next
    return r

nodes = [ListNode(v) for v in [1, 2, 6, 3, 4, 5, 6]]
for i in range(len(nodes) - 1):
    nodes[i].next = nodes[i+1]
head = remove_all(nodes[0], 6)
print(to_list(head))  # [1, 2, 3, 4, 5]

Временная и пространственная сложность

Большинство операций со связными списками имеют следующие оценки сложности. Доступ по индексу: O(n) — необходимо пройти от головы списка. Вставка или удаление в известном узле: O(1) — достаточно перенастроить указатели. Вставка или удаление в позиции k: O(k) — сначала нужно выполнить обход. Поиск: O(n) — в худшем случае просматривается весь список. Для всех операций на месте пространственная сложность равна O(1), если не учитывать дополнительные структуры данных.

Сравните это с массивами: массивы обеспечивают доступ за O(1), но вставка и удаление занимают O(n) из-за сдвига элементов. Связные списки лучше подходят, когда часто выполняются вставки и удаления в произвольных позициях.

Советы для собеседования по связным спискам

Перед написанием кода для связного списка изобразите список наглядно, используя блоки и стрелки. Вслух проверьте пограничные случаи: пустой список, один узел, чётная и нечётная длина. Используйте фиктивный головной узел для упрощения граничных условий. Всегда рано проверяйте if not head. После написания кода выполните трассировку решения на списке из трёх узлов, чтобы обнаружить ошибки в указателях раньше интервьюера.

Большинство ошибок в связных списках возникает по одной из трёх причин: забыли сохранить next перед его перезаписью, допустили ошибку на единицу в условии завершения или не обработали случай изменения головы списка — фиктивный узел полностью устраняет третью причину.

Быстрая проверка

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

Итоги урока

В этом уроке Вы узнали: связный список строится из объектов узлов с полями для значения и следующего узла, шаблон фиктивной головы устраняет пограничные случаи, связанные с изменением головы, а схема с медленным и быстрым указателями — основа поиска середины и обнаружения циклов. Далее мы рассмотрим разворот связного списка — одну из наиболее часто встречающихся задач на работу с указателями.

Часто задаваемые вопросы

Урок «Класс узла и построение списка» бесплатный?

Да — полный текст урока «Класс узла и построение списка» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Класс узла и построение списка»?

Определите dataclass Node, вручную создавайте списки, связывая узлы, и напишите вспомогательные функции insert/delete/print для визуализации изменений указателей Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Coding Interview Prep?

Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Класс узла и построение списка»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Coding Interview Prep?

Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Класс узла и построение списка
  2. Разворот связного списка
  3. Обнаружение циклов алгоритмом Флойда
  4. Слияние, разделение и поиск с конца
← Назад к Coding Interview Prep