Класс узла и построение списка
Определите dataclass Node, вручную создавайте списки, связывая узлы, и напишите вспомогательные функции insert/delete/print для визуализации изменений указателей
«Класс узла и построение списка» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA 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) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Класс узла и построение списка»?
Определите dataclass Node, вручную создавайте списки, связывая узлы, и напишите вспомогательные функции insert/delete/print для визуализации изменений указателей Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DSA Interview Prep?
Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «Класс узла и построение списка»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DSA Interview Prep?
Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Класс узла и построение списка
- Разворот связного списка
- Обнаружение циклов алгоритмом Флойда
- Слияние, разделение и поиск с конца