¿Por qué max es más lento que sort?

92

Descubrí que maxes más lento que la sortfunción en Python 2 y 3.

Python 2

$ python -m timeit -s 'import random;a=range(10000);random.shuffle(a)' 'a.sort();a[-1]'
1000 loops, best of 3: 239 usec per loop
$ python -m timeit -s 'import random;a=range(10000);random.shuffle(a)' 'max(a)'        
1000 loops, best of 3: 342 usec per loop

Python 3

$ python3 -m timeit -s 'import random;a=list(range(10000));random.shuffle(a)' 'a.sort();a[-1]'
1000 loops, best of 3: 252 usec per loop
$ python3 -m timeit -s 'import random;a=list(range(10000));random.shuffle(a)' 'max(a)'
1000 loops, best of 3: 371 usec per loop

¿Por qué es max ( O(n)) más lenta que la sortfunción ( O(nlogn))?

WeizhongTu
fuente
3
Ejecutó el análisis de Python 2 una vez y el código de Python 3 es exactamente el mismo.
erip
9
a.sort()funciona en el lugar. Pruebasorted(a)
Andrea Corbellini
Si lo solucionó, vuelva a publicar lo que hizo para solucionarlo, por favor.
Pretzel
4
@Pretzel OP significa que la publicación ha sido editada, no que el problema se haya solucionado.
erip
2
@WeizhongTu pero sortlas clases, y luego ase ordenan siempre
njzk2

Respuestas:

125

Debe tener mucho cuidado al usar el timeitmódulo en Python.

python -m timeit -s 'import random;a=range(10000);random.shuffle(a)' 'a.sort();a[-1]'

Aquí, el código de inicialización se ejecuta una vez para producir una matriz aleatoria a. Luego, el resto del código se ejecuta varias veces. La primera vez que ordena la matriz, pero cada dos veces se llama al método sort en una matriz ya ordenada. Solo se devuelve el tiempo más rápido, por lo que en realidad está midiendo cuánto tiempo le toma a Python ordenar una matriz ya ordenada.

Parte del algoritmo de clasificación de Python es detectar cuándo la matriz ya está ordenada parcial o completamente. Cuando está completamente ordenado, simplemente tiene que escanear una vez a través de la matriz para detectar esto y luego se detiene.

Si en cambio lo intentaste:

python -m timeit -s 'import random;a=range(100000);random.shuffle(a)' 'sorted(a)[-1]'

luego, la clasificación ocurre en cada ciclo de tiempo y puede ver que el tiempo para clasificar una matriz es mucho más largo que simplemente encontrar el valor máximo.

Editar: la respuesta de @ skyking explica la parte que dejé sin explicar: a.sort()sabe que está trabajando en una lista, por lo que puede acceder directamente a los elementos. max(a)funciona en cualquier iterable arbitrario, por lo que debe usar una iteración genérica.

Duncan
fuente
10
Buena atrapada. Nunca me di cuenta de que el estado del intérprete se conserva en todas las ejecuciones del código. Ahora me pregunto cuántos puntos de referencia defectuosos produje en el pasado. : -}
Frerich Raabe
1
Eso era obvio para mí. Pero tenga en cuenta que incluso si ordena una matriz ya ordenada, debe verificar todos los elementos. Que es tanto trabajo como obtener el máximo ... Para mí esto parece una respuesta a medias.
Karoly Horvath
2
@KarolyHorvath, tienes razón. Creo que @skyking obtuvo la otra mitad de la respuesta: a.sort()sabe que está trabajando en una lista, por lo que puede acceder directamente a los elementos. max(a)funciona en una secuencia arbitraria para no utilizar iteraciones genéricas.
Duncan
1
@KarolyHorvath tal vez la predicción de rama pueda explicar por qué ordenar repetidamente una matriz ordenada es más rápido: stackoverflow.com/a/11227902/4600
marcospereira
1
@JuniorCompressor listsort.txtexplica "Tiene un rendimiento sobrenatural en muchos tipos de matrices parcialmente ordenadas (se necesitan menos de lg (N!) Comparaciones y tan pocas como N-1)" y luego explica todo tipo de optimizaciones sangrientas. Supongo que puede hacer muchas suposiciones que maxno, es decir, la clasificación no es asintóticamente más rápida.
Frerich Raabe
87

En primer lugar, tenga en cuenta que max()utiliza el protocolo iterador , mientras que list.sort()utiliza código ad-hoc . Claramente, el uso de un iterador es una sobrecarga importante, por eso está observando esa diferencia en los tiempos.

Sin embargo, aparte de eso, sus pruebas no son justas. Estás a.sort()en la misma lista más de una vez. El algoritmo utilizado por Python está diseñado específicamente para ser rápido para datos ya ordenados (parcialmente). Tus pruebas dicen que el algoritmo está haciendo bien su trabajo.

Estas son pruebas justas:

$ python3 -m timeit -s 'import random;a=list(range(10000));random.shuffle(a)' 'max(a[:])'
1000 loops, best of 3: 227 usec per loop
$ python3 -m timeit -s 'import random;a=list(range(10000));random.shuffle(a)' 'a[:].sort()'
100 loops, best of 3: 2.28 msec per loop

Aquí estoy creando una copia de la lista cada vez. Como puede ver, el orden de magnitud de los resultados es diferente: micro- vs milisegundos, como era de esperar.

Y recuerde: ¡gran-Oh especifica un límite superior! El límite inferior del algoritmo de clasificación de Python es Ω ( n ). Ser O ( n log n ) no implica automáticamente que cada ejecución requiera un tiempo proporcional an log n . Ni siquiera implica que deba ser más lento que un algoritmo O ( n ), pero esa es otra historia. Lo que es importante entender es que en algunos casos favorables, un algoritmo O ( n log n ) puede ejecutarse en O ( n ) tiempo o menos.

Andrea Corbellini
fuente
31

Esto podría deberse a que l.sortes miembro de listwhile maxes una función genérica. Esto significa que l.sortpuede confiar en la representación interna de listwhile maxtendrá que pasar por un protocolo de iterador genérico.

Esto hace que la búsqueda de cada elemento l.sortsea ​​más rápida que la búsqueda de cada elemento max.

Supongo que si en su lugar usa sorted(a), obtendrá el resultado más lento que max(a).

Rey del cielo
fuente
5
Esa suposición está a solo una línea de distancia para volverse más concreta. Sin cuestionar su conocimiento, solo que tal adición es trivial para la demostración de aquellos que no la conocen.
Reti43
Tienes razón en que sorted(a)es más lento que max(a). No es sorprendente que a.sort()tenga aproximadamente la misma velocidad que , pero su conjetura sobre la razón por la que no lo es, es porque el OP cometió un error en sus pruebas como se señala en la respuesta aceptada.
martineau
El punto era que existe la posibilidad de que el protocolo de iterador genérico tenga suficiente sobrecarga para compensar el log(n)factor de complejidad. Es decir O(n), solo se garantiza que un O(nlogn)algoritmo sea ​​más rápido que un algoritmo para lo suficientemente grande n(por ejemplo, porque el tiempo de cada operación puede diferir entre los algoritmos; nlognlos pasos rápidos pueden ser más rápidos que nlos pasos lentos). En este caso, no se consideró exactamente dónde está el punto de equilibrio (pero uno debe tener en cuenta que el log nfactor no es un factor muy importante para los pequeños n).
Skyking