Most pythonic way of rotating a circular list to a canonical point

Tim Chase python.list at tim.thechases.com
Sun Aug 2 20:19:16 CEST 2015


On 2015-08-01 13:34, Lukas Barth wrote:
> I have a list of numbers that I treat as "circular", i.e. [1,2,3]
> and [2,3,1] should be the same. Now I want to rotate these to a
> well defined status, so that I can can compare them.
> 
> If all elements are unique, the solution is easy: find the minimum
> element, find its index, then use mylist[:index] + mylist[index:],
> i.e. the minimum element will always be at the beginning.
> 
> But say I have [0,1,0,2,0,3]. I can in fact guarantee that no
> *pair* will appear twice in that list, i.e. I could search for the
> minimum, if that is unique go on as above, otherwise find *all*
> positions of the minimum, see which is followed by the smallest
> element, and then rotate that position to the front.

Well, you can pull the minimum (or maximum) of all the rotations to
get the "canonical" version:

  def canonical(lst):
    """return the "canonical" representation of a list"""
    return min(
      lst if i == 0 else (lst[i:] + lst[:i])
      for i in range(len(lst))
      )

which you can determine, then hash once, and store.  It's not a cheap
operation, but once you've determined the canonical/hash version,
then equality-testing becomes an O(1) test if comparing hashes, or
O(N) if comparing lists rather than an O(N*2) brute-force test
(which could be lower depending on the commonality).

  def circular_compare1(a, b):
    if a == b: return True
    return any(a == (b[i:] + b[:i]) for i in range(1, len(b)))

  def circular_compare2(a, b):
    lena = len(a)
    if lena != len(b): return False
    return any(
      all(a[i] == b[(i + offset) % lena] for i in range(lena))
      for offset in range(lena)
      )

  for fn in (circular_compare1, circular_compare2):
    for (a, b), expected in (
        (([1,2,3], [1,2,3]), True), # the same
        (([1,2,3], [3,1,2]), True), # simple rotation
        (([1,2,3], [1,2,3,4]), False), # mismatched lengths
        (([0,1,0,2,0,3], [0,2,0,3,0,1]), True), # repeated elements, simple rotation
        ):
      result = bool(fn(a, b))
      if result == expected:
        print "PASS %s %r/%r=%s, not %s" % (
          fn.__name__, a, b, result, expected
          )
      else:
        print "FAIL %s %r/%r=%s, not %s" % (
          fn.__name__, a, b, result, expected
          )
      ca = canonical(a)
      cb = canonical(b)
      print "   Canonical A:", ca
      print "   Canonical B:", cb
      print "   Equal:", (ca == cb)

-tim





More information about the Python-list mailing list