Search code examples
pythonperformanceswap

Python Swap two digits in a number?


What is the fastest way to swap two digits in a number in Python? I am given the numbers as strings, so it'd be nice if I could have something as fast as

string[j] = string[j] ^ string[j+1] 
string[j+1] = string[j] ^ string[j+1]
string[j] = string[j] ^ string[j+1]

Everything I've seen has been much more expensive than it would be in C, and involves making a list and then converting the list back or some variant thereof.


Solution

  • This is faster than you might think, at least faster than Jon Clements' current answer in my timing test:

    i, j = (i, j) if i < j else (j, i) # make sure i < j
    s = s[:i] + s[j] + s[i+1:j] + s[i] + s[j+1:]
    

    Here's my test bed should you want to compare any other answers you get:

    import timeit
    import types
    
    N = 10000
    R = 3
    SUFFIX = '_test'
    SUFFIX_LEN = len(SUFFIX)
    
    def setup():
        import random
        global s, i, j
        s = 'abcdefghijklmnopqrstuvwxyz'
        i = random.randrange(len(s))
        while True:
            j = random.randrange(len(s))
            if i != j: break
    
    def swapchars_martineau(s, i, j):
        i, j = (i, j) if i < j else (j, i) # make sure i < j
        return s[:i] + s[j] + s[i+1:j] + s[i] + s[j+1:]
    
    def swapchars_martineau_test():
        global s, i, j
        swapchars_martineau(s, i, j)
    
    def swapchars_clements(text, fst, snd):
        ba = bytearray(text)
        ba[fst], ba[snd] = ba[snd], ba[fst]
        return str(ba)
    
    def swapchars_clements_test():
        global s, i, j
        swapchars_clements(s, i, j)
    
    # find all the functions named *SUFFIX in the global namespace
    funcs = tuple(value for id,value in globals().items()
                if id.endswith(SUFFIX) and type(value) is types.FunctionType)
    
    # run the timing tests and collect results
    timings = [(f.func_name[:-SUFFIX_LEN],
                min(timeit.repeat(f, setup=setup, repeat=R, number=N))
               ) for f in funcs]
    timings.sort(key=lambda x: x[1])  # sort by speed
    fastest = timings[0][1]  # time fastest one took to run
    longest = max(len(t[0]) for t in timings) # len of longest func name (w/o suffix)
    
    print 'fastest to slowest *_test() function timings:\n' \
          ' {:,d} chars, {:,d} timeit calls, best of {:d}\n'.format(len(s), N, R)
    
    def times_slower(speed, fastest):
        return speed/fastest - 1.0
    
    for i in timings:
        print "{0:>{width}}{suffix}() : {1:.4f} ({2:.2f} times slower)".format(
                    i[0], i[1], times_slower(i[1], fastest), width=longest, suffix=SUFFIX)
    

    Addendum:

    For the special case of swapping digit characters in a positive decimal number given as a string, the following also works and is a tiny bit faster than the general version at the top of my answer.

    The somewhat involved conversion back to a string at the end with the format() method is to deal with cases where a zero got moved to the front of the string. I present it mainly as a curiosity, since it's fairly incomprehensible unless you grasp what it does mathematically. It also doesn't handle negative numbers.

    n = int(s)
    len_s = len(s)
    ord_0 = ord('0')
    di = ord(s[i])-ord_0
    dj = ord(s[j])-ord_0
    pi = 10**(len_s-(i+1))
    pj = 10**(len_s-(j+1))
    s = '{:0{width}d}'.format(n + (dj-di)*pi + (di-dj)*pj, width=len_s)