<?php
declare(strict_types=1);
/** @param list<int> $arr */
function quickSort(array &$arr, int $low = 0, ?int $high = null): void
{
if ($high === null) {
$high = count($arr) - 1;
}
if ($low < $high) {
$pivotIdx = partition($arr, $low, $high);
quickSort($arr, $low, $pivotIdx - 1);
quickSort($arr, $pivotIdx + 1, $high);
}
}
function partition(array &$arr, int $low, int $high): int
{
$pivot = $arr[$high]; // Take last element as pivot
$i = $low - 1; // Boundary of "small" elements
for ($j = $low; $j < $high; $j++) {
if ($arr[$j] <= $pivot) {
$i++;
[$arr[$i], $arr[$j]] = [$arr[$j], $arr[$i]];
}
}
[$arr[$i + 1], $arr[$high]] = [$arr[$high], $arr[$i + 1]];
return $i + 1;
}
func quickSort(arr []int, low, high int) {
if low < high {
pivotIdx := partition(arr, low, high)
quickSort(arr, low, pivotIdx-1)
quickSort(arr, pivotIdx+1, high)
}
}
func partition(arr []int, low, high int) int {
pivot := arr[high] // Take last element as pivot
i := low - 1 // Boundary of "small" elements
for j := low; j < high; j++ {
if arr[j] <= pivot {
i++
arr[i], arr[j] = arr[j], arr[i]
}
}
arr[i+1], arr[high] = arr[high], arr[i+1]
return i + 1
}
// In production code you would call Array.Sort(arr) (introsort).
// This implementation is for learning the algorithm itself.
static void QuickSort(int[] arr, int low, int high)
{
if (low < high)
{
int pivotIdx = Partition(arr, low, high);
QuickSort(arr, low, pivotIdx - 1);
QuickSort(arr, pivotIdx + 1, high);
}
}
static int Partition(int[] arr, int low, int high)
{
int pivot = arr[high]; // Take last element as pivot
int i = low - 1; // Boundary of "small" elements
for (int j = low; j < high; j++)
{
if (arr[j] <= pivot)
{
i++;
(arr[i], arr[j]) = (arr[j], arr[i]);
}
}
(arr[i + 1], arr[high]) = (arr[high], arr[i + 1]);
return i + 1;
}
# In production code you would call arr.sort() / sorted(arr) (Timsort).
# This implementation is for learning the algorithm itself.
def quick_sort(arr: list[int], low: int = 0, high: int | None = None) -> None:
if high is None:
high = len(arr) - 1
if low < high:
pivot_idx = partition(arr, low, high)
quick_sort(arr, low, pivot_idx - 1)
quick_sort(arr, pivot_idx + 1, high)
def partition(arr: list[int], low: int, high: int) -> int:
pivot = arr[high] # Take last element as pivot
i = low - 1 # Boundary of "small" elements
for j in range(low, high):
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i + 1], arr[high] = arr[high], arr[i + 1]
return i + 1
<?php
declare(strict_types=1);
function hoarePartition(array &$arr, int $low, int $high): int
{
$pivot = $arr[$low];
$i = $low - 1;
$j = $high + 1;
while (true) {
do {
$i++;
} while ($arr[$i] < $pivot);
do {
$j--;
} while ($arr[$j] > $pivot);
if ($i >= $j) {
return $j;
}
[$arr[$i], $arr[$j]] = [$arr[$j], $arr[$i]];
}
}
func hoarePartition(arr []int, low, high int) int {
pivot := arr[low]
i := low - 1
j := high + 1
for {
for {
i++
if arr[i] >= pivot {
break
}
}
for {
j--
if arr[j] <= pivot {
break
}
}
if i >= j {
return j
}
arr[i], arr[j] = arr[j], arr[i]
}
}
static int HoarePartition(int[] arr, int low, int high)
{
int pivot = arr[low];
int i = low - 1;
int j = high + 1;
while (true)
{
do
{
i++;
} while (arr[i] < pivot);
do
{
j--;
} while (arr[j] > pivot);
if (i >= j)
{
return j;
}
(arr[i], arr[j]) = (arr[j], arr[i]);
}
}
def hoare_partition(arr: list[int], low: int, high: int) -> int:
pivot = arr[low]
i = low - 1
j = high + 1
while True:
# Python has no do-while, so the first step is done before the loop
i += 1
while arr[i] < pivot:
i += 1
j -= 1
while arr[j] > pivot:
j -= 1
if i >= j:
return j
arr[i], arr[j] = arr[j], arr[i]
## Выбор Pivot
Плохой выбор pivot приводит к O(n²). Стратегии:
<?php
declare(strict_types=1);
// 1. Last element (Lomuto) — bad for sorted arrays
$pivot = $arr[$high];
// 2. Random — good on average
$randIdx = random_int($low, $high);
[$arr[$randIdx], $arr[$high]] = [$arr[$high], $arr[$randIdx]];
$pivot = $arr[$high];
// 3. Median of three — good compromise
function medianOfThree(array &$arr, int $low, int $high): int
{
$mid = intdiv($low + $high, 2);
if ($arr[$low] > $arr[$mid]) {
[$arr[$low], $arr[$mid]] = [$arr[$mid], $arr[$low]];
}
if ($arr[$low] > $arr[$high]) {
[$arr[$low], $arr[$high]] = [$arr[$high], $arr[$low]];
}
if ($arr[$mid] > $arr[$high]) {
[$arr[$mid], $arr[$high]] = [$arr[$high], $arr[$mid]];
}
// Now $arr[$mid] is the median
[$arr[$mid], $arr[$high - 1]] = [$arr[$high - 1], $arr[$mid]];
return $arr[$high - 1];
}
import "math/rand"
// 1. Last element (Lomuto) — bad for sorted arrays
pivot := arr[high]
// 2. Random — good on average
randIdx := rand.Intn(high-low+1) + low
arr[randIdx], arr[high] = arr[high], arr[randIdx]
pivot = arr[high]
// 3. Median of three — good compromise
func medianOfThree(arr []int, low, high int) int {
mid := (low + high) / 2
if arr[low] > arr[mid] {
arr[low], arr[mid] = arr[mid], arr[low]
}
if arr[low] > arr[high] {
arr[low], arr[high] = arr[high], arr[low]
}
if arr[mid] > arr[high] {
arr[mid], arr[high] = arr[high], arr[mid]
}
// Now arr[mid] is the median
arr[mid], arr[high-1] = arr[high-1], arr[mid]
return arr[high-1]
}
// 1. Last element (Lomuto) — bad for sorted arrays
int pivot = arr[high];
// 2. Random — good on average
int randIdx = Random.Shared.Next(low, high + 1); // Upper bound is exclusive
(arr[randIdx], arr[high]) = (arr[high], arr[randIdx]);
pivot = arr[high];
// 3. Median of three — good compromise
static int MedianOfThree(int[] arr, int low, int high)
{
int mid = (low + high) / 2;
if (arr[low] > arr[mid])
{
(arr[low], arr[mid]) = (arr[mid], arr[low]);
}
if (arr[low] > arr[high])
{
(arr[low], arr[high]) = (arr[high], arr[low]);
}
if (arr[mid] > arr[high])
{
(arr[mid], arr[high]) = (arr[high], arr[mid]);
}
// Now arr[mid] is the median
(arr[mid], arr[high - 1]) = (arr[high - 1], arr[mid]);
return arr[high - 1];
}
import random
# 1. Last element (Lomuto) — bad for sorted arrays
pivot = arr[high]
# 2. Random — good on average
rand_idx = random.randint(low, high) # Both bounds are inclusive
arr[rand_idx], arr[high] = arr[high], arr[rand_idx]
pivot = arr[high]
# 3. Median of three — good compromise
def median_of_three(arr: list[int], low: int, high: int) -> int:
mid = (low + high) // 2
if arr[low] > arr[mid]:
arr[low], arr[mid] = arr[mid], arr[low]
if arr[low] > arr[high]:
arr[low], arr[high] = arr[high], arr[low]
if arr[mid] > arr[high]:
arr[mid], arr[high] = arr[high], arr[mid]
# Now arr[mid] is the median
arr[mid], arr[high - 1] = arr[high - 1], arr[mid]
return arr[high - 1]
## Worst Case
Отсортированный массив + pivot = последний:
[1, 2, 3, 4, 5], pivot = 5
Partition: [] [5] [1, 2, 3, 4]
0 элементов слева, n-1 справа
Дерево рекурсии: Сбалансированное:
n n
n-1 n/2 n/2
n-2 n/4 n/4 n/4 n/4
n-3 ...
...
Глубина n -> O(n²) Глубина log n -> O(n log n)
Сложность
Случай
Время
Когда
Лучший
O(n log n)
Pivot всегда в середине
Средний
O(n log n)
Случайный pivot
Худший
O(n²)
Pivot всегда min/max
Память: O(log n) для стека рекурсии (в среднем), O(n) в худшем.
Quick Select: k-й элемент за O(n)
Модификация Quick Sort для поиска k-го наименьшего элемента:
<?php
declare(strict_types=1);
/** Find k-th smallest element (1-indexed) */
function quickSelect(array &$arr, int $k): int
{
$k--; // 0-indexed
$select = function (int $low, int $high) use (&$select, &$arr, $k): int {
if ($low === $high) {
return $arr[$low];
}
$pivotIdx = partition($arr, $low, $high);
if ($pivotIdx === $k) {
return $arr[$k];
}
return $pivotIdx < $k
? $select($pivotIdx + 1, $high)
: $select($low, $pivotIdx - 1);
};
return $select(0, count($arr) - 1);
}
// [3, 2, 1, 5, 6, 4], k=2 -> 2 (second smallest)
// Find k-th smallest element (1-indexed)
func quickSelect(arr []int, k int) int {
k-- // 0-indexed
var sel func(low, high int) int
sel = func(low, high int) int {
if low == high {
return arr[low]
}
pivotIdx := partition(arr, low, high)
if pivotIdx == k {
return arr[k]
}
if pivotIdx < k {
return sel(pivotIdx+1, high)
}
return sel(low, pivotIdx-1)
}
return sel(0, len(arr)-1)
}
// [3, 2, 1, 5, 6, 4], k=2 -> 2 (second smallest)
// Find k-th smallest element (1-indexed)
static int QuickSelect(int[] arr, int k)
{
k--; // 0-indexed
int Select(int low, int high)
{
if (low == high)
{
return arr[low];
}
int pivotIdx = Partition(arr, low, high);
if (pivotIdx == k)
{
return arr[k];
}
return pivotIdx < k
? Select(pivotIdx + 1, high)
: Select(low, pivotIdx - 1);
}
return Select(0, arr.Length - 1);
}
// [3, 2, 1, 5, 6, 4], k=2 -> 2 (second smallest)
func optimizedQuickSort(arr []int, low, high int) {
for low < high {
// For small arrays — insertion sort
if high-low < 16 {
insertionSortRange(arr, low, high)
return
}
// Randomized pivot
randIdx := rand.Intn(high-low+1) + low
arr[randIdx], arr[high] = arr[high], arr[randIdx]
pivotIdx := partition(arr, low, high)
// Tail call optimization: recurse on smaller part
if pivotIdx-low < high-pivotIdx {
optimizedQuickSort(arr, low, pivotIdx-1)
low = pivotIdx + 1
} else {
optimizedQuickSort(arr, pivotIdx+1, high)
high = pivotIdx - 1
}
}
}
static void OptimizedQuickSort(int[] arr, int low, int high)
{
while (low < high)
{
// For small arrays — insertion sort
if (high - low < 16)
{
InsertionSortRange(arr, low, high);
return;
}
// Randomized pivot
int randIdx = Random.Shared.Next(low, high + 1);
(arr[randIdx], arr[high]) = (arr[high], arr[randIdx]);
int pivotIdx = Partition(arr, low, high);
// Tail call optimization: recurse on smaller part
if (pivotIdx - low < high - pivotIdx)
{
OptimizedQuickSort(arr, low, pivotIdx - 1);
low = pivotIdx + 1;
}
else
{
OptimizedQuickSort(arr, pivotIdx + 1, high);
high = pivotIdx - 1;
}
}
}
import random
def optimized_quick_sort(
arr: list[int], low: int = 0, high: int | None = None
) -> None:
if high is None:
high = len(arr) - 1
while low < high:
# For small arrays — insertion sort
if high - low < 16:
insertion_sort_range(arr, low, high)
return
# Randomized pivot
rand_idx = random.randint(low, high)
arr[rand_idx], arr[high] = arr[high], arr[rand_idx]
pivot_idx = partition(arr, low, high)
# Tail call optimization: recurse on smaller part
if pivot_idx - low < high - pivot_idx:
optimized_quick_sort(arr, low, pivot_idx - 1)
low = pivot_idx + 1
else:
optimized_quick_sort(arr, pivot_idx + 1, high)
high = pivot_idx - 1
## Quick Sort vs Merge Sort
Свойство
Quick Sort
Merge Sort
Среднее
O(n log n)
O(n log n)
Худшее
O(n²)
O(n log n)
Память
O(log n)
O(n)
Стабильность
Нет
Да
Cache
Отличный
Хуже
На практике
Обычно быстрее
Гарантированный
Запомни: Quick Sort на практике быстрее Merge Sort благодаря лучшей cache locality и меньшему потреблению памяти. Рандомизированный pivot делает worst case крайне маловероятным. Quick Select — O(n) для поиска k-го элемента. Современные std sort (Go, C++) используют introsort: quick sort + heap sort fallback при деградации.
Итоги
Partition: ключевая операция, размещает pivot на правильное место
O(n log n) в среднем, O(n²) в худшем
In-place, но нестабильна
Randomized pivot = почти гарантированный O(n log n)
Quick Select: k-й элемент за O(n)
На практике быстрее merge sort (cache-friendly)
Проверь себя
Зачем в оптимизированном Quick Sort переключаться на Insertion Sort для маленьких подмассивов?
Что делает partition в Quick Sort?
Почему Quick Sort на практике обычно быстрее Merge Sort, несмотря на одинаковую среднюю сложность O(n log n)?
Какова средняя сложность Quick Select для поиска k-го наименьшего элемента?
Какой массив является ХУДШИМ случаем для Quick Sort с pivot = последний элемент (Lomuto partition)?