Давайте начнем с некоторыми математическими фактами:
- Для положительного п, aⁿ = a⨯a⨯ ... ⨯an раз
- Для отрицательного n, aⁿ = ⅟a⁻ⁿ = ⅟ (a⨯a⨯ ... ⨯a). Это означает, что a не может быть равен нулю.
- Для n = 0, aⁿ = 1, даже если a равен нулю или отрицателен.
Итак, давайте начнем с положительного n случая и работаем оттуда.
Поскольку мы хотим, чтобы наше решение было рекурсивным, мы должны найти способ определить aⁿ на основе меньшего n и работать оттуда. Обычно люди думают о рекурсии - попытаться найти решение для n-1 и работать оттуда.
И в самом деле, так как это математически верно, что aⁿ = a⨯ (aⁿ⁻¹), наивный подход был бы очень похож на то, что вы создали:
public static int pow(int a, int n) {
if (n == 0) {
return 1;
}
return (a * pow(a,n-1));
}
Однако сложность этого является O (п). Зачем? Поскольку для n = 0 он не выполняет никаких умножений. При n = 1 оно одно умножение. При n = 2 он называет pow (a, 1), который, как мы знаем, является одним умножением и умножает его один раз, поэтому мы имеем два умножения. На каждом этапе рекурсии есть одно умножение, и есть n шагов. Так что это O (n).
Для того, чтобы сделать это O (log n), нам нужен каждый шаг, который необходимо применить к фракции n, а не только n-1. Здесь снова есть математический факт, который может нам помочь: a n₁ + n₂ = a n₁ ⨯a n₂.
Это означает, что мы можем вычислить aⁿ как п/2 ⨯a п/2.
Но что произойдет, если n нечетно? что-то вроде a⁹ будет 4.5 ⨯a 4.5. Но здесь речь идет о целых силах. Обработка фракций - это совсем другое дело. К счастью, мы можем просто сформулировать это как a⨯a⁴⨯a⁴.
Таким образом, для четного числа используют п/2 ⨯a п/2, и для нечетного числа, использовать a⨯ в ⨯a п/2 (целочисленное деление п/2 , давая нам 9/2 = 4).
public static int pow(int a, int n) {
if (n == 0) {
return 1;
}
if (n % 2 == 1) {
// Odd n
return a * pow(a, n/2) * pow(a, n/2);
} else {
// Even n
return pow(a, n/2) * pow(a, n/2);
}
}
Это действительно дает нам правильные результаты (для положительного n, то есть). Но на самом деле сложность здесь, опять же, O (n), а не O (log n). Зачем? Потому что мы дважды вычисляем полномочия.Это означает, что мы на самом деле называем это 4 раза на следующем уровне, 8 раз на следующем уровне и т. Д. Количество шагов рекурсии экспоненциально, поэтому это отменяется с предполагаемой экономией, которую мы сделали, разделив n на два.
Но на самом деле, лишь небольшая коррекция необходима:
public static int pow(int a, int n) {
if (n == 0) {
return 1;
}
int powerOfHalfN = pow(a, n/2);
if (n % 2 == 1) {
// Odd n
return a * powerOfHalfN * powerOfHalfN;
} else {
// Even n
return powerOfHalfN * powerOfHalfN;
}
}
В этой версии мы называем рекурсию только один раз. Таким образом, мы получаем, скажем, мощность 64, очень быстро через 32, 16, 8, 4, 2, 1 и делаем. Только одно или два умножения на каждом шаге, и есть только шесть шагов. Это O (log n).
Вывод из всего этого:
- Чтобы получить O (журнал N), нам нужно рекурсию, которая работает на фракции п на каждом шаге, а не только п - 1 или п - ничего.
- Но фракция является лишь частью истории. Нам нужно быть осторожным, чтобы не вызывать рекурсию более одного раза, поскольку использование нескольких рекурсивных вызовов за один шаг создает экспоненциальную сложность, которая отменяется с использованием доли n.
Наконец, мы готовы позаботиться об отрицательных числах. Нам просто нужно получить ответный ⅟a⁻ⁿ. Следует отметить две важные вещи:
- Не допускайте деления на ноль. То есть, если вы получили a = 0, вы не должны выполнять расчет. В Java мы выставляем исключение в таком случае. Наиболее подходящим готовым исключением является IllegalArgumentException. Это исключение RuntimeException, поэтому вам не нужно добавлять в свой метод предложение
throws
. Было бы хорошо, если бы вы либо поймали его, либо не допустили такой ситуации в вашем методе main
, когда читаете в аргументах.
- Вы больше не можете вернуть целое число (на самом деле мы должны использовать
long
, потому что мы запускаем целочисленное переполнение для довольно низких мощностей с int
) - потому что результат может быть дробным.
Таким образом, мы определяем метод так, чтобы он возвращался дважды. Это означает, что мы также должны исправить тип powerOfHalfN
. И вот результат:
public static double pow(int a, int n) {
if (n == 0) {
return 1.0;
}
if (n < 0) {
// Negative power.
if (a == 0) {
throw new IllegalArgumentException(
"It's impossible to raise 0 to the power of a negative number");
}
return 1/pow(a, -n);
} else {
// Positive power
double powerOfHalfN = pow(a, n/2);
if (n % 2 == 1) {
// Odd n
return a * powerOfHalfN * powerOfHalfN;
} else {
// Even n
return powerOfHalfN * powerOfHalfN;
}
}
}
Обратите внимание, что та часть, которая обрабатывает отрицательный п используется только на верхнем уровне рекурсии. Как только мы вызываем pow()
рекурсивно, он всегда с положительными числами, и знак не изменяется до тех пор, пока он не достигнет 0.
Это должно быть адекватным решением для вашего упражнения. Однако лично мне не нравится if
там в конце, так что вот еще одна версия. Можете ли вы сказать, почему это делается так же?
public static double pow(int a, int n) {
if (n == 0) {
return 1.0;
}
if (n < 0) {
// Negative power.
if (a == 0) {
throw new IllegalArgumentException(
"It's impossible to raise 0 to the power of a negative number");
}
return 1/pow(a, -n);
} else {
// Positive power
double powerOfHalfN = pow(a, n/2);
double[] factor = { 1, a };
return factor[n % 2] * powerOfHalfN * powerOfHalfN;
}
}
всякий раз, когда вам нужно только добавить нечто инициализировать сумму = 0, , но если вы хотите, чтобы умножать и добавить сумму = инициализировать 1 – Vihar
Почему вы используете 'float', когда вы сказали, чтобы использовать' int'? Что еще более важно - не только ваше использование «результата» неверно (из-за нуля), сложность вашей функции - O (n), а не O (log n).Вы должны уменьшить проблему до половины ее размера в рекурсии, чтобы получить O (log n). – RealSkeptic
Это 'O (n)' сложность, хотя она рекурсивна. Алгоритм 'O (logn)' всегда разделил бы мощность ('n' в этом случае) на 2, не уменьшаясь на 1. – Daniel