Flawed solution
Unfortunately, the solution by thkang and the modified version from unutbu fail for surprisingly simple inputs.
For example, we (correctly) get an integer result for the lists [1, 2, 1]
and [1, 1, 2]
:
>>> is_shifted_copy([1, 2, 1], [1, 1, 2])
2
However, when swapping the arguments, we get the (incorrect) result False
:
>>> is_shifted_copy([1, 1, 2], [1, 2, 1])
False
Similarly, other lists that are shifted copies aren't treated correctly:
>>> is_shifted_copy([1, 2, 2], [2, 1, 2])
False
>>> is_shifted_copy([1, 1, 2, 1], [1, 2, 1, 1])
False
>>> is_shifted_copy([1, 2, 1, 3, 3], [3, 1, 2, 1, 3])
False
To understand the source of this problem, let me reproduce the current version of thkang's solution (with a modified call to next
for Python 3):
import itertools
def is_shifted_copy(list1, list2):
if len(list1) != len(list2):
return False
iterator = iter(list2)
for i, item in enumerate(itertools.chain(list1, list1)):
try:
if item == next(iterator):
continue
else:
iterator = iter(list2) # Reset iterator
except StopIteration:
return i - len(list2)
else:
return False
Now, the following happens for a call like is_shifted_copy([1, 2, 2], [2, 1, 2])
:
i |
item |
next(iterator) |
Result |
0 |
1 |
2^ |
reset iterator |
1 |
2 |
2^ |
continue |
2 |
2 |
1 |
reset iterator |
3 |
1 |
2^ |
reset iterator |
4 |
2 |
2^ |
continue |
5 |
2 |
1 |
reset iterator, return False |
^ First element of list2
.
As we can see, the repeated elements in the input lists cause the iterator itertools.chain(list1, list1)
to be exhausted before we even have the chance to get to the final element of list2
.
Possible fix
If we insist on using iterators (to prevent copying of potentially large input lists, for example), we'd have to ensure the iterator for the first list isn't exhausted before we can make a comparison with the elements from the second list. The only approach I can currently think of that guarantees a non-exhausted first iterator is creating a new iterator for all possible shifted versions of list1
:
import itertools
def is_shifted_copy(list1, list2):
length = len(list1)
if len(list2) != length:
return
for i in range(length):
iterator1 = itertools.islice(itertools.cycle(list1), i, i + length + 1)
iterator2 = iter(list2)
for item in iterator1:
try:
if item != next(iterator2):
break
except StopIteration:
return -i % length
return None
>>> is_shifted_copy([1, 2, 3], [1, 2, 3])
0
>>> is_shifted_copy([1, 2, 3], [3, 1, 2])
1
>>> is_shifted_copy([1, 2, 3], [2, 3, 1])
2
>>> is_shifted_copy([1, 2, 3], [3, 2, 1])
None
>>> is_shifted_copy([1, 2, 3], [1])
None
>>> is_shifted_copy([1, 1, 2], [2, 1, 1])
1
>>> is_shifted_copy([1, 2, 1], [1, 1, 2])
1
>>> is_shifted_copy([1, 1, 2], [1, 2, 1])
2
>>> is_shifted_copy([1, 2, 2], [2, 1, 2])
1
>>> is_shifted_copy([1, 1, 2, 1], [1, 2, 1, 1])
3
>>> is_shifted_copy([1, 2, 1, 3, 3], [3, 1, 2, 1, 3])
1
At best, the above algorithm finishes in O(n)
steps. The algorithm performs worst, namely O(n^2)
in time, for input lists with mostly duplicate elements.