Fast and Slow Pointers - kjingers/Grokking-Notes GitHub Wiki

Cycle in Linked List

Easy

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


def has_cycle(head):

  slow = head.next
  fast = head.next.next

  while True:

    if slow is None or fast is None:
      return False

    if slow.value == fast.value:
      return True

    slow = slow.next
    fast = fast.next.next

To get the length of a cycle, first find the node where the pointers meet as above. Save that node. Then keep going next while iterating a counter until you reach that same node again.

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


def find_cycle_length(head):
  slow, fast = head, head
  while fast is not None and fast.next is not None:
    fast = fast.next.next
    slow = slow.next
    if slow == fast:  # found the cycle
      return calculate_cycle_length(slow)

  return 0


def calculate_cycle_length(slow):
  current = slow
  cycle_length = 0
  while True:
    current = current.next
    cycle_length += 1
    if current == slow:
      break
  return cycle_length

Find Start of Cycle

First, find the length of the cycle K like above. Then, with two new pointers p1 and p2, move one of them (p2) ahead K nodes. Next, increment p1 and p2 in a loop until they are the same. This will be the start of the cycle.

def find_cycle_length(slow):
  cycleLength = 0

  p = slow

  while True:
    p = p.next
    cycleLength += 1
    if p == slow:
      break
  
  return cycleLength


def find_cycle_start(head):
  fast = head
  slow = head

  while fast is not None and slow is not None:
    fast = fast.next.next
    slow = slow.next

    if fast == slow:
      break
    
  cycleLength = find_cycle_length(slow)

  p1 = head
  p2 = head

  for _ in range(cycleLength):
    p2 = p2.next
  
  while p1 is not p2:
    p1 = p1.next
    p2 = p2.next
  
  return p1

Happy Number

Square sum of a number's digits either loops or goes to 1. Use fast and slow pointer method to check if looping. If looped, check if value is 1 to determine if happy number or not

def find_cycle_length(slow):
  cycleLength = 0

  p = slow

  while True:
    p = p.next
    cycleLength += 1
    if p == slow:
      break
  
  return cycleLength


def find_cycle_start(head):
  fast = head
  slow = head

  while fast is not None and slow is not None:
    fast = fast.next.next
    slow = slow.next

    if fast == slow:
      break
    
  cycleLength = find_cycle_length(slow)

  p1 = head
  p2 = head

  for _ in range(cycleLength):
    p2 = p2.next
  
  while p1 is not p2:
    p1 = p1.next
    p2 = p2.next
  
  return p1

Middle of the Linked List

Just use a fast and slow pointer. When fast pointer finishes, slow pointer points to middle.

def find_middle_of_linked_list(head):
  slow = head
  fast = head

  while fast and fast.next:
    slow = slow.next
    fast = fast.next.next

  return slow

Palindrome Linked List

def reverse(head):
  prev = None

  while head is not None:
    next = head.next
    head.next = prev
    prev = head
    head = next

  return prev

def printList(head):
  copy = head
  while copy is not None:
    print(copy.value, end=" -> ")
    copy = copy.next
  print("None")
  return

def is_palindromic_linked_list(head):

  slow, fast = head, head
  isPalindrome = True

  # Find middle
  while fast is not None and fast.next is not None:
    fast = fast.next.next
    slow = slow.next


  # Reverse Second Half
  reversed_head = reverse(slow)
  reversed_head_copy = reversed_head
  
  #Compare First half with reversed second half
  while reversed_head is not None and head is not None:

    if head.value != reversed_head.value:
      isPalindrome = False

    reversed_head = reversed_head.next
    head = head.next

  # Reverse it back
  reverse(reversed_head_copy)

  return isPalindrome

Rearrange Linked List

Use above to find middle of linked list and reverse the second half of the linked list. Then iterate through the lists to interleave them together.

def reverse(head):
  prev = None
  while head is not None:
    next = head.next
    head.next = prev
    prev = head
    head = next
  return prev

def reorder(head):

  fast, slow = head, head

  # Find middle of Linked List
  while fast is not None and fast.next is not None:
    fast = fast.next.next
    slow = slow.next

  sec_half_head = reverse(slow)
  head_first_half = head

  #printList(sec_half_head)

  while head_first_half is not None and sec_half_head is not None:
    temp = head_first_half.next
    head_first_half.next = sec_half_head
    head_first_half = temp

    temp = sec_half_head.next
    sec_half_head.next = head_first_half
    sec_half_head = temp

  if head_first_half is not None:
    head_first_half.next = None

  return

Cycle in Circular Array

Solution is not too difficult. For this we check if there is a loop starting from each index from 0 to len(arr). Just like normal, loop is when slow = fast at some point. The only other checks we have, is we need to make sure the direction of each index is the same as the starting one. Can improve time complexity to O(n), by keeping track of visited indices. Can probably do this with dictionary where `d[index] = 1 for visited.

def next_index(arr, idx, direction):

  if direction != (arr[idx] >= 0):
    return -1

  next_index = (idx + arr[idx]) % len(arr)

  if next_index == idx:
    return -1

  return next_index
 
def circular_array_loop_exists(arr):

  # Loop through Each Index with Fast and Slow
  # Pointers to check if loop.

  for i in range(len(arr)):
    slow = i
    fast = i

    # Positive is forward, Negtive is backward
    direction = arr[i] >= 0

    while True:

      slow = next_index(arr, slow, direction)
      fast = next_index(arr, fast, direction)
      fast = next_index(arr, fast, direction)

      if slow == -1 or fast == -1 or slow == fast:
        break

    if slow != -1 and slow == fast:
      return True
  
  return False