Алгоритмическая сложность

Вычислительный процесс алгоритма
последовательность шагов, пройденная при исполнении алгоритма для некоторых входных данных.
Временна́я сложность алгоритма
время \(T\), необходимое для завершения вычислительного процесса алгоритма для некоторых входных данных.
  • \(T=k t\)
  • \(k\) – число элементарных действий. Свойство самого алгоритма
  • \(t\) – среднее время выполнения элементарного действия. Свойство вычислительного устройства
  • Обычно временна́я сложность оценивается с точностью до константного множителя
Порядок роста
положительно-определенная функция \(g(x)\) имеет порядок роста \(f(x)\) (записывается \(g(x)=O(f(x))\)), если \(\exists c>0, x_0 : \: \forall x>x_0, \, g(x) \leq c f(x)\).
  • Сложность сохранения в массив \(O(n)\)
  • Сложность умножения матриц \(O(n^3)\)
Пространственная сложность алгоритма
количество дополнительной памяти \(S\), которое алгоритм требует для работы. Память \(D\), необходимая для хранения входных данных, не включается в \(S\).
тоже оценивается порядком роста

Классы сложности алгоритмов

Классы сложности
категории, которые имеют схожую (в смысле порядка роста) сложность
  • DTIME – Машина Тьюринга находит решение задачи за конечное время (количество шагов). Часто уточняется асимптотика, \(\mathrm{DTIME}(f(n))\).
  • DSPACE – Машина Тьюринга находит решение задачи, используя конечное количество дополнительной памяти (ячеек). Часто уточняется асимптотика, \(\mathrm{DSPACE}(f(n))\).
  • \(\mathrm{P}=\mathrm{DTIME}(n^k)\)
  • \(\mathrm{EXPTIME}=\mathrm{DTIME}(2^{n^k})\).
  • \(\mathrm{L}=\mathrm{DSPACE}(\log n)\).
  • \(\mathrm{PSPACE}=\mathrm{DSPACE}(n^k)\).
  • \(\mathrm{EXPSPACE}=\mathrm{DSPACE}(2^{n^k})\).
  • Теоретические классы \(NTIME\), \(NSPACE\) и соответственно \(NL\), \(NP\), \(NPSPACE\), и т.д.
  • Основаны на недетерминированной машине Тьюринга.
  • Отличие НМТ от обычной МТ – для каждого из состояний может быть несколько возможных действий. При этом НМТ выбирает всегда вариант, приводящий к решению за минимальное число шагов.

Известные отношения классов:

  • \(\mathrm P \subseteq \mathrm{NP} \subseteq \mathrm{PSPACE} \subseteq \mathrm{EXPTIME} \subseteq\) \(\subseteq \mathrm{NEXPTIME} \subseteq \mathrm{EXPSPACE}\)
  • \(\mathrm {P} \neq \mathrm{EXPTIME}\)
  • \(\mathrm {NP} \neq \mathrm{NEXPTIME}\)
  • \(\mathrm {PSPACE} \neq \mathrm{EXPSPACE}\)

Также известно, что если \(\mathrm{P} = \mathrm{NP}\), то \(\mathrm{EXPTIME} = \mathrm{NEXPTIME}\).

Вопрос равенства P и NP – один из главных нерешенных вопросов современной информатики.

Примеры алгоритмов

Возведение в целую степень

  1. В начале результат равен \(x\).
  2. Записать \(n\) в двоичной системе счисления
  3. Заменить в этой записи каждую из \(1\) парой букв КХ, а каждый \(0\) – буквой К
  4. Вычеркнуть крайнюю левую пару КХ
  5. Читая полученную строку слева направо, встречая букву К возвести результат в квадрат, а встречая букву X – умножить результат на \(x\).
  • число операций умножения равно количеству цифр в двоичном представлении \(n\) в лучшем случае, и \(2(n-1)\) в худшем случае
  • Временная сложность \(T(n) = O(\log n)\).
  • Дополнительной памяти практически не требуется (если вместо записи \(KX\) сразу применять операции), и она не зависит от входных данных
  • Пространственная сложность \(S(n) = O(1)\).
  • “Наивная” реализация – \(O(n)\)

Умножение целых

  • Первый множитель последовательно умножается на два
  • второй – делится нацело на 2
  • Результаты записываются в два столбика, пока во втором не получится 1.
  • Результат умножения – сумма чисел первого столбика, напротив которых стоят нечётные числа во втором столбике.
  • \(2 \log_2 n\) операций сдвига, где \(n\) – меньшее из двух чисел
  • В худшем случае \(\log_2 n - 1\) операций сложения
  • В любом случае, временная сложность \(T(n) = O(\log n)\).
  • Для эффективной реализации алгоритма (если сразу суммировать числа), дополнительной памяти практически не требуется, и она не зависит от входных данных, поэтому \(S(n) = O(1)\)

Пример

\(23\times 43\)

43 23
86 11
172 5
344 2
688 1
  • Результат \(43+86+172+688 = 989\)
  • 10 операций сдвига и 4 операции сложения. \(\log_2(23) \approx 4.52\).

Алгоритмы поиска

Линейный поиск

Имеется массив \(a\) размера \(n\) (индексация с 1), требуется найти элемент массива равный \(P\).

  1. Установить \(k:=1\)
  2. Если \(k<n\), перейти к шагу 3, иначе алгоритм завершён неудачно
  3. Если \(a_k = P\), вывести \(k\), алгоритм завершён удачно
  4. Иначе, если \(k<n\), \(k:=k+1,\) перейти к шагу 2

  • сложность \(O(n)\) операций сравнения.

Поиск минимального элемента

Имеется массив \(a\) размера \(n\) (индексация с 1), требуется найти минимальный элемент.

  1. Установить \(k:=1\), \(μ:=a_1\)
  2. Если \(k<n\), перейти к шагу 3, иначе, вывести \(μ\), алгоритм завершён
  3. \(k:=k+1,\)
  4. Если \(a_k < μ\), \(μ:=a_k\)
  5. перейти к шагу 2

  • Сложность алгоритма \(O(n)\)

Бинарный поиск в упорядоченном массиве

Если массив упорядочен, линейный поиск неэффективен

Постановка
Имеется упорядоченный по возрастанию массив \(a\) размера \(n\) (индексация с 1), найти индекс элемента равного \(P\)
  1. Установить \(k:=1\), \(m:=n\)
  2. Если \(m<k\), алгоритм завершён неудачно
  3. \(p:=\floor{(n+k)/2}\)
  4. Если \(a_p < P\), то \(k:=p+1\), если \(a_p > P\), то \(m:=p-1\); иначе, \(a_p=P\), вывести \(p\), алгоритм завершён удачно
  5. перейти к шагу 2

Сложность алгоритма \(O(\log n)\)

Алгоритмы сортировки

Общая постановка: упорядочить произвольный массив \(n\) элементов по возрастанию (точнее, неубыванию). Полагается, что для элементов корректно определены операции \(<\), \(>\) и \(=\).

Простые сортировки

Сортировка “пузырьком”

Идея:

for m from n-1 to 1
  for j from 1 to m
    if a[j] > a[j+1]
      swap(a[j], a[j+1])
    end if
  end for j
end for m

Оптимизация: раннее завершение если массив не отсортирован

for m from n-1 to 1
  swapped = false
  for j from 1 to m
    if a[j] > a[j+1]
      swapped = true
      swap(a[j], a[j+1])
    end if
  end for j
  if swapped
    break
  end if
end for m
  • в лучшем случае \(n-1\) операций сравнения (если массив отсортирован).
  • В худшем случае \(n-1+n-2+n-3+\ldots\) \(= n(n-1)/2\) \(= O(n^2).\)
  • Максимально \(n(n-1)/2 = O(n^2)\) обменов.

Сортировка выбором

for i from 1 to n - 1
  min := i

  for j from i + 1 to n
     if a[j] < a[min]
        min := j
     end if
  end for j

  if indexMin ≠ i
     swap(a[min], a[i])
  end if
end for i

Сложность: \(n(n-1)/2 = O(n^2)\) операций сравнения. Не более \(n-1 = O(n)\) обменов.

Сортировка вставками

for i from 2 to n
    x := a[i]
    j := i
    while (j > 1 and a[j-1] > x)
        a[j] := a[j-1]
        j := j - 1
    end while
    a[j] := x
end for i
  • сложность по сравнениям \(1+2+\ldots+n-1\) \(=n(n-1)/2\) \(= O(n^2)\)
  • сложность по присваиваниям \(O(n^2)\).
  • сложность по присваиваниям можно улучшить до \(O(n)\)
  • сложность по сравнениям можно улучшить до \(O(n\log n)\)
  • скомбинировать – нельзя, худшая сложность всегда \(O(n^2)\)

Сортировка слиянием

algorithm merge_sort(a[n])
  if n ≤ 1
    return a

  mid := n/2

  left := merge_sort(a[1..mid])
  right := merge_sort(a[mid+1..n])

  return merge(left, right)
end algorithm merge_sort
algorithm merge(a[n], b[m])
  result := new array[n+m]
  i := 1
  j := 1
  k := 1
  while (i ≤ n and j ≤ m)
    if a[i] < b[j]
      result[k] := a[i]
      i := i+1
    else
      result[k] := b[j]
      j := j+1
    end if
    k := k+1
  end while
  while (i ≤ n)
    result[k] := a[i]
    k := k+1
    i := i+1
  end while
  while (j ≤ m)
    result[k] := b[j]
    k := k+1
    j := j+1
  end while
  return result
end algorithm merge
  • \(t(n) = 2t(n/2)+c\cdot n\)
  • \(t(1) = O(1) = c\)

Тогда:

\(t(n) = 2t(n/2)+c\cdot n\)

\(t(n) = 2(2t(n/2^2)+c\cdot n/2)+c\cdot n\)

\(t(n) = 2^2 t(n/2^2)+2c\cdot n\)

\(t(n) = 2^2 (2t(n/2^3)+c\cdot n/4)+2c\cdot n\)

\(t(n) = 2^3t(n/2^3)+3c\cdot n\)

\(\ldots\)

\(t(n) = c 2^{\log_2 n} + c\cdot n \log_2 n\)

\(t(n) = c n + c\cdot n\log_2 n\)

\(t(n) = O(n\log n)\)

  • Можно показать, что сортировка, основанная на бинарном сравнении, не может иметь сложность ниже \(O(n\log n).\)

Сортировка подсчётом

  • Возможно ли производить сортировку быстрее, чем за \(\mathcal O(n\log n)\)?
  • За шаг алгоритма нужно получать больше информации, чем даёт сравнение.
  • Простейший вариант – сортировка подсчётом.
  • Рассмотрим \(\brace{x_i},\) \(N=|\brace{x_i}|\)
  • \(0 \le x_i < \overline x\)
  • \(x_i \in \mathbb Z\)
  • Построим \(\brace{c_j},\)
  • \(c_j = \underset{\brace{x_i}}{\mathrm{count}}(j)\)
  • Сложность построения \(\brace{c_j}\)\(\mathcal O(N).\)
  • Из \(\brace{c_j}\) построим отсортированный массив \(\brace{x_k}\)
  • Сложность построения \(\brace{x_k}\)\(\mathcal O(\overline x).\)
  • Сложность алгоритма – \(\mathcal O(N+\overline x),\)
  • \(\mathcal O(N)\) если \(\overline x\) – константа.
  • Осмысленно только если \(\overline x \ll N\)
  • пространственная сложность \(\mathcal O(\overline x),\)
  • при \(\overline x = 2^{64},\) – по меньшей мере 18 эксабайт (18·10¹⁸ байт, 18 млн терабайт)


Поразрядная сортировка (radix sort)

  • основана на сортировке подсчётом
  • для сортировки чисел в \(P\)-ичной системе счисления, достаточно поразрядной сортировки
  • сортировку подсчётом применяется к каждому разряду последовательно
  • сложность примерно \(\mathcal O(N\log_P \overline x)=\mathcal O(N).\)

K – число \(P\)-ичных разрядов в числе.

algorithm radix_sort(a[N])
  for r from 1 to K
    for j from 1 to P-1
      c[j] := 0
    end for i
    for i from 1 to N
      x := r-тый разряд a[i]
      c[x] := c[x] + 1
    end for i
    for j from 1 to P-1
      c[j] := c[j-1] + c[j]
    end for i
    for i from N to 1
      x := r-тый разряд a[i]
      c[x] := c[x] - 1
      b[c[x]] := a[i]
    end for i
    a := b
  end for r
end algorithm radix_sort
  • Сложность \(\mathcal O(K\cdot(P+N)) = \mathcal O((P+N)\ceil{\log_P \overline x}).\)
  • Если \(\overline x\) и \(P\) константы, сложность \(\mathcal O(N)\)
  • осмысленно если \(N\gg P\) и \(\overline x \ll N\)
  • пространственная сложность \(\mathcal O(N+P)\)

Часто в качестве \(P\) выбирается степень двойки, например \(2^8\), \(2^{16}\). В таком случае сводится к битовым операциям.