Запись
O описывает ограничивающее поведение функции, наихудший случай для алгоритма. Обычно достаточно сравнить разные алгоритмы для одной и той же задачи, например, алгоритмы сортировки по этой функции.
Я бы сказал, что почти для всех алгоритмов (кроме алгоритмов O (1);)) всегда есть некоторые входные данные, которые заставляют алгоритм завершаться за меньшее время (или с меньшим потреблением памяти), чем обозначается описывающей нотацией O для этот алгоритм.
Предположим, у нас есть алгоритм подсчета, подобный этому:
private int counter(int n) {
int counter;
for (int i = 0; i < 2; i++) {
counter = 0;
for (int i = 0; i < n; i++) {
counter++;
}
}
return counter;
}
Рост линейный, поэтому обозначение O для этого счетчика O (n) (я смотрю только на шаги, а не на память). Вы могли бы поспорить, что, эй, мы считаем дважды, и вместо этого напишите O (2n). Правда. Вы даже можете написать O (2n + c), чтобы указать, что нам нужны дополнительные шаги (время) для создания и инициализации локальной переменной.
Вот улучшенная реализация, которая все еще является линейной (O (n)), но завершается значительно быстрее:
private int counter(int n) {
int counter =0;
for (int i = 0; i < n; i++) {
counter++;
}
return counter;
}
Оба могут быть описаны как O (n) для обозначения линейного роста. Этого может быть достаточно, например, чтобы сравнить эти реализации с O (n ^ 2) или O (1) реализацией этого счетчика. Но чтобы сравнить «линейные» версии A и B, мы должны быть более точными и определить первую как O (2n), а вторую как O (n). Теперь сравнение значений нотации с O дает ожидаемый результат: реализация B «лучше».