Кто-нибудь может дать "простой английский" интуитивное, но формальное объяснение того, что делает QuickSort n log n? По моему мнению, он должен сделать проход над n элементами, и он делает это log n раз... Я не уверен, как выразить это словами, почему он делает это log n раз.
Интуитивное объяснение того, почему QuickSort является n log n?
Ответ 1
Каждая операция разбиения выполняет операции O (n) (один проход по массиву). В среднем каждое разбиение разбивает массив на две части (которые суммируются до операций log n). Всего мы имеем операции O (n * log n).
т.е. в среднем log n операций разбиения, и каждое разбиение занимает O (n) операций.
Ответ 2
сложности
Quicksort начинается с разделения входных данных на две части: он выбирает значение "pivot" и разделяет входные данные на значения, меньшие, чем значение pivot, и на значения, превышающие значение pivot (и, конечно, любое значение, равное значению pivot, имеет конечно, перейдем к тому или иному, но для базового описания не имеет большого значения, к чему они приводят).
Поскольку входные данные (по определению) не отсортированы, для их разделения необходимо просмотреть каждый элемент входных данных, чтобы выполнить операцию O (N). После того, как он разделил входные данные в первый раз, он рекурсивно сортирует каждый из этих "кусков". Каждый из этих рекурсивных вызовов просматривает каждый из своих входов, поэтому между двумя вызовами он заканчивается посещением каждого входного значения (снова). Итак, на первом "уровне" разбиения у нас есть один вызов, который просматривает каждый элемент ввода. На втором уровне у нас есть два шага разделения, но между ними они (опять же) смотрят на каждый элемент ввода. Каждый последующий уровень имеет больше отдельных шагов разбиения, но в целом вызовы на каждом уровне проверяют все входные элементы.
Он продолжает разделять входные данные на более мелкие части, пока не достигнет некоторого нижнего предела размера раздела. Наименьшее, что могло бы быть, - это отдельный элемент в каждом разделе.
Идеальный чехол
В идеальном случае мы надеемся, что каждый шаг разделения разбивает входные данные пополам. "Половинки", вероятно, не будут в точности равны, но если мы хорошо выберем опору, они должны быть довольно близки. Для простоты математики, давайте предположим, идеальное разбиение, чтобы мы каждый раз получали точные половинки.
В этом случае число раз, которое мы можем разделить на две части, будет логарифмом числа оснований-2 от количества входов. Например, учитывая 128 входов, мы получаем размеры разделов 64, 32, 16, 8, 4, 2 и 1. Это 7 уровней разделения (и да log 2 (128) = 7).
Итак, у нас есть log (N) разделение "уровней", и каждый уровень должен посещать все N входов. Таким образом, log (N) уровней, умноженных на N операций на уровень, дает нам O (N log N) общей сложности.
Худший случай
Теперь давайте вернемся к тому предположению, что каждый уровень разделения будет "разбивать" входные данные ровно пополам. В зависимости от того, насколько удачный выбор элемента разбиения мы делаем, мы можем не получить точно равные половины. Так что худшее, что могло случиться? В худшем случае это стержень, который на самом деле является наименьшим или наибольшим элементом на входе. В этом случае мы делаем уровень разбиения O (N), но вместо того, чтобы получить две половинки одинакового размера, мы получили один раздел из одного элемента и один раздел из N-1 элементов. Если это происходит для каждого уровня разбиения, мы, очевидно, в конечном итоге выполняем O (N) уровней разбиения, прежде чем четное разбиение становится одним элементом.
Это дает технически правильную сложность big-O для быстрой сортировки (big-O официально относится к верхней границе сложности). Поскольку у нас есть O (N) уровней разбиения, и каждый уровень требует O (N) шагов, мы в конечном итоге получим O (N * N) (то есть O (N 2) сложность.
Практические реализации
На практике реальная реализация обычно останавливает разбиение до того, как оно фактически достигнет разделов одного элемента. В типичном случае, когда раздел содержит, скажем, 10 элементов или меньше, вы прекратите разделение и будете использовать что-то вроде сортировки вставкой (поскольку обычно это происходит быстрее для небольшого числа элементов).
Модифицированные алгоритмы
Совсем недавно были изобретены другие модификации быстрой сортировки (например, Introsort, PDQ Sort), которые предотвращают этот O (N 2) наихудший случай. Introsort делает это, отслеживая текущий уровень разделения, и когда/если он идет слишком глубоко, он переключится на сортировку кучи, которая медленнее, чем Quicksort для типичных входных данных, но гарантирует сложность O (N log N) для любых входов.
Сортировка PDQ добавляет к этому еще один поворот: поскольку сортировка кучи медленнее, она старается избегать переключения на сортировку кучи, если это возможно. К этому, если похоже, что она получает плохие сводные значения, она случайным образом перемешает некоторые входные данные перед выбором стержень. Затем, если (и только если) не удается получить достаточно лучшие сводные значения, он переключится на использование сортировки Heap.
Ответ 3
Ну, это не всегда n (log n). Это время исполнения, когда выбранная ось находится примерно посередине. В худшем случае, если вы выбрали наименьший или наибольший элемент в качестве оси, то время будет O (N ^ 2).
Чтобы визуализировать 'n log n', вы можете считать, что ось будет элементом, ближайшим к среднему значению всех элементов массива, который будет отсортирован. Это разделило бы массив на 2 части примерно такой же длины. В обоих случаях вы применяете процедуру быстрой сортировки.
Как и на каждом шаге, вы увеличиваете вдвое длину массива, вы будете делать это для log n (base 2) раз, пока не достигнете length = 1 i.e отсортированный массив из 1 элемента.
Ответ 4
Фактически вам нужно найти положение всех элементов N (ось поворота), но максимальное количество сравнений - это logN для каждого элемента (первый - N, второй опорный N/2,3rd N/4). предполагая, что ось вращения является медианным элементом)
Ответ 5
За логарифмами лежит ключевая интуиция:
Количество раз, которое вы можете разделить число n на константу до достижения 1, равно O (log n).
Другими словами, если вы видите среду выполнения, имеющую термин O (log n), есть хороший шанс, что вы найдете что-то, что постоянно уменьшается с постоянным коэффициентом.
В быстрой сортировке постоянным фактором является размер самого большого рекурсивного вызова на каждом уровне. Quicksort работает, выбирая сводную область, разбивая массив на два подмассива элементов, меньших, чем сводная, и элементов, больших, чем сводная, а затем рекурсивно сортируя каждый подмассив.
Если вы выбираете опору случайным образом, то есть 50% -ная вероятность того, что выбранный опорный пункт будет в середине 50% элементов, что означает, что есть 80% -ная вероятность, что больший из двух подмассивов будет не более 75%. размер оригинала. (Вы видите почему?)
Следовательно, хорошая интуиция, объясняющая, почему быстрая сортировка выполняется во время O (n log n), заключается в следующем: каждый слой в дереве рекурсии выполняет O (n), и, поскольку каждый рекурсивный вызов имеет хорошие шансы уменьшить размер массива по крайней мере, на 25%, мы ожидаем, что будет O (log n) слоев, прежде чем вы исчерпаете элементы для выброса из массива.
Это предполагает, конечно, что вы выбираете шарниры случайным образом. Многие реализации быстрой сортировки используют эвристику, чтобы попытаться получить хороший стержень без особой работы, и эти реализации, к сожалению, могут привести к плохому общему времени выполнения в худшем случае. Отличный ответ @Jerry Coffin на этот вопрос говорит о некоторых вариациях быстрой сортировки, которые гарантируют O (n log n) наихудшее поведение при переключении используемых алгоритмов сортировки, и что это отличное место для поиска дополнительной информации об этом.