В приведённой функции для связного списка на Python обнаружьте и исправьте ошибки, оцените асимптотику и предложите тесты (код):
def remove_cycle(head):
slow = head
fast = head.next
while slow != fast:
slow = slow.next
fast = fast.next.next
while head != fast:
head = head.next
fast = fast.next
while fast.next != head:
fast = fast.next
fast.next = None
Объясните, почему исходный код может упасть или неправильно работать, как его корректно реализовать и как проверить на краевых случаях

29 Сен в 10:22
14 +1
0
Ответы
1

Кратко — в исходном коде несколько проблем:

нет проверок на пустой список или на конец списка (head == None или head.next == None) — возможен AttributeError;инициализация fast = head.next и цикл while slow != fast без проверки fast/fast.next приведут к ошибке или бесконечному циклу, когда цикла в списке нет;используется сравнение != вместо сравнения по идентичности узлов (лучше is / is not), это может конфликтовать с переопределённым eq;код меняет переменную head при поиске начала цикла (хотя это не ошибка как таковая, лучше для ясности использовать отдельный указатель).

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

Ниже — корректная реализация и тесты.

Код (Python):

class Node:
def init(self, val=0, next=None):
self.val = val
self.next = next
def repr(self):
return f"Node({self.val})"

def remove_cycle(head):
"""
Удаляет цикл в связном списке, если он есть.
Мутирует список in-place. Возвращает None.
"""
if head is None:
return

1) Детекция цикла (Floyd), безопасно проверяем fast и fast.nextslow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
break
else:
# цикл не найден
return
# 2) Находим начало цикла
ptr1 = head
ptr2 = slow
while ptr1 is not ptr2:
ptr1 = ptr1.next
ptr2 = ptr2.next
cycle_start = ptr1 # начало цикла
# 3) Находим последний узел цикла и обрываем ссылку
prev = cycle_start
while prev.next is not cycle_start:
prev = prev.next
prev.next = None
returnВспомогательные функции для тестов

def make_list(values):
"""Создаёт линейный список из значений и возвращает head."""
if not values:
return None
head = Node(values[0])
cur = head
for v in values[1:]:
cur.next = Node(v)
cur = cur.next
return head

def make_cycle(head, pos):
"""
Соединяет хвост списка с узлом на позиции pos (0-based).
pos == -1 означает: не создавать цикл.
Возвращает head.
"""
if head is None or pos == -1:
return head
tail = head
nodes = [tail]
while tail.next:
tail = tail.next
nodes.append(tail)
if pos < 0 or pos >= len(nodes):
raise IndexError("pos out of range")
tail.next = nodes[pos]
return head

def has_cycle(head):
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False

def to_pylist(head, max_nodes=1000):
"""
Преобразует список в Python list значений. max_nodes предотвращает бесконечный цикл.
"""
res = []
cur = head
cnt = 0
while cur is not None and cnt < max_nodes:
res.append(cur.val)
cur = cur.next
cnt += 1
if cnt >= max_nodes:
raise RuntimeError("Potential infinite list (cycle?)")
return res

Тесты

def test_remove_cycle():

1) Пустой списокhead = None
remove_cycle(head)
assert head is None
# 2) Одиночный узел без цикла
head = Node(1)
remove_cycle(head)
assert not has_cycle(head)
assert to_pylist(head) == [1]
# 3) Одиночный узел сам на себя (цикл длины 1)
head = Node(1)
head.next = head
assert has_cycle(head)
remove_cycle(head)
assert not has_cycle(head)
assert to_pylist(head) == [1]
# 4) Несколько узлов без цикла
head = make_list([1,2,3,4,5])
remove_cycle(head)
assert not has_cycle(head)
assert to_pylist(head) == [1,2,3,4,5]
# 5) Цикл, соединённый в середине (tail -> node at pos)
head = make_list([1,2,3,4,5,6])
make_cycle(head, 2) # tail -> Node(3)
assert has_cycle(head)
remove_cycle(head)
assert not has_cycle(head)
assert to_pylist(head) == [1,2,3,4,5,6]
# 6) Цикл на голову (tail -> head)
head = make_list([10,20,30])
make_cycle(head, 0)
assert has_cycle(head)
remove_cycle(head)
assert not has_cycle(head)
assert to_pylist(head) == [10,20,30]
# 7) Двух-узловой цикл
a = Node(1); b = Node(2)
a.next = b; b.next = a
assert has_cycle(a)
remove_cycle(a)
assert not has_cycle(a)
assert to_pylist(a) == [1,2]
print("Все тесты пройдены")

if name == "main":
test_remove_cycle()

Пояснения и замечания:

Почему исходный код падает/работает неправильно:
fast = head.next без проверки может быть None и далее обращение fast.next вызовет исключение;если в списке нет цикла, while slow != fast(с инициализацией fast=head.next) никогда корректно не закончится — либо будет исключение, либо бесконечный цикл;использование != может вести себя непредсказуемо при переопределённом eq у узлов — безопаснее сравнивать по идентичности (is).Асимптотика:
Детекция цикла: O(n)Поиск начала цикла: O(n)Поиск предыдущего узла в цикле: O(k) ≤ O(n)Итог: O(n) по времени, O(1) по дополнительной памяти.Тесты: включают пустой список, одиночный узел (с циклом и без), циклы разных типов (в середине, на голове, длина 2 и т.д.). В тестах используется безопасная конвертация списка в Python-список с ограничением на максимальное число узлов, чтобы избежать зависания при ошибке удаления цикла.

Если хотите, могу:

вернуть функцию, которая помимо разрыва цикла будет возвращать узел, с которого начинался цикл;добавить unittest/TestCase-оформление для автоматической проверки;оптимизировать/уточнить поведение при специальных структурах Node (например, если у Node переопределён eq).
29 Сен в 10:47
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир