Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

A perfect number is a special type of positive integer that holds a unique property as it is equal to the sum of its proper divisors, excluding the number itself.

One of the most intriguing aspects of perfect numbers is their rarity. Perfect numbers are infrequent within the realm of integers. In fact, throughout history, only a handful of perfect numbers have been identified. The first four perfect numbers known to humanity are 6, 28, 496, and 8128.

In this article, we’ll delve into the concept of perfect numbers and explore various approaches to check if a given number falls into this intriguing category.

2. Understanding Perfect Numbers

Let’s examine the first few examples to grasp the concept of perfect numbers. The number 6, for instance, has proper divisors 1, 2, and 3. Summing these divisors yields 1 + 2 + 3 = 6, making 6 a perfect number. Similarly, the number 28 has proper divisors 1, 2, 4, 7, and 14, with their sum equating to 1 + 2 + 4 + 7 + 14 = 28. Hence, 28 is another perfect number.

We can employ different methodologies to determine whether a given number is perfect. Let’s explore three common approaches.

3. Brute Force Method

One way to check for perfection is by iterating through all possible divisors of the number and summing them. If the sum equals the number itself, we have a perfect number. This method can be implemented using a loop that traverses from 1 to n/2, where n is the given number. For each divisor found, we accumulate the sum. Finally, we compare the sum with the original number to determine perfection:

public static boolean isPerfectBruteForce(int number) {
    int sum = 0;
    for (int i = 1; i <= number / 2; i++) {
        if (number % i == 0) {
            sum += i;
        }
    }
    return sum == number;
}

The choice to iterate up to n/2 is based on the observation that the largest possible proper divisor of a number n, excluding n itself, is n/2. Divisors larger than n/2 would result in a quotient less than 2, which is not considered a proper divisor. This approach ensures that we cover all the necessary divisors efficiently.

The brute force method iterates through all possible divisors from 1 to n/2 and calculates the sum. The time complexity is linear concerning the input number.

4. Stream-based Method

We can also utilize Java Streams to check for perfect numbers. In this method, we generate a stream of divisors from 2 to the square root of the number being tested (rounded down), filter the divisors that evenly divide the number, and then calculate the sum by adding each divisor and its corresponding pair (number/test). Finally, we compare the sum with the original number to determine perfection:

public static boolean isPerfectStream(int number) {
    int sum = IntStream.rangeClosed(2, (int) Math.sqrt(number))
      .filter(test -> number % test == 0)
      .reduce(1, (s, test) -> s + test + (number / test));
    return sum == number;
}

When searching for divisors of a number, we can stop iterating once we reach the square root of the number. This is because factors always come in pairs. For example, if a is a divisor of a number, then b = number/a is also a divisor. If a is less than or equal to the square root of the number, then b will be greater than or equal to the square root of the number. Therefore, by finding divisors up to the square root, we have covered all possible factors.

The stream-based method uses Java Streams to filter and sum the divisors up to the square root of the number. By only looping up to the square root, we can reduce the amount of work needed to find the divisors and improve efficiency. This optimized approach significantly reduces the execution time compared to the previous attempts, making it a more efficient solution for checking perfect numbers.

5. Euclid-Euler Theorem

A more efficient method, based on the Euclid-Euler theorem, allows us to identify perfect numbers without iterating through all possible divisors. According to the theorem, if 2^(p-1) * (2^p – 1) is a prime number, then (2^p – 1) * 2^(p-1) is a perfect number. Here, p must be a prime number. By utilizing the theorem, we can focus on calculating perfect numbers based on the formula. This approach allows us to efficiently identify perfect numbers without the need to iterate through all possible divisors or explicitly find prime numbers:

public static boolean isPerfectEuclidEuler(int number) {
    int p = 2;
    int perfectNumber = (int) (Math.pow(2, p - 1) * (Math.pow(2, p) - 1));
    while (perfectNumber <= number) {
        if (perfectNumber == number) {
            return true;
        }
        p++;
        perfectNumber = (int) (Math.pow(2, p - 1) * (Math.pow(2, p) - 1));
    }
    return false;
}

The Euclid-Euler theorem method leverages prime numbers and calculates perfect numbers based on the theorem. The time complexity of the Euclid-Euler method is better than that of the brute force and stream-based methods because it only requires checking numbers up to log n.

Although the time complexity is better than the brute force and Stream-based methods, the execution time of the Euclid-Euler method we provided may not meet expectations. One reason for this suboptimal performance is the usage of Math.pow(), which is known for its slowness. Considering that we’re only calculating the power of base 2, we can improve the efficiency by utilizing binary shift operations instead of Math.pow():

public static boolean isPerfectEuclidEulerUsingShift(int number) {
    int p = 2;
    int perfectNumber = (2 << (p - 1)) * ((2 << p) - 1);
    while (perfectNumber <= number) {
        if (perfectNumber == number) {
            return true;
        }
        p++;
        perfectNumber = (2 << (p - 1)) * ((2 << p) - 1);
    }
    return false;
}

By replacing Math.pow() with binary shift operations (2 << p) for calculating powers of 2, we can achieve faster and more efficient calculations. This optimization significantly improves the performance of the Euclid-Euler method.

6. Analysis and Comparison

Let’s analyze and compare the three different methods we provided for checking perfect numbers.

The brute force method, although simple, iterates through all possible divisors and compares the sum with the original number. It is suitable for smaller numbers but becomes less efficient as the input size increases. This method has a time complexity linear to the input number, resulting in slower execution times for larger numbers.

The stream-based method offers a more concise and expressive implementation by utilizing Java Streams to filter and sum the divisors efficiently. By limiting the iteration to the square root of the number, it reduces unnecessary calculations and iterations, improving efficiency and reducing execution time. The stream-based method strikes a balance between simplicity and efficiency.

The Euclid-Euler theorem method provides the most efficient approach among the three. Leveraging the theorem’s formula allows us to calculate perfect numbers directly without checking all possible divisors. This method significantly reduces the computational effort and achieves remarkable efficiency. It is particularly effective for large numbers, as it avoids iterating through numerous divisors.

To compare the performance of these methods, we conducted benchmarking using JMH, a common tool for micro benchmarking Java programs. The benchmark was performed using a known perfect number, specifically 33550336. The results of the benchmark are:

Benchmark                                               Mode  Cnt         Score         Error  Units
PerfectNumberBenchmark.bruteForceBenchmark             thrpt    5        55.070 ±       2.674  ops/s
PerfectNumberBenchmark.streamBenchmark                 thrpt    5     96114.246 ±    3666.451  ops/s
PerfectNumberBenchmark.euclidEulerBenchmark            thrpt    5    144639.676 ±    3409.540  ops/s
PerfectNumberBenchmark.euclidEulerUsingShiftBenchmark  thrpt    5  99191865.954 ± 5924410.475  ops/s

The “Cnt” column represents the number of iterations performed for each benchmark mode. The “Score” column represents the throughput or the number of operations per second achieved by each benchmark. It indicates how often a particular operation or algorithm was executed within a second. The “Error” column represents the statistical error associated with the benchmark results. It provides an estimate of the variability or uncertainty in the measured values. A smaller error indicates more consistent and reliable results.

The Euclid-Euler method stands out as the most efficient approach, followed by the stream-based method. The brute force method, while straightforward, is less efficient and more suitable for smaller numbers. When dealing with larger numbers, the Euclid-Euler or stream-based methods are recommended for optimal performance.

7. Conclusion

In this article, we explored the fascinating concept of perfect numbers and examined various approaches to determine if a given number falls into this category.

As always, the source code is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments