QuickSort

def quicksort(a):
    random.shuffle(a)
    quicksort(a, 0, len(a))

def quicksort(a, l, r):
    if l < r:
        return
    # выбираем опорный элемент x
    px = pivot(a, l, r)
    # разбиваем все элементы массива
    # на куски меньше x и больше x
    m = partition(a, l, r, px)
    quicksort(a, l, m)
    quicksort(a, m + 1, r)

def partition(a, l, r, px):
    # время работы O(n)
    a[px], a[r] = a[r], a[px]
    m = l
    for p in range(l, r):
        if a[p] < a[r]:
            a[p], a[m] = a[m], a[p]
            m += 1
    a[m], a[r] = a[r], a[m]
    # m -- индекс опрного элемента после разбиения
    return m

def pivot(a, l, r):
    # дубовый выбор опорного элемента
    return l
    # среднее
    return (l + r - 1) / 2
    # крутой вариант, даст элемент из середины отсортированного массива
    # если медиана считается за O(n), то вообще заебись
    # но тогда у сортировки получается большая, так что нафиг
    # *как-то противоречиво получилось
    return median(a, l, r)
    # тоже неплохо
    return median(a[l], a[(l + r) / 2], a[r])
    # основной на практике вариант
    return random.choice(a[l:r])

Худший случай: В худшем случае массив будет разбиваться на куски размера $1$ и $n - 1$. Это осуществляется, если исходный массив отсортирован. В таком случае алгоритм будет работать $T(n) = T(1) + T(n - 1) + O(n) = \Theta(n^2)$.

Средний случай: В среднем случае происходит неплохое разбиение, примерно на равные куски. Время работы $T(n) = T(\frac n4) + T(\frac {3n} 4) + \Theta(n) \leq 2T(\frac {3n} 4) + O(n) = O(n \log n)$

"Хорошее" разбиение: 1. $T(n) = T(n - 1000) + T(1000) + O(n)$ -- плохое в ассимптотическом смысле 2. $T(n) = T(0.99 n) + T(0.01 n) + O(n)$ -- хорошее, т.к. коэффициент влияет лишь на основание логарифма

Вероятностные алгоритмы

  1. Расширим модель, добавив функцию rand(), возвращающую случайное число
  2. Добавляем к машине Тьюринга еще одну входную ленту, на которой записаны случайные биты. A(x, r), x -- входные данные, r -- случайные данные, A -- обычный детерминированный алгоритм

Есть два типа вероятностных алгоримов: 1. "Лас-Вегас" -- алгоритм дает точное решение, но иногда работает долго 2. "Монте-Карло" -- алгоритм всегда работает быстро, но дает только приближенное решение

QuickSort -- "Лас-Вегас" алгоритм.

Анализ QuickSort:

Предпологаем, что все элементы различны.

Мат. ожидание средней глубины рекурсии: Пусть хорошее разбиение $[\frac 14; \frac 34]$ $P[\#\text{разбиение хорошее}] = \frac 12$ Вызываем pivot до тех пор, пока разбиение не станет хорошим. $E[\#\text{число разбиений, пока не получим хорошее}] = 1\cdot\frac 12 + 2 \cdot\frac1 4 + 3 \cdot\frac1 8 + ... = 2$ $E[\#] = p\cdot 1 + (1 - p)[1+ E[\#]] \Rightarrow E[\#]=\frac 1p$

Если не модифицировать разбиение, то средний размер куска дерева, в котором есть плохое разбиение и следующее за ним хорошее $=2$.

$E[\#\text{глубины рекурсии самой левой ветви}] \leq E[\#\text{глубины, на которой массив уменьшился в два раза}] + E[\#\text{глубины для}\frac n2]$ $R(n) \leq 2 + R(\frac n2), \ R(1) = 1 \Rightarrow R(n) \leq O(\log n)$. Т.о. средняя глубина рекурсии $O(\log n)$

Сложность вероятностного алоритма:

Сложность -- это мат.ожидание количества операций. Это называется "сложность в среднем".

Сложность QuickSort: $E[\#\text{количество сравнений}] = \sum\limits_{i=1}^{n-1}\sum\limits_{j=i+1}^n 1\cdot P[\text{сравним i и j}] = \sum\limits_{i=1}^{n-1}\sum\limits_{j=i+1}^n \frac 2 {j - i + 1}=$ $= \sum\limits_{i=1}^{n-1}\sum\limits_{k=1}^{n-i} \frac 2 {k+1} \leq \sum\limits_{i=1}^{n-1} \ln n = O(n \log n)$

Замечание: тот же анализ рабтает для детерменированного pivot (например, первый элемент) и равномерного распределения на входах.

Варианты QuickSort: 1. IntroSort: запускает QuickSort и следит за глубиной рекурсии. Если глубина больше $C\log n$, то запускаем, например, HeapSort, на всем массиве. 2. QuickSort3: в partition делим массив на три части -- меньше опорного, равные ему, большие его. 3. QuickSortStable: partition будет рекурсивный и использовать циклические сдвиги. $O(n\log^2 n)$. 4. QuickSortRec: гарантирует глубину рекурсии $\Theta(\log n)$.

def quicksort_rec(a, l, r)):
    while r - l > 0:
        px = pivot(a, l, r)
        m = partition(a, l, r, px)
        if m - l > (r - l) // 2:
            quicksort_rec(a, m + 1, r)
            r = m - 1
        else:
            quicksort_rec(a, l, m)
            l = m + 1