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)
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
| Method | Time Complexity | Best For |
|---|---|---|
| Concatenation + sliding window | O(n²) | Standard lists |
| Deque rotation | O(n²) | Visualizing rotations |
| Index-based check | O(n²) worst, O(n) average | Few matching start elements |
String in operator | O(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.