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

Представим два алгоритма, которые решают одну и ту же задачу. На десяти элементах оба заканчивают работу почти мгновенно. На тысяче – тоже. Но что произойдёт, если элементов станет миллион?

Именно здесь между алгоритмами может появиться огромная разница. Поэтому после того, как мы убедились, что алгоритм работает правильно, возникает следующий вопрос: сколько работы ему приходится выполнять?

Почему нельзя просто измерить секунды

Кажется, что самый простой способ сравнить два алгоритма – запустить их и посмотреть на время. Например:

АлгоритмВремя работы
A0.03 секунды
B0.08 секунды

Можно решить, что алгоритм A лучше. Но такое сравнение не очень надёжно. Результат зависит от множества вещей:

  • мощности компьютера;
  • языка программирования;
  • реализации;
  • нагрузки на систему;
  • объёма входных данных.

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

Размер входных данных

Посмотрим на простой поиск. Нужно найти число 9:

[4, 7, 2, 9, 5]

Алгоритм проверяет элементы один за другим:

searchпсевдокод
function search(numbers, target):    for number in numbers:        if number == target:            return true    return false

Количество работы здесь зависит от того, сколько элементов находится в массиве. Для размера входных данных обычно используют обозначение n. Если в массиве пять элементов, то n = 5. Если миллион – n = 1 000 000.

То есть n – это не какое-то специальное значение алгоритма. Это просто удобное обозначение размера входных данных.

Чем больше данных, тем больше работы

Рассмотрим поиск числа, которого в массиве нет:

numbers = [4, 7, 2, 9, 5]target = 10

Чтобы убедиться, что 10 отсутствует, алгоритму придётся проверить каждый элемент:

Поиск числа, которого нет, – по шагам
function search(numbers, target):
for number in numbers:
if number == target:
return true
return false
search([4, 7, 2, 9, 5], 10)
Ищем 10 в массиве [4, 7, 2, 9, 5].
4071229354
Проверок0

Если в массиве будет 100 элементов, в таком случае потребуется до 100 проверок. Если n = 1 000 000, проверок может быть 1 000 000.

Количество работы растёт вместе с размером входных данных.

Один алгоритм может выполнять разное количество работы

Теперь поместим искомое число 9 в начало:

[9, 4, 7, 2, 5]

Алгоритм проверит первый элемент – 9 == 9 – и сразу закончит работу. Получается всего одна проверка. Но если 9 находится в конце:

[4, 7, 2, 5, 9]

придётся пройти через весь массив. То есть один и тот же алгоритм может работать по-разному даже при одинаковом размере входных данных. Для массива из пяти элементов:

СлучайСколько проверок
Лучший случай1 проверка
Худший случай5 проверок

Если элементов n:

СлучайСколько проверок
Лучший случай1 проверка
Худший случайn проверок

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

Считаем не каждую инструкцию

Посмотрим ещё раз на алгоритм:

searchпсевдокод
function search(numbers, target):    for number in numbers:        if number == target:            return true    return false

Внутри происходят разные действия:

  • переход к следующему элементу;
  • сравнение;
  • проверка условия;
  • возможно, возврат результата.

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

В этом алгоритме таким действием можно считать сравнение number == target. В худшем случае оно выполнится примерно n раз. Именно характер роста количества операций будет интересовать нас дальше.

Сравним два способа поиска

Есть отсортированный массив, в котором нужно найти 37:

[3, 7, 12, 24, 31, 37, 42]

Первый способ – проверять элементы по очереди. В худшем случае придётся пройти через весь массив: если элементов n = 1 000, проверок может быть около 1 000.

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

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

Что значит «масштабируется»

Допустим, количество данных увеличилось в десять раз: было n = 100, стало n = 1 000.

Для одного алгоритма количество работы тоже может увеличиться примерно в десять раз. У другого – всего на несколько дополнительных шагов. А у третьего – вырасти в сто раз.

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

Почему маленькие примеры могут обманывать

Допустим, два алгоритма работают с десятью элементами. Первому требуется 10 условных операций, второму – 100. Разница небольшая, и оба выполнятся почти мгновенно.

Но если элементов станет 100 000, одному алгоритму может понадобиться около 100 000 операций, а другому – 10 000 000 000.

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

Нас интересует рост, а не точное число

Представим два алгоритма. Первый выполняет примерно 2n операций, второй – n. Второй действительно делает меньше работы. Но у обоих есть важное общее свойство: если размер входных данных увеличить примерно в два раза, количество работы тоже увеличится примерно в два раза.

Теперь рассмотрим другой алгоритм – с n * n операций. Если n увеличится в два раза, работы станет уже примерно в четыре раза больше.

Нас интересует, как растёт количество работы при увеличении входных данных

Время и память

Количество операций – не единственная характеристика алгоритма. Иногда более быстрый способ требует хранить дополнительные данные.

Например, один алгоритм может постоянно использовать только несколько переменных – left, right, mid. А другому может понадобиться создать дополнительный массив почти такого же размера, как исходный. Поэтому при анализе алгоритмов обычно рассматривают две вещи:

  1. Временная сложность

    Как растёт количество выполняемой работы.

  2. Пространственная сложность

    Как растёт количество дополнительной памяти.

Пока мы сосредоточимся на времени.

От количества операций к сложности

Теперь у нас есть основная идея. Мы не спрашиваем «сколько миллисекунд занял алгоритм на моём компьютере?». Вместо этого спрашиваем: как меняется количество его работы, когда увеличивается размер входных данных?

Например, при n = 10, n = 100, n = 1 000 и n = 1 000 000. Некоторые алгоритмы почти не почувствуют такой рост. Другие станут заметно медленнее. А некоторые очень быстро станут практически непригодными.

Чтобы коротко описывать такое поведение, используют специальную запись – Big O.

// теперь вы знаете

  1. Скорость алгоритма нельзя надёжно оценивать только в секундах: они зависят от компьютера, языка, реализации и нагрузки
  2. Размер входных данных обычно обозначают n
  3. Количество работы зависит от размера входных данных: чтобы убедиться, что числа нет среди n элементов, нужно n проверок
  4. Один алгоритм выполняет разное количество операций на разных входных данных: лучший случай – 1 проверка, худший – n
  5. Считают не каждую инструкцию, а основное действие, количество которого растёт вместе с входом
  6. Разные алгоритмы масштабируются по-разному: за пять шагов один доходит с 32 до 27 элементов, другой – до 1
  7. Маленькие примеры скрывают разницу: 10 и 100 операций близки, 100 000 и 10 000 000 000 – нет
  8. Временная сложность описывает рост работы, пространственная – рост дополнительной памяти

Проверьте себя

Почему обычно нельзя сравнивать алгоритмы только по времени выполнения в секундах?

Что обычно означает n при анализе алгоритма?

Какой случай потребует от этого алгоритма больше всего проверок?

function search(numbers, target):
for number in numbers:
if number == target:
return true
return false

Выберите все правильные утверждения.

Выбери все верные варианты

Алгоритм выполняет примерно n действий. Если размер входных данных увеличить с 100 до 1 000, примерно во сколько раз увеличится количество работы?

Алгоритм выполняет примерно n * n действий. Если увеличить n в два раза, примерно во сколько раз увеличится количество работы?

Два алгоритма на массиве из десяти элементов работают почти одинаково быстро. Можно ли только по этому результату сделать вывод, что они будут одинаково хорошо работать и на миллионе элементов?