Почему max медленнее, чем sort?

92

Я обнаружил, что maxэто медленнее, чем sortфункция в Python 2 и 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

Почему это max ( O(n)) медленнее , чем sortфункции ( O(nlogn))?

WeizhongTu
источник
3
Вы один раз провели анализ Python 2, и код Python 3 точно такой же.
erip
9
a.sort()работает на месте. Попробуйтеsorted(a)
Андреа Корбеллини
Если вы исправили это, опубликуйте, что вы сделали, чтобы исправить это, пожалуйста.
Pretzel
4
@Pretzel OP означает, что сообщение было отредактировано, а не проблема решена.
erip
2
@WeizhongTu но sortсортирует, а потом aсортирует навсегда
njzk2

Ответы:

125

Вы должны быть очень осторожны при использовании timeitмодуля в Python.

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

Здесь код инициализации запускается один раз для создания рандомизированного массива a. Затем остальной код запускается несколько раз. В первый раз он сортирует массив, но каждый раз вы вызываете метод сортировки для уже отсортированного массива. Возвращается только самое быстрое время, поэтому вы фактически рассчитываете, сколько времени потребуется Python для сортировки уже отсортированного массива.

Часть алгоритма сортировки Python состоит в том, чтобы определить, когда массив уже частично или полностью отсортирован. Когда он полностью отсортирован, ему просто нужно один раз просканировать массив, чтобы обнаружить это, а затем он останавливается.

Если вместо этого вы попробовали:

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

тогда сортировка происходит в каждом временном цикле, и вы можете видеть, что время на сортировку массива действительно намного больше, чем просто нахождение максимального значения.

Изменить: ответ @ skyking объясняет ту часть, которую я оставил необъясненной: a.sort()знает, что он работает со списком, поэтому может напрямую обращаться к элементам. max(a)работает с любой произвольной итерацией, поэтому необходимо использовать общую итерацию.

Дункан
источник
10
Хороший улов. Я никогда не понимал, что состояние интерпретатора сохраняется во время выполнения кода. Теперь мне интересно, сколько ошибочных тестов я создал в прошлом. : -}
Frerich Raabe
1
Для меня это было очевидно. Но обратите внимание, что даже если вы сортируете уже отсортированный массив, вам нужно проверить все элементы. Это так же сложно, как и получить максимум ... Мне это кажется полуответом.
Karoly Horvath
2
@KarolyHorvath, вы правы. Я думаю, что @skyking получил вторую половину ответа: a.sort()знает, что работает со списком, поэтому может напрямую обращаться к элементам. max(a)работает с произвольной последовательностью, чтобы не использовать общую итерацию.
Дункан
1
@KarolyHorvath, возможно, предсказание ветвления может объяснить, почему повторная сортировка отсортированного массива выполняется быстрее: stackoverflow.com/a/11227902/4600
marcospereira
1
@JuniorCompressor listsort.txtобъясняет: «Он обладает сверхъестественной производительностью на многих типах частично упорядоченных массивов (требуется меньше lg (N!) Сравнений и всего лишь N-1)», а затем продолжает объяснять все виды кровавой оптимизации. Я полагаю, он может делать много предположений, которые maxне могут, т.е. сортировка не выполняется асимптотически быстрее.
Frerich Raabe
86

Прежде всего, обратите внимание, что max()используется протокол итератора , а list.sort()используется специальный код . Очевидно, что использование итератора - это важные накладные расходы, поэтому вы наблюдаете эту разницу во времени.

Однако в остальном ваши тесты нечестные. Вы участвуете a.sort()в одном списке более одного раза. Алгоритм , используемый Python специально разработан , чтобы быть быстро для уже (частично) сортируются данные. Ваши тесты говорят, что алгоритм хорошо выполняет свою работу.

Это честные тесты:

$ 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

Здесь я каждый раз создаю копию списка. Как видите, порядок результатов отличается: микросекунды и миллисекунды, как и следовало ожидать.

И помните: big-Oh указывает верхнюю границу! Нижняя граница алгоритма сортировки Python равна Ω ( n ). Быть O ( n log n ) автоматически не означает, что каждый запуск занимает время, пропорциональное n log n . Это даже не означает, что он должен быть медленнее, чем алгоритм O ( n ), но это уже другая история. Важно понимать, что в некоторых благоприятных случаях алгоритм O ( n log n ) может работать за O ( n ) или меньше времени.

Андреа Корбеллини
источник
31

Это может быть связано с тем, что l.sortis a member of listwhile maxявляется универсальной функцией. Это означает, что l.sortможно полагаться на внутреннее представление, в listто время как maxпридется пройти общий протокол итератора.

Это делает выборку каждого элемента l.sortбыстрее, чем выборку каждого элемента max.

Я предполагаю, что если вы вместо этого будете использовать, sorted(a)вы получите результат медленнее, чем max(a).

Skyking
источник
5
Это предположение - всего лишь один шаг до того, чтобы стать более конкретным. Не подвергая сомнению ваши знания, просто такое дополнение тривиально для демонстрации тем, кто его не знает.
Reti43
Вы правы, что sorted(a)медленнее max(a). Неудивительно, что это примерно такая же скорость, как a.sort(), но ваше предположение относительно причины, почему нет - это потому, что OP допустил ошибку в своем тестировании, как указано в принятом ответе.
martineau
Дело в том, что существует вероятность того, что общий протокол итератора имеет достаточно накладных расходов, чтобы компенсировать log(n)фактор сложности. То есть O(n)алгоритм гарантированно будет быстрее, чем O(nlogn)алгоритм для достаточно больших n(например, потому что время для каждой операции может различаться между алгоритмами - nlognбыстрые шаги могут быть быстрее, чем nмедленные шаги). Где именно в данном случае не учитывалось безубыточность (но следует помнить, что этот log nфактор не является очень большим фактором для малых n).
skyking