Является ли временная сложность этого метода постоянной?

Мне было поручено определить временную сложность этого метода, но я не уверен, почему она постоянна. Я думал, что общая временная сложность равна n2^n, потому что внешний цикл выполняется n раз, а внутренний цикл выполняется 2^k раз, потому что k умножается на коэффициент два при каждом запуске внешнего цикла. Однако, когда я тестировал метод с помощью System.nanoTime(), время, похоже, оставалось постоянным при увеличении размера данных. Я не уверен, прав я или нет, но если я ошибаюсь и метод имеет постоянную временную сложность, не могли бы вы объяснить, почему? Спасибо!

public static void f6(int n) {
    int k = 1, a = 0;
    for(int i = 0; i < n; i += 1) {
        for(int j = 0; j <= k * 2; j += 1) {
            a = i + j;
        }
        k = k * 2;
    }
}

Тестовый левый столбец: имя функции; средний столбец: количество предметов; правая колонка: затраченное время:

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

rzwitserloot 30.04.2024 23:18

Возможно, стоит посмотреть, что оптимизатор сделал с этим кодом. Поскольку возвращаемого значения нет, возможно, все это было заменено возвратом. Сделайте это int f6(int n) и измените внутренний цикл на a += i+j. В противном случае есть компиляторы, которые заметят, что a = i + 2^i при выходе из внутреннего цикла и в последний раз через цикл i=n-1. IOW они придут к выводу, что возврат n-1+k^(n-1) может быть вычислен за постоянное время, и внешний пользователь никогда не заметит разницы. Попробуйте синхронизировать его в режиме отладки, когда он выполняет код дословно, и вы можете получить более содержательные ответы.

Martin Brown 30.04.2024 23:33

Возможно, вас это заинтересует: stackoverflow.com/questions/504103/…

Old Dog Programmer 30.04.2024 23:35

@MartinBrown Оптимизаторы также, вероятно, смогут определить, что a всегда пишется, а не читается, а это означает, что его можно удалить; что оставляет внутренний цикл пустым, поэтому его можно удалить.

yshavit 30.04.2024 23:46

Если не компилятор, то среда выполнения также выполняет оптимизацию, если вы запускаете ее повторно и замечает это.

John Bayko 30.04.2024 23:58

@yshavit на самых агрессивных оптимизирующих компиляторах высшего класса сегодня вы должны, по крайней мере, распечатать возвращаемый результат тестируемой функции или сделать с ней что-то еще, что имеет побочные эффекты, чтобы быть уверенным, что она выполняется при полной глобальной оптимизации. Я обычно использую if (result ==42) printf("Surprise\n"); для этой цели. До сих пор ни один оптимизатор не разглядел этот обман.

Martin Brown 01.05.2024 09:59

@MartinBrown Конечно, но внутренний цикл ничего не печатает и не имеет каких-либо других очевидных побочных эффектов. И я предполагаю, что оптимизатор может это понять.

yshavit 01.05.2024 15:41

О, вообще-то, извини, ты просто предложил обходной путь к тому, что я сказал?

yshavit 01.05.2024 18:32
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
8
87
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы определили переменные a и k как целые числа и (по крайней мере, переменную k) нельзя увеличить до ^2 более 32 раз. Таким образом, JVM «знает», что уравнение k = k^2 имеет смысл только первый 31 раз и не будет выполнять чрезмерных итераций.

Более того, если вы не используете результат после расчета, JVM также может решить не выполнять расчет.

Итак, решение:

  1. замените int a,k на long a,k (учтите, что теперь функция имеет смысл только при n <= 64).
  2. Использовать результат (sysout будет достаточно)
Ответ принят как подходящий

Я думал, что общая временная сложность равна n2^n, потому что внешний цикл выполняется n раз, а внутренний цикл выполняется 2^k раз, потому что k умножается на коэффициент два при каждом запуске внешнего цикла.

Что ж, O(n2n), безусловно, является верхней границей асимптотической сложности метода. Ваш анализ верен, насколько это возможно. Но если вы посмотрите немного глубже, вы можете получить более плотный вариант, который по-прежнему имеет простую форму.

Запишите некоторые счетчики итераций внутреннего цикла:

1  2  4  8 ...

вплоть до 2n-1. Сумма всех этих чисел может быть выражена как Σi = 1…n 2i-1. Если вы еще не знаете результат этой суммы, то вам следует это выучить. И я позволю тебе это сделать.

Однако, когда я тестировал метод с помощью System.nanoTime(), время, похоже, оставалось постоянным при увеличении размера данных.

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

Предположим, например, что достаточно умный компилятор может понять, что весь метод можно свести к нескольким операциям (на самом деле константным) и выдать код, который просто делает это вместо фактического выполнения циклов. Я не думаю, что ваш компилятор достаточно умен для этого, но теоретически это возможно.

Будет ли сумма равна 2^n-1? В таком случае, почему это будет более жесткая граница, если ее можно упростить до 2^n?

Woojin Rho 01.05.2024 01:07

@WoojinRho, 1 * (2^n) - 1 — более жесткая граница, чем ваша n * (2^n), потому что коэффициент 1 в первом асимптотически меньше, чем соответствующий коэффициент n во втором. Однако было бы общепринято игнорировать - 1 и просто писать его как O(2^n) (а не как предложенный вами O(n2^n)). Возможно, вы не понимаете, что это охватывает все операции во всем гнезде циклов, а не только во внутреннем цикле.

John Bollinger 01.05.2024 14:18

Если вы посмотрите на значения k (который определяет, как часто выполняется внутренний цикл), вы найдете следующие значения:

1  2  4  8 ... 536870912  1073741824  -2147483648  0  0  0

Количество запусков внутреннего цикла ограничено и для достаточно больших значений n (при n = 31). После этого время выполнения этого метода увеличивается только линейно со значением n (поскольку внешний цикл по-прежнему выполняется от 32 до n).

Таким образом, для значений n от 1 до 31 он работает за O(2^n), для больших значений — за O(n).

Другие вопросы по теме

Какова точная временная сложность этого алгоритма?
Учитывая массив с максимальной кучей неизвестного размера, найдите размер кучи
Найдите недостающее значение в полном BST, заполненном каждым числом от 1 до 2^K, где K — количество уровней
Найти количество уникальных длительностей по списку длительностей и верхней границе
Какова временная сложность этого алгоритма с двумя массивами?
Почему встроенная функция sorted() работает медленнее для списка, содержащего числа по убыванию, если каждое число появляется дважды подряд?
Сортировка массива с повторяющимся элементом (по его частоте) за линейное время
Временная сложность функции math.isqrt() для целочисленного квадратного корня Python 3.8+
Временная сложность одновременной итерации
Временная сложность для рекурсивного двоичного поиска, который также печатает текущий подмассив