Fast & Slow Pointers (Черепаха и Заяц) — два указателя двигаются с разной скоростью: slow на 1 шаг, fast на 2. Это позволяет решать задачи на циклы, середину и структуру списка.
slow: o . o . o . o . o
fast: o . . o . . o . . o
Если есть цикл — fast догонит slow.
Если нет цикла — fast дойдёт до конца.
Задача 1: Обнаружение цикла (Floyd's Algorithm)
function hasCycle(?ListNode $head): bool
{
$slow = $head;
$fast = $head;
while ($fast !== null && $fast->next !== null) {
$slow = $slow->next; // 1 step
$fast = $fast->next->next; // 2 steps
if ($slow === $fast) {
return true;
}
}
return false; // fast reached the end — no cycle
}
func hasCycle(head *ListNode) bool {
slow := head
fast := head
for fast != nil && fast.Next != nil {
slow = slow.Next // 1 step
fast = fast.Next.Next // 2 steps
if slow == fast {
return true
}
}
return false // fast reached the end — no cycle
}
static bool HasCycle(ListNode? head)
{
var slow = head;
var fast = head;
while (fast?.Next is not null)
{
slow = slow!.Next; // 1 step
fast = fast.Next.Next; // 2 steps
if (ReferenceEquals(slow, fast))
{
return true;
}
}
return false; // fast reached the end — no cycle
}
def has_cycle(head: ListNode | None) -> bool:
slow = head
fast = head
while fast is not None and fast.next is not None:
slow = slow.next # 1 step
fast = fast.next.next # 2 steps
if slow is fast: # identity check, not ==
return True
return False # fast reached the end — no cycle
После обнаружения цикла: перемещаем один указатель на head, оба двигаются по 1 шагу.
function detectCycleStart(?ListNode $head): ?ListNode
{
$slow = $head;
$fast = $head;
// Step 1: Find meeting point
while ($fast !== null && $fast->next !== null) {
$slow = $slow->next;
$fast = $fast->next->next;
if ($slow === $fast) {
// Found cycle, now find start
$slow = $head;
while ($slow !== $fast) {
$slow = $slow->next;
$fast = $fast->next; // Both 1 step!
}
return $slow; // Start of cycle
}
}
return null; // No cycle
}
func detectCycleStart(head *ListNode) *ListNode {
slow := head
fast := head
// Step 1: Find meeting point
for fast != nil && fast.Next != nil {
slow = slow.Next
fast = fast.Next.Next
if slow == fast {
// Found cycle, now find start
slow = head
for slow != fast {
slow = slow.Next
fast = fast.Next // Both 1 step!
}
return slow // Start of cycle
}
}
return nil // No cycle
}
static ListNode? DetectCycleStart(ListNode? head)
{
var slow = head;
var fast = head;
// Step 1: Find meeting point
while (fast?.Next is not null)
{
slow = slow!.Next;
fast = fast.Next.Next;
if (ReferenceEquals(slow, fast))
{
// Found cycle, now find start
slow = head;
while (!ReferenceEquals(slow, fast))
{
slow = slow!.Next;
fast = fast!.Next; // Both 1 step!
}
return slow; // Start of cycle
}
}
return null; // No cycle
}
def detect_cycle_start(head: ListNode | None) -> ListNode | None:
slow = head
fast = head
# Step 1: Find meeting point
while fast is not None and fast.next is not None:
slow = slow.next
fast = fast.next.next
if slow is fast:
# Found cycle, now find start
slow = head
while slow is not fast:
slow = slow.next
fast = fast.next # Both 1 step!
return slow # Start of cycle
return None # No cycle
### Математическое доказательство
head start
| |
v v
o--o--o--o--o--o--o--o
|<-- a -->| | |
|<- c ->|
| |
o--o--o--o
|<- b ->|
a = расстояние от head до начала цикла
b = расстояние от начала цикла до точки встречи
C = длина цикла
При встрече:
slow прошёл: a + b
fast прошёл: a + b + k*C (k полных кругов)
fast = 2 * slow:
a + b + k*C = 2(a + b)
k*C = a + b
a = k*C - b
Значит: если идти a шагов от head и от точки встречи —
оба окажутся в начале цикла!
func findMiddleFirst(head *ListNode) *ListNode {
slow := head
fast := head
for fast.Next != nil && fast.Next.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
return slow
}
// 1->2->3->4
// slow: 1, 2 fast: 1, 3
// Returns 2 (first middle)
static ListNode? FindMiddleFirst(ListNode? head)
{
var slow = head;
var fast = head;
while (fast!.Next?.Next is not null)
{
slow = slow!.Next;
fast = fast.Next.Next;
}
return slow;
}
// 1->2->3->4
// slow: 1, 2 fast: 1, 3
// Returns 2 (first middle)
def find_middle_first(head: ListNode | None) -> ListNode | None:
slow = head
fast = head
while fast.next is not None and fast.next.next is not None:
slow = slow.next
fast = fast.next.next
return slow
# 1->2->3->4
# slow: 1, 2 fast: 1, 3
# Returns 2 (first middle)
func isPalindromeList(head *ListNode) bool {
if head == nil || head.Next == nil {
return true
}
// 1. Find middle
slow := head
fast := head
for fast.Next != nil && fast.Next.Next != nil {
slow = slow.Next
fast = fast.Next.Next
}
// 2. Reverse second half
secondHalf := reverseList(slow.Next)
// 3. Compare
firstHalf := head
for secondHalf != nil {
if firstHalf.Val != secondHalf.Val {
return false
}
firstHalf = firstHalf.Next
secondHalf = secondHalf.Next
}
return true
}
func reverseList(head *ListNode) *ListNode {
var prev *ListNode
for head != nil {
next := head.Next
head.Next = prev
prev = head
head = next
}
return prev
}
static bool IsPalindromeList(ListNode? head)
{
if (head?.Next is null)
{
return true;
}
// 1. Find middle
var slow = head;
var fast = head;
while (fast.Next?.Next is not null)
{
slow = slow.Next!;
fast = fast.Next.Next;
}
// 2. Reverse second half
var secondHalf = ReverseList(slow.Next);
// 3. Compare
var firstHalf = head;
while (secondHalf is not null)
{
if (firstHalf!.Val != secondHalf.Val)
{
return false;
}
firstHalf = firstHalf.Next;
secondHalf = secondHalf.Next;
}
return true;
}
static ListNode? ReverseList(ListNode? head)
{
ListNode? prev = null;
while (head is not null)
{
var next = head.Next;
head.Next = prev;
prev = head;
head = next;
}
return prev;
}
def is_palindrome_list(head: ListNode | None) -> bool:
if head is None or head.next is None:
return True
# 1. Find middle
slow = head
fast = head
while fast.next is not None and fast.next.next is not None:
slow = slow.next
fast = fast.next.next
# 2. Reverse second half
second_half = reverse_list(slow.next)
# 3. Compare
first_half = head
while second_half is not None:
if first_half.val != second_half.val:
return False
first_half = first_half.next
second_half = second_half.next
return True
def reverse_list(head: ListNode | None) -> ListNode | None:
prev: ListNode | None = None
while head is not None:
head.next, prev, head = prev, head, head.next
return prev
function removeNthFromEnd(?ListNode $head, int $n): ?ListNode
{
$dummy = new ListNode(0);
$dummy->next = $head;
$fast = $dummy;
$slow = $dummy;
// Fast is ahead of slow by n+1 steps
for ($i = 0; $i <= $n; $i++) {
$fast = $fast->next;
}
// Move both until fast reaches the end
while ($fast !== null) {
$slow = $slow->next;
$fast = $fast->next;
}
// slow->next is the target node
$slow->next = $slow->next->next;
return $dummy->next;
}
// Remove 2nd from end in [1, 2, 3, 4, 5]
// fast ahead by n+1=3 steps: fast=3
// Move: slow=1,fast=4 -> slow=2,fast=5 -> slow=3,fast=null
// slow->next = 4 (remove)
// Result: [1, 2, 3, 5]
func removeNthFromEnd(head *ListNode, n int) *ListNode {
dummy := &ListNode{Next: head}
fast := dummy
slow := dummy
// Fast is ahead of slow by n+1 steps
for i := 0; i <= n; i++ {
fast = fast.Next
}
// Move both until fast reaches the end
for fast != nil {
slow = slow.Next
fast = fast.Next
}
// slow.Next is the target node
slow.Next = slow.Next.Next
return dummy.Next
}
// Remove 2nd from end in [1, 2, 3, 4, 5]
// fast ahead by n+1=3 steps: fast=3
// Move: slow=1,fast=4 -> slow=2,fast=5 -> slow=3,fast=nil
// slow.Next = 4 (remove)
// Result: [1, 2, 3, 5]
static ListNode? RemoveNthFromEnd(ListNode? head, int n)
{
var dummy = new ListNode(0, head);
var fast = dummy;
var slow = dummy;
// Fast is ahead of slow by n+1 steps
for (var i = 0; i <= n; i++)
{
fast = fast!.Next;
}
// Move both until fast reaches the end
while (fast is not null)
{
slow = slow!.Next;
fast = fast.Next;
}
// slow.Next is the target node
slow!.Next = slow.Next!.Next;
return dummy.Next;
}
// Remove 2nd from end in [1, 2, 3, 4, 5]
// fast ahead by n+1=3 steps: fast=3
// Move: slow=1,fast=4 -> slow=2,fast=5 -> slow=3,fast=null
// slow.Next = 4 (remove)
// Result: [1, 2, 3, 5]
def remove_nth_from_end(head: ListNode | None, n: int) -> ListNode | None:
dummy = ListNode(0, head)
fast: ListNode | None = dummy
slow = dummy
# Fast is ahead of slow by n+1 steps
for _ in range(n + 1):
fast = fast.next
# Move both until fast reaches the end
while fast is not None:
slow = slow.next
fast = fast.next
# slow.next is the target node
slow.next = slow.next.next
return dummy.next
# Remove 2nd from end in [1, 2, 3, 4, 5]
# fast ahead by n+1=3 steps: fast=3
# Move: slow=1,fast=4 -> slow=2,fast=5 -> slow=3,fast=None
# slow.next = 4 (remove)
# Result: [1, 2, 3, 5]
## Задача 6: Пересечение двух списков
function getIntersection(?ListNode $headA, ?ListNode $headB): ?ListNode
{
if ($headA === null || $headB === null) {
return null;
}
$a = $headA;
$b = $headB;
// When a reaches the end — switch to headB
// When b reaches the end — switch to headA
// They will meet at the intersection (or both become null)
while ($a !== $b) {
$a = ($a !== null) ? $a->next : $headB;
$b = ($b !== null) ? $b->next : $headA;
}
return $a; // Intersection point or null
}
func getIntersection(headA, headB *ListNode) *ListNode {
if headA == nil || headB == nil {
return nil
}
a := headA
b := headB
// When a reaches the end — switch to headB
// When b reaches the end — switch to headA
// They will meet at the intersection (or both become nil)
for a != b {
if a != nil {
a = a.Next
} else {
a = headB
}
if b != nil {
b = b.Next
} else {
b = headA
}
}
return a // Intersection point or nil
}
static ListNode? GetIntersection(ListNode? headA, ListNode? headB)
{
if (headA is null || headB is null)
{
return null;
}
var a = headA;
var b = headB;
// When a reaches the end — switch to headB
// When b reaches the end — switch to headA
// They will meet at the intersection (or both become null)
while (!ReferenceEquals(a, b))
{
a = a is not null ? a.Next : headB;
b = b is not null ? b.Next : headA;
}
return a; // Intersection point or null
}
def get_intersection(
head_a: ListNode | None, head_b: ListNode | None
) -> ListNode | None:
if head_a is None or head_b is None:
return None
a = head_a
b = head_b
# When a reaches the end — switch to head_b
# When b reaches the end — switch to head_a
# They will meet at the intersection (or both become None)
while a is not b:
a = a.next if a is not None else head_b
b = b.next if b is not None else head_a
return a # Intersection point or None
Почему работает:
len(A) + len(B) = len(B) + len(A)
Оба пройдут одинаковое расстояние до точки пересечения.
## Сводная таблица паттернов
| Задача | Подход | Сложность |
|--------------------------|-------------------|-----------------|
| Обнаружение цикла | Fast/Slow | O(n) / O(1) |
| Начало цикла | Fast/Slow + reset | O(n) / O(1) |
| Середина списка | Fast/Slow | O(n) / O(1) |
| Палиндром | Середина + reverse| O(n) / O(1) |
| N-й с конца | Fast ahead by n | O(n) / O(1) |
| Пересечение | Two pointer switch| O(n+m) / O(1) |
> **Запомни:** Fast & Slow — это не просто про связные списки. Этот паттерн применяется для: обнаружения циклов в любых последовательностях, нахождения дубликатов (Floyd's для массивов), определения структурных свойств (середина, длина). На интервью: если задача про связный список — первым делом подумай о двух указателях.
## Итоги
1. Fast (2 шага) + Slow (1 шаг) = обнаружение цикла
2. Reset к head после встречи = начало цикла
3. Когда fast дошёл до конца, slow на середине
4. Палиндром = середина + разворот + сравнение
5. N-й с конца = опережение fast на n шагов
6. Все задачи решаются за O(n) времени и O(1) памяти
Проверь себя
Как проверить, является ли связный список палиндромом, используя O(1) дополнительной памяти?
После обнаружения цикла в алгоритме Флойда, как найти НАЧАЛО цикла?
В задаче «удалить n-й элемент с конца» списка, зачем fast опережает slow на n+1 шагов, а не на n?
Для списка 1->2->3->4->5, где окажется slow-указатель когда fast дойдёт до конца?
В алгоритме Флойда для обнаружения цикла, почему fast двигается именно на 2 шага, а не на 3?