Сложный пример: биквадратное уравнение
Рассмотрим теперь более сложный случай. Пусть нам необходимо написать функцию, рассчитывающую минимальный из имеющихся корней биквадратного уравнения: ax4 + bx2 + c = 0. Данное уравнение решается путём замены y = x2, решения квадратного уравнения ay2 + by + c = 0 и последующего решения уравнения x2 = y с подставленными корнями квадратного уравнения y1 и y2. Попробуем сначала записать алгоритм решения задачи в виде последовательности действий:
- Если a равно 0, уравнение вырождается в bx2 + c = 0. Вырожденное уравнение:
- при b равном 0 не имеет решений (или имеет бесконечно много)
- при c / b > 0 также не имеет решений
- в противном случае минимальный корень — это
x = -sqrt(-c / b)
- Рассчитаем дискриминант
d = b2 - 4ac
. - Если d меньше 0, у квадратного уравнения нет решений, как и у биквадратного.
- В противном случае найдём корни квадратного уравнения
y1 = (-b + sqrt(d))/(2a)
иy2 = (-b - sqrt(d))/(2a)
. - Вычислим
y3 = Max(y1, y2)
. - Если y3 < 0, у уравнения
x2 = y3
нет решений. - В противном случае, минимальный корень биквадратного уравнения — это
x = -sqrt(y3)
.
Запишем теперь то же самое на Котлине. Для обозначения ситуации, когда решений нет, будем использовать специальную константу Double.NaN
, так называемое не-число. На практике она может получиться как результат некоторых некорректных действий с вещественными числами, например, после вычисления квадратного корня из -1.
fun minBiRoot(a: Double, b: Double, c: Double): Double { // 1: в главной ветке if выполняется НЕСКОЛЬКО операторов if (a == 0.0) { if (b == 0.0) return Double.NaN // ... и ничего больше не делать val bc = -c / b if (bc < 0.0) return Double.NaN // ... и ничего больше не делать return -sqrt(bc) // Дальше функция при a == 0.0 не идёт } val d = discriminant(a, b, c) // 2 if (d < 0.0) return Double.NaN // 3 // 4 val y1 = (-b + sqrt(d)) / (2 * a) val y2 = (-b - sqrt(d)) / (2 * a) val y3 = max(y1, y2) // 5 if (y3 < 0.0) return Double.NaN // 6 return -sqrt(y3) // 7 }
Данная реализация активно использует оператор return. Если в предыдущих примерах он использовался исключительно в конце функций, то в этом примере он встречается в теле функции многократно в конструкции вида if (something) return result
. Такая конструкция читается как «если что-то, результат функции равен тому-то (и дальше ничего делать не надо)». Заметьте, что в данном случае вторая часть оператора if — ветка else — отсутствует. Это эквивалентно записи if (something) return result else {}
, то есть в ветке «иначе» не делается ничего. В случае, если условие в if не выполнено, функция пропускает оператор return и выполняет оператор, следующий за оператором if.
Всегда ли может отсутствовать ветка else? Нет, не всегда. Это зависит от контекста, то есть конкретного варианта использования if..else. В примере вроде val x = if (condition) 1 else 2
исчезнование ветки else не позволит функции «понять», чему же должно быть равно значение x
, что приведёт к ошибке:
'if' must have both main and 'else' branches if used as an expression.
В переводе с английского — оператор if должен иметь как главную ветку, так и ветку else, если он используется как выражение. Два наиболее распространённых случая такого рода — val x = if …
или return if …
. В обоих случаях у if есть результат, который затем используется для записи в x
или для формирования результата функции.
Обратите также внимание на самый первый оператор if в minBiRoot
. Он выглядит как if (a == 0.0) { … }
с несколькими операторами в фигурных скобках. По умолчанию, if может иметь только один оператор как в главной ветке, так и в ветке else. Если в случае истинности или ложности условия необходимо выполнить несколько операторов, их следует заключить в фигурные скобки, образуя блок операторов. Блок операторов выполняется последовательно, так же, как и тело функции. Блок может содержать любые операторы, в том числе и другие операторы if.
Проверим нашу реализацию minBiRoot
с помощью тестовой функции. Для этого нам необходимо проверить различные случаи:
- a = b = 0, например 0x4 + 0x2 + 1 = 0 — корней нет.
- a = 0, c / b > 0, например 0x4 + 1x2 + 2 = 0 — корней нет.
- a = 0, c / b < 0, например 0x4 + 1x2 — 4 = 0 — корни есть, в данном случае минимальный из них -2.
- d < 0, например 1x4 -2x2 + 4 = 0 — корней нет.
- d > 0, но оба корня y отрицательны, например 1x4 + 3x2 + 2 = 0, y1 = -2, y2 = -1, корней нет.
- d > 0, хотя бы один корень y положителен, например 1x4 — 3x2 + 2 = 0, y1 = 1, y2 = 2, минимальный корень -1.41.
Тестовая функция может выглядеть так:
@Test fun minBiRoot() { assertEquals(Double.NaN, minBiRoot(0.0, 0.0, 1.0), 1e-2) assertEquals(Double.NaN, minBiRoot(0.0, 1.0, 2.0), 1e-2) assertEquals(-2.0, minBiRoot(0.0, 1.0, -4.0), 1e-10) assertEquals(Double.NaN, minBiRoot(1.0, -2.0, 4.0), 1e-2) assertEquals(Double.NaN, minBiRoot(1.0, 3.0, 2.0), 1e-2) assertEquals(-1.41, minBiRoot(1.0, -3.0, 2.0), 1e-2) }
Обратите внимание, что функция assertEquals
при работе с типом Double
имеет третий аргумент — максимально допустимую погрешность. Учитывая, что расчёты с вещественными числами выполняются приближённо, это важная часть теста. Например, заменив в последнем вызове 1e-2
на 1e-3
(0.01 на 0.001), мы обнаружим, что тест перестал проходить — точное значение корня будет -1.41421356…, а заданное нами -1.41 с погрешностью 0.00421356…, что больше по модулю, чем 0.001.
Упражнения
Упражнения для урока 2 разбиты на две задачи — одну про if..else и другую про логические функции. Откройте вначале файл srс/lesson2/task1/IfElse.kt
в проекте KotlinAsFirst
.
Выберите любую из задач в нём. Придумайте её решение и запишите его в теле соответствующей функции.
Откройте файл test/lesson2/task1/Tests.kt
, найдите в нём тестовую функцию — её название должно совпадать с названием написанной вами функции. Запустите тестирование, в случае обнаружения ошибок исправьте их и добейтесь прохождения теста.
Внимательно прочитайте текст тестовой функции. Какие случаи ей проверяются и как? Существуют ли другие важные случаи, которые следовало бы проверить? Проверьте ещё один или два случая, добавив в текст тестовой функции новые вызовы assertEquals
.
Откройте теперь файл srс/lesson2/task2/Logical.kt
, содержащий задачи на написание логических функций. Решите одну из них, обратите внимание на имеющиеся тестовые функции — они находятся в файле test/lesson2/task2/Tests.kt
.
Решите ещё хотя бы одну задачу из урока 2 на ваш выбор. Убедитесь в том, что можете решать такие задачи уверенно и без посторонней помощи. После этого вы можете перейти к следующему разделу.
не много подумал над последней задачей, не понимал условие, пока не нарисовал наглядно на системе координат