Avatar ·

Как быстро проверить, является ли квадратный корень целого числа целым числом?

Всем привет! Я начинающий Java-разработчик, и столкнулся с задачей: нужно определять, является ли квадратный корень целого числа целым числом (то есть число — полный квадрат). Например, 25 — да (5), 26 — нет. Сначала я написал так: ```java public static boolean isPerfectSquare(int n) { double sqrt = Math.sqrt(n); return sqrt == Math.floor(sqrt); } ``` Но потом подумал: а вдруг из-за погрешностей double это будет работать неправильно для больших чисел? Например, для n = 2147483647 (максимальный int) или рядом с границами. Может, лучше использовать целочисленную арифметику? Но как? Пробовал через цикл: ```java for (int i = 1; i * i <= n; i++) { if (i * i == n) return true; } return false; ``` Но это ужасно медленно для больших n. Есть ли способ быстрее? В интернете видел упоминания про бинарный поиск, но не уверен, как его применить. Мне нужно именно для Java, и чтобы работало для всех int от 0 до Integer.MAX_VALUE. Подскажите, пожалуйста, самый быстрый и надёжный способ! <<>>

Правильный и быстрый способ: целочисленный квадратный корень через бинарный поиск

Ваше опасение насчёт double обосновано. Math.sqrt() использует числа с плавающей точкой, и для больших целых чисел (более 2^53) точность может потеряться. Для int (до 2^31-1) это случается редко, но всё же может произойти на граничных значениях. Лучше использовать целочисленную арифметику — она точная и быстрая.

Самый эффективный метод — бинарный поиск целочисленного квадратного корня. Он работает за O(log n), то есть для int нужно всего ~31 итерация. Идея: ищем такое целое число mid, что mid * mid <= n, но (mid+1)*(mid+1) > n. Затем проверяем, равно ли mid * mid исходному числу.

Реализация на Java

public static boolean isPerfectSquare(int n) {
    if (n < 0) return false; // отрицательные числа не имеют целого корня
    if (n < 2) return true;   // 0 и 1 — полные квадраты

    int left = 1;
    int right = n / 2; // корень не больше половины числа
    while (left <= right) {
        int mid = left + (right - left) / 2; // защита от переполнения
        long square = (long) mid * mid; // используем long, чтобы избежать переполнения int
        if (square == n) {
            return true;
        } else if (square < n) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    return false;
}

Почему это работает?

  • Безопасность от переполнения: mid * mid вычисляется в long, поэтому даже для mid = 46340 (корень из Integer.MAX_VALUE) результат не выйдет за пределы long.
  • Скорость: Всего ~31 итерация — это значительно быстрее, чем проверка всех чисел до корня.
  • Точность: Целочисленная арифметика гарантирует 100% точность для всех int.

Альтернатива: метод Ньютона (Вавилонский метод) с целыми числами

Метод Ньютона для целых чисел тоже работает быстро и иногда даже быстрее бинарного поиска. Вот реализация:

public static boolean isPerfectSquareNewton(int n) {
    if (n < 0) return false;
    if (n < 2) return true;
    
    long x = n / 2; // начальное приближение
    while (x * x > n) {
        x = (x + n / x) / 2;
    }
    // после цикла x — целая часть квадратного корня
    return x * x == n;
}

Этот метод сходится быстрее бинарного поиска (обычно за 5-10 итераций), но требует осторожности с начальным приближением. Для больших n он предпочтительнее.

Сравнение производительности

  • Ваш первый метод (Math.sqrt + floor): Быстрый, но потенциально ненадёжный для больших int. Для большинства случаев работает, но не гарантирует точность.
  • Цикл от 1 до sqrt(n): Медленный. Для n = 10^9 нужно 31623 итераций.
  • Бинарный поиск: 31 итерация. Надёжен на 100%.
  • Метод Ньютона: ~5-10 итераций. Самый быстрый.

Рекомендую использовать метод Ньютона для максимальной скорости или бинарный поиск для простоты и понятности кода.

Проверка граничных случаев

System.out.println(isPerfectSquare(0));       // true
System.out.println(isPerfectSquare(1));       // true
System.out.println(isPerfectSquare(2147395600)); // true (46340^2)
System.out.println(isPerfectSquare(Integer.MAX_VALUE)); // false
System.out.println(isPerfectSquare(-4));      // false

Все тесты проходят корректно.

<<>>

Использование Math.sqrt с дополнительной проверкой

Если вы уверены, что ваши числа не превышают 2^53 (что верно для int), то Math.sqrt вполне надёжен. Но для подстраховки можно добавить проверку через умножение. Этот подход проще в написании и не требует реализации бинарного поиска.

Реализация

public static boolean isPerfectSquareSafe(int n) {
    if (n < 0) return false;
    double sqrt = Math.sqrt(n);
    int root = (int) Math.round(sqrt); // округляем до ближайшего целого
    return root * root == n;
}

Math.round исправляет небольшие погрешности double. Затем проверяем, что квадрат округлённого значения равен исходному числу. Этот метод работает для всех int, потому что максимальное значение (2147483647) меньше 2^53, где double точен.

Почему это работает?

  • Math.sqrt для int всегда выдаёт результат с погрешностью не более 1 ulp (unit in the last place).
  • Округление до ближайшего целого (Math.round) даёт правильный корень, если число — полный квадрат.
  • Проверка root * root == n — финальная верификация.

Недостатки

  • Для чисел больше 2^53 (например, long) этот метод может давать ошибки.
  • Незначительно медленнее, чем чистый Math.sqrt, но всё равно быстрее бинарного поиска.

Этот способ — хороший компромисс между простотой и надёжностью для int. Если вам нужно обрабатывать long, используйте целочисленные методы из первого ответа.