Checking If A Number Is Prime, Efficiently
How To Reduce Time Complexity From O(n) To O(sqrt(n))
Table of contents
If you've solved algorithm challenges on sites like Leetcode, HackerRank, or FreeCodeCamp, you've likely run into the need to check whether a number is prime or not. A prime number is any positive integer which is only divisible by itself and 1. In this post, I will highlight a common approach to this problem, and discuss how to improve its time complexity.
Accuracy
In proper JS fashion, there are multiple approaches that could lead us to an accurate solution. For instance, a brute-force approach could look something like this:
const num1 = 7;
const num2 = 4;
const isPrime = n => {
if (n <= 1) return false;
else if (n == 2) return true;
else {
for (let i = 2; i < n; i++) {
if (n % i == 0) return false;
}
return true;
}
}
isPrime(num1);
// => true
isPrime(num2)
// => false
As you can see from the example above, we are able to write an accurate function with fairly simple logic:
If
n
is less than or equal to 1, returnfalse
.Else if
n
is equal to 2, returntrue
.Else, loop through all numbers between
n
and 1 and returnfalse
ifn
is divisible by any.When/if the loop ends, return
true
.
What is isPrime
doing when called on num1
? First, it checks if the argument is less than or equal to 1, because 2 is the smallest prime number. num1
has the value of 7, so it skips the if
and if...else
blocks. Next, in the else
block, we loop through all the numbers between - but not including - our argument and 1. If our argument is divisible by any iteration of i
, isPrime
will return false
. If our loop ends without a return, isPrime
will return true
. Because 7 is not divisible by any number between itself and 1, isPrime(num1)
returns true
. Try following the path of isPrime(num2)
to find where it returns false
.
Efficiency and Time Complexity
This function is perfectly accurate, but it is not as efficient as it could be. To highlight this, consider checking a large prime number such as 3779. That would require our function to perform 3777 tasks in the else
block alone! In other words, the solution above has a time complexity of O(n), increasing linearly as the value of n
increases.
We can improve our isPrime
function's time complexity. Take a look at the following change:
const isPrime = n => {
if (n <= 1) return false;
else if (n ==2) return true;
else {
const maxDiv = Math.sqrt(n)
for (let i = 2; i <= maxDiv; i++) {
if (n % i == 0) return false;
}
return true;
}
}
isPrime(3779);
// => true
isPrime(3778)
// => false
By only looping between 1 (exclusive) and the square root of n
(inclusive), we are reducing the required tasks for isPrime(3779)
from 3777 to 60. That's a big improvement. Now the time complexity of our solution is O(sqrt(n)), because the maximum number of required tasks is proportional to the square root of n
.
Why It Works
But how is the maximum divisor of a number its square root? For example, 60 has many divisors, and some of them are definitely larger than its square root of 7.746. The important thing to note here, is that while 60 has divisors larger than 7.746 (10, 12, 15, 20, 30), each of them requires a factor smaller than 7.746 to reach the product of 60. In the for
loop of our first solution, we are doing extra work by checking divisors twice, as well as many other unnecessary checks. Continuing with our example of 60, we don't need to loop through all the numbers between 1 and 60, because by the time we reach its square root, we have checked all the factor pairs. In other words, we checked all the larger divisors just by checking for divisors less than or equal to maxDiv
.
Conclusion
There will always be many solutions to every algorithm problem, and not all solutions are created equal. Some require our machines to work much harder and longer than others, and understanding the time complexity of our solutions improves the efficiency of our code.
Depending on what kind of code you write, you will likely come across the need to check the primeness of a number. By applying what we know about prime numbers to our code, we can exploit their unique qualities to make our code cleaner and more efficient, without compromising accuracy. That's prime stuff.