Jie Hu
Jie Hu

Reputation: 87

In python, why string.count() is faster than loop?

In leetcode, I there's a question to check if a disordered series of string "U","D","L","R" will form a circle.

My submission is like:

def judgeCircle(moves):

    l=r=u=d=0

    for i in moves:
        if i == 'L':
            l+=1
        if i == 'D':
            d+=1
        if i == 'R':
            r+=1
        if i == 'U':
            u+=1

    return ((l-r)==0) and ((u-d)==0)

and the judger think it cost 239ms while another single line solution:

def judgeCircle(moves):
    return (moves.count('R')==moves.count('L')) and 
           (moves.count('U')==moves.count('D'))

costs only 39ms?

Though I understand the less code, the better, but I thought the 2nd will loop 4 times, am I misunderstanding?

Thanks

Upvotes: 4

Views: 3050

Answers (2)

PM 2Ring
PM 2Ring

Reputation: 55499

Here's some timeit code showing the speeds of various approaches, using both perfect data with equal counts of all 4 keys, and random data with approximately equal numbers of each key.

#!/usr/bin/env python3

''' Test speeds of various algorithms that check
    if a sequence of U, D, L, R moves make a closed circle.

    See https://stackoverflow.com/q/46568696/4014959

    Written by PM 2Ring 2017.10.05
'''

from timeit import Timer
from random import seed, choice, shuffle
from collections import Counter, defaultdict

def judge_JH0(moves):
    l = r = u = d = 0
    for i in moves:
        if i == 'L':
            l += 1
        if i == 'D':
            d += 1
        if i == 'R':
            r += 1
        if i == 'U':
            u += 1
    return ((l-r) == 0) and ((u-d) == 0)

def judge_JH1(moves):
    l = r = u = d = 0
    for i in moves:
        if i == 'L':
            l += 1
        elif i == 'D':
            d += 1
        elif i == 'R':
            r += 1
        elif i == 'U':
            u += 1
    return (l == r) and (u == d)

def judge_count(moves):
    return ((moves.count('R') == moves.count('L')) and 
        (moves.count('U') == moves.count('D')))

def judge_counter(moves):
    d = Counter(moves)
    return (d['R'] == d['L']) and (d['U'] == d['D'])

def judge_dict(moves):
    d = {}
    for c in moves:
        d[c] = d.get(c, 0) + 1
    return ((d.get('R', 0) == d.get('L', 0)) and 
        (d.get('U', 0) == d.get('D', 0)))

def judge_defdict(moves):
    d = defaultdict(int)
    for c in moves:
        d[c] += 1
    return (d['R'] == d['L']) and (d['U'] == d['D'])


# All the functions
funcs = (
    judge_JH0,
    judge_JH1,
    judge_count,
    judge_counter,
    judge_dict,
    judge_defdict,
)

def verify(data):
    print('Verifying...')
    for func in funcs:
        name = func.__name__
        result = func(data)
        print('{:20} : {}'.format(name, result))
    print()

def time_test(data, loops=100):
    timings = []
    for func in funcs:
        t = Timer(lambda: func(data))
        result = sorted(t.repeat(3, loops))
        timings.append((result, func.__name__))
    timings.sort()
    for result, name in timings:
        print('{:20} : {}'.format(name, result))
    print()

# Make some data
keys = 'DLRU'
seed(42)
size = 100

perfect_data = list(keys * size)
shuffle(perfect_data)
print('Perfect')
verify(perfect_data)

random_data = [choice(keys) for _ in range(4 * size)]
print('Random data stats:')
for k in keys:
    print(k, random_data.count(k))
print()
verify(random_data)

loops = 1000
print('Testing perfect_data')
time_test(perfect_data, loops=loops)

print('Testing random_data')
time_test(random_data, loops=loops)

typical output

Perfect
Verifying...
judge_JH0            : True
judge_JH1            : True
judge_count          : True
judge_counter        : True
judge_dict           : True
judge_defdict        : True

Random data stats:
D 89
L 100
R 101
U 110

Verifying...
judge_JH0            : False
judge_JH1            : False
judge_count          : False
judge_counter        : False
judge_dict           : False
judge_defdict        : False

Testing perfect_data
judge_counter        : [0.11746118000155548, 0.11771785900054965, 0.12218693499744404]
judge_count          : [0.12314812499971595, 0.12353860199800692, 0.12495016200409736]
judge_defdict        : [0.20643479600403225, 0.2069275510002626, 0.20834802299941657]
judge_JH1            : [0.25801684000180103, 0.2689959089984768, 0.27642749399819877]
judge_JH0            : [0.36819701099739177, 0.37400564400013536, 0.40291943999909563]
judge_dict           : [0.3991459790049703, 0.4004156189985224, 0.4040740730051766]

Testing random_data
judge_count          : [0.061543637995782774, 0.06157537500257604, 0.06704995800100733]
judge_counter        : [0.11995147699781228, 0.12068584300141083, 0.1207217440023669]
judge_defdict        : [0.2096717179956613, 0.21544414199888706, 0.220649760995002]
judge_JH1            : [0.261116588000732, 0.26281095200101845, 0.2706491360004293]
judge_JH0            : [0.38465088899829425, 0.38476935599464923, 0.3921787180006504]
judge_dict           : [0.40892754300148226, 0.4094729179996648, 0.4135226650032564]

These timings were obtained on my old 2GHz 32 bit machine running Python 3.6.0 on Linux.


Here are a couple more functions.

def judge_defdictlist(moves):
    d = defaultdict(list)
    for c in moves:
        d[c].append(c)
    return (len(d['R']) == len(d['L'])) and (len(d['U']) == len(d['D']))

# Sort to groups in alphabetical order: DLRU
def judge_sort(moves):
    counts = [sum(1 for _ in g) for k, g in groupby(sorted(moves))]
    return (counts[0] == counts[3]) and (counts[1] == counts[2])

judge_defdictlist is slower than judge_defdict but faster than judge_JH1, and of course it uses more RAM than judge_defdict.

judge_sort is slower than judge_JH0, but faster than judge_dict.

Upvotes: 4

myaut
myaut

Reputation: 11514

Both code samples have algorithmic complexity of O(n), but you shouldn't be fooled by big O as it only shows trend. Execution time for an O(n) algorithm can be expressed as C * n where C is constant which depends on many factors.

For .count() code, you'll need to do 4 loops in string_count() C function, but C functions are fast. It also uses some advanced algorithms inside like fastsearch. Only string search is performed here, minimum interpreter overheads.

In the pure Python code you need only single loop, but each iteration will require to execute much more lower-level code because Python is the interpreted language*. For example, you're creating new unicode or string object for each iteration of the loop, and creating objects is a very expensive operation. Since integer objects are immutable, you'll need to re-create them for each counter.

* Assuming you're using CPython interpreter, which is pretty much default

Upvotes: 4

Related Questions