Perché max è più lento dell'ordinamento?


92

Ho scoperto che maxè più lento della sortfunzione in Python 2 e 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

Perchè è max ( O(n)) più lento rispetto alla sortfunzione di ( O(nlogn))?


3
Hai eseguito l'analisi Python 2 una volta e il codice Python 3 è esattamente lo stesso.
erip

9
a.sort()funziona sul posto. Provasorted(a)
Andrea Corbellini

Se l'hai riparato, postare ciò che hai fatto per risolverlo, per favore.
Pretzel

4
@ Pretzel OP significa che il post è stato modificato, non che il problema sia stato risolto.
erip

2
@WeizhongTu ma sortordina e poi aviene ordinato per sempre
njzk2

Risposte:


125

Devi stare molto attento quando usi il timeitmodulo in Python.

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

Qui il codice di inizializzazione viene eseguito una volta per produrre un array randomizzato a. Quindi il resto del codice viene eseguito più volte. La prima volta che ordina l'array, ma ogni altra volta si chiama il metodo di ordinamento su un array già ordinato. Viene restituito solo il tempo più veloce, quindi stai effettivamente calcolando quanto tempo impiega Python per ordinare un array già ordinato.

Parte dell'algoritmo di ordinamento di Python consiste nel rilevare quando l'array è già parzialmente o completamente ordinato. Quando è completamente ordinato, deve semplicemente scansionare una volta attraverso l'array per rilevarlo e poi si ferma.

Se invece hai provato:

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

quindi l'ordinamento avviene su ogni ciclo di temporizzazione e puoi vedere che il tempo per l'ordinamento di un array è davvero molto più lungo rispetto a trovare il valore massimo.

Modifica: la risposta di @ skyking spiega la parte che ho lasciato inspiegabile: a.sort()sa che sta lavorando su un elenco, quindi può accedere direttamente agli elementi. max(a)funziona su qualsiasi iterabile arbitrario, quindi deve usare l'iterazione generica.


10
Buona pesca. Non mi sono mai reso conto che lo stato dell'interprete viene mantenuto durante le esecuzioni del codice. Ora mi chiedo quanti benchmark difettosi ho prodotto in passato. : -}
Frerich Raabe

1
Questo era ovvio per me. Ma nota che anche se ordini un array già ordinato devi controllare tutti gli elementi. Il che è tanto faticoso quanto ottenere il massimo ... Per me questa sembra una mezza risposta.
Karoly Horvath

2
@KarolyHorvath, hai ragione. Penso che @skyking abbia ottenuto l'altra metà della risposta: a.sort()sa che sta lavorando su un elenco, quindi può accedere direttamente agli elementi. max(a)lavora su una sequenza arbitraria per utilizzare iterazioni generiche.
Duncan

1
@KarolyHorvath forse la previsione del ramo può spiegare perché l'ordinamento ripetuto di un array ordinato è più veloce: stackoverflow.com/a/11227902/4600
marcospereira

1
@JuniorCompressor listsort.txtspiega "Ha prestazioni soprannaturali su molti tipi di array parzialmente ordinati (meno di confronti lg (N!) Necessari e solo N-1)" e poi prosegue spiegando tutti i tipi di ottimizzazioni cruente. Suppongo che possa fare molte ipotesi che maxnon possono, cioè l'ordinamento non è asintoticamente più veloce.
Frerich Raabe

87

Prima di tutto, nota che max() utilizza il protocollo iteratore , mentre list.sort()utilizza codice ad-hoc . Chiaramente, l'utilizzo di un iteratore è un sovraccarico importante, ecco perché stai osservando quella differenza nei tempi.

Tuttavia, a parte questo, i tuoi test non sono equi. Stai correndoa.sort() sullo stesso elenco più di una volta. L' algoritmo utilizzato da Python è specificamente progettato per essere veloce per dati già (parzialmente) ordinati. I tuoi test dicono che l'algoritmo sta facendo bene il suo lavoro.

Questi sono test equi:

$ 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

Qui creo ogni volta una copia dell'elenco. Come puoi vedere, l'ordine di grandezza dei risultati è diverso: micro- vs millisecondi, come ci aspetteremmo.

E ricorda: big-Oh specifica un limite superiore! Il limite inferiore per l'algoritmo di ordinamento di Python è Ω ( n ). Essere O ( n log n ) non implica automaticamente che ogni esecuzione richieda un tempo proporzionale a n log n . Non implica nemmeno che debba essere più lento di un algoritmo O ( n ), ma questa è un'altra storia. Ciò che è importante capire è che in alcuni casi favorevoli, un algoritmo O ( n log n ) può essere eseguito in tempo O ( n ) o meno.


31

Ciò potrebbe essere dovuto al fatto che l.sortè un membro di listwhile maxè una funzione generica. Ciò significa che l.sortpuò fare affidamento sulla rappresentazione interna di listwhile maxdovrà passare attraverso un protocollo iteratore generico.

Questo fa sì che ogni elemento fetch l.sortsia più veloce di ogni elemento fetch che maxfa.

Presumo che se invece usi sorted(a)otterrai il risultato più lento di max(a).


5
Questa ipotesi è solo una riga di tempo per diventare più concreti. Non mettere in discussione la tua conoscenza, solo che una tale aggiunta è banale per la dimostrazione di chi non la conosce.
Reti43

Hai ragione che sorted(a)è più lento di max(a). Non sorprende che sia più o meno la stessa velocità di a.sort(), ma la tua congettura sul motivo per cui non lo è - è perché l'OP ha commesso un errore nei loro test, come indicato nella risposta accettata.
martineau

Il punto era che esiste la possibilità che il protocollo iteratore generico abbia un overhead sufficiente per compensare il log(n)fattore di complessità. Questo è che un O(n)algoritmo è garantito solo per essere più veloce di un O(nlogn)algoritmo per dimensioni sufficientemente grandi n(ad esempio perché il tempo per ciascuna operazione può differire tra gli algoritmi - nlogni passaggi veloci possono essere più veloci dei npassaggi lenti). Esattamente dove il pareggio non è stato considerato in questo caso (ma si dovrebbe essere consapevoli che il log nfattore non è un fattore molto grande per i piccoli n).
skyking
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.