Skip to main content

How to Check if Two Lists are Circularly Identical in Python

Two lists are circularly identical if one is a rotated version of the other. For example:

  • List A: [10, 20, 30, 40]
  • List B: [30, 40, 10, 20] (A rotated by 2 positions)

This problem appears in geometry processing, pattern recognition, and cyclic sequence analysis.

The Concatenation Trick

The most elegant solution uses a key insight: If B is a rotation of A, then B must appear as a contiguous subsequence within A + A.

Visual proof:

A     = [1, 2, 3]
A + A = [1, 2, 3, 1, 2, 3]

All rotations are visible:
[1, 2, 3] - original (position 0)
[2, 3, 1] - rotation (position 1)
[3, 1, 2] - rotation (position 2)

Implementation

def is_circularly_identical(list_a: list, list_b: list) -> bool:
"""Check if list_b is a rotation of list_a."""
# Lengths must match
if len(list_a) != len(list_b):
return False

# Empty lists are considered identical
if not list_a:
return True

# Check if list_b appears in doubled list_a
doubled = list_a + list_a
n = len(list_b)

for i in range(len(list_a)):
if doubled[i:i + n] == list_b:
return True

return False


# Test cases
a = [1, 2, 3, 4]
b = [3, 4, 1, 2]
c = [2, 1, 3, 4]

print(is_circularly_identical(a, b)) # True (rotated by 2)
print(is_circularly_identical(a, c)) # False (not a rotation)
Don't Use String Conversion

A tempting shortcut is converting lists to strings:

# ❌ DANGEROUS - Can produce false positives
str(list_a) in str(list_a + list_a)

This fails with certain values:

  • [1, 11] and [11, 1] might incorrectly match
  • [12, 3] and [1, 23] could cause issues

Always use proper list comparison.

Using collections.deque

The deque class provides an optimized rotate() method that physically rotates elements:

from collections import deque


def is_circularly_identical_deque(list_a: list, list_b: list) -> bool:
"""Check circular identity by rotating a deque."""
if len(list_a) != len(list_b):
return False

if not list_a:
return True

d_a = deque(list_a)
d_b = deque(list_b)

# Try each rotation
for _ in range(len(list_a)):
if d_a == d_b:
return True
d_a.rotate(1)

return False


a = [1, 2, 3, 4]
b = [4, 1, 2, 3]

print(is_circularly_identical_deque(a, b)) # True

Finding the Rotation Offset

Return how many positions B is rotated from A:

from collections import deque


def find_rotation_offset(list_a: list, list_b: list) -> int | None:
"""Find the rotation offset, or None if not circularly identical."""
if len(list_a) != len(list_b):
return None

if not list_a:
return 0

d_a = deque(list_a)
d_b = deque(list_b)

for offset in range(len(list_a)):
if d_a == d_b:
return offset
d_a.rotate(1)

return None


a = [1, 2, 3, 4]
b = [3, 4, 1, 2]

offset = find_rotation_offset(a, b)
print(f"Rotation offset: {offset}") # Rotation offset: 2

Optimized Solution with Index Finding

If you only need to check once, find where list_b[0] appears in list_a:

def is_circularly_identical_optimized(list_a: list, list_b: list) -> bool:
"""Optimized check by finding potential starting points."""
if len(list_a) != len(list_b):
return False

if not list_a:
return True

n = len(list_a)
first_elem = list_b[0]

# Find all positions where list_b could start
for start in range(n):
if list_a[start] == first_elem:
# Check if rotation from this position matches
match = True
for j in range(n):
if list_a[(start + j) % n] != list_b[j]:
match = False
break
if match:
return True

return False

Working with Strings

For strings, the concatenation trick works directly:

def is_circular_string(s1: str, s2: str) -> bool:
"""Check if s2 is a rotation of s1."""
if len(s1) != len(s2):
return False
return s2 in s1 + s1


print(is_circular_string("hello", "llohe")) # True
print(is_circular_string("hello", "lohel")) # True
print(is_circular_string("hello", "world")) # False

Finding All Rotations

Generate all circular rotations of a list:

def get_all_rotations(lst: list) -> list:
"""Return all circular rotations of a list."""
if not lst:
return [[]]

rotations = []
for i in range(len(lst)):
rotation = lst[i:] + lst[:i]
rotations.append(rotation)

return rotations


rotations = get_all_rotations([1, 2, 3])
for r in rotations:
print(r)

Output:

[1, 2, 3]
[2, 3, 1]
[3, 1, 2]

Canonical Form for Grouping

To group circularly identical lists, compute a canonical (normalized) form:

def canonical_rotation(lst: list) -> tuple:
"""Return the lexicographically smallest rotation as a tuple."""
if not lst:
return tuple()

n = len(lst)
rotations = [tuple(lst[i:] + lst[:i]) for i in range(n)]
return min(rotations)


# Lists with same canonical form are circularly identical
lists = [
[3, 1, 2],
[1, 2, 3],
[2, 3, 1],
[4, 5, 6]
]

groups = {}
for lst in lists:
key = canonical_rotation(lst)
if key not in groups:
groups[key] = []
groups[key].append(lst)

for canonical, members in groups.items():
print(f"{canonical}: {members}")

Output:

(1, 2, 3): [[3, 1, 2], [1, 2, 3], [2, 3, 1]]
(4, 5, 6): [[4, 5, 6]]

Method Comparison

MethodTime ComplexityBest For
Concatenation + sliding windowO(n²)Standard lists
Deque rotationO(n²)Visualizing rotations
Index-based checkO(n²) worst, O(n) averageFew matching start elements
String in operatorO(n)String rotations only

Summary

  • The concatenation trick (B in A+A) is the standard algorithmic approach.
  • Use sliding window comparison for lists since Python doesn't support list in list.
  • Never convert to strings: it causes false matches with numeric elements.
  • Use deque.rotate() when you need to physically manipulate rotations.
  • Compute canonical form when grouping circularly identical sequences.