Sieve in Crystal: Complete Solution & Deep Dive Guide

Tabs labeled

Mastering Prime Numbers in Crystal: The Sieve of Eratosthenes from Zero to Hero

The Sieve of Eratosthenes is a brilliantly efficient, ancient algorithm for finding all prime numbers up to a specified integer. In Crystal, this is elegantly implemented by creating a boolean or bit array to mark numbers as prime or composite, then iteratively eliminating the multiples of each discovered prime.

Ever found yourself staring at a coding challenge, tasked with finding prime numbers, and your first instinct is to write a clunky loop inside another loop? You check every number for its divisors, one by one. It works for small inputs, but as the limit grows, your program slows to a crawl, your console hangs, and you feel the creeping dread of inefficiency. This is a common pain point for developers, a sign that a more refined tool is needed.

This is where the genius of an ancient Greek mathematician, Eratosthenes, comes to the rescue. Imagine an algorithm so elegant and fast that it feels like magic, sifting through numbers and leaving only the primes behind. In this guide, we won't just show you the code; we will dissect the Sieve of Eratosthenes, understand its core principles, and implement a highly optimized version in Crystal. You will walk away with not just a solution, but a deep understanding of a timeless computer science concept.


What Exactly is the Sieve of Eratosthenes?

The Sieve of Eratosthenes is an algorithm for finding all prime numbers up to a given limit. A "sieve" is a tool used to separate desired elements from unwanted material, and that's precisely what this algorithm does: it "sifts" out the composite numbers, leaving only the primes.

The concept, attributed to Eratosthenes of Cyrene, a Greek mathematician who was the chief librarian at the Library of Alexandria, is remarkably intuitive. It operates on a simple, yet powerful, observation: if a number is composite (not prime), it must have a prime factor that is less than or equal to its square root.

Instead of testing each number for primality individually (a process known as trial division), the Sieve works in bulk. It starts with a list of all integers from 2 up to the desired limit and progressively eliminates multiples of primes. What remains at the end is the complete set of prime numbers within that range.

Who Was Eratosthenes?

Understanding the origin of an algorithm adds a layer of appreciation for its design. Eratosthenes (c. 276 BC – c. 195/194 BC) was more than just a mathematician; he was an astronomer, geographer, poet, and music theorist. He is best known for being the first person to calculate the circumference of the Earth with surprising accuracy. His work on prime numbers is a testament to the elegant problem-solving of classical antiquity, a method so efficient that it remains relevant in modern computing.


Why Use the Sieve Algorithm in Crystal?

In the world of programming, we often have multiple ways to solve the same problem. So, why choose the Sieve of Eratosthenes, especially in a modern, compiled language like Crystal?

The primary reason is efficiency for finding all primes within a specific range. While trial division is simple to write, its time complexity is poor. The Sieve of Eratosthenes, on the other hand, has a near-linear time complexity of O(n log log n), making it exceptionally fast for generating primes up to millions or even billions, provided memory allows.

Crystal, as a language, is a perfect match for this task. Its key features amplify the algorithm's strengths:

  • Performance: Crystal compiles to native code, delivering C-like speed. This means the loops and array manipulations at the heart of the Sieve execute incredibly fast.
  • Type Safety: Crystal's static type system catches errors at compile time, ensuring that operations on arrays of numbers or booleans are safe and predictable.
  • Expressive Syntax: The language's clean, Ruby-inspired syntax allows for an implementation that is both powerful and easy to read, a rare combination in high-performance code.
  • Rich Standard Library: Crystal provides efficient data structures like Array, StaticArray, and even BitArray out of the box, which are perfect for implementing the Sieve with different memory profiles.

By pairing a classic, efficient algorithm with a modern, high-performance language, you get a solution that is both robust and blazingly fast. This is a core challenge in the kodikra learning path, designed to teach you how to leverage your language's strengths to solve classic computational problems.


How the Sieve of Eratosthenes Works: A Step-by-Step Breakdown

Let's visualize the process. Imagine you want to find all prime numbers up to 30. The algorithm follows these logical steps:

  1. Create the List: Start by creating a list or array representing all integers from 2 up to your limit (30). We can use a boolean array, where true initially means "is potentially prime".
  2. Start with the First Prime: The first number, 2, is a prime. Keep it.
  3. Eliminate Multiples of 2: Go through the list and mark all multiples of 2 (4, 6, 8, 10, ..., 30) as "not prime" (or false).
  4. Move to the Next Prime: The next unmarked number in your list is 3. This must be a prime. Keep it.
  5. Eliminate Multiples of 3: Mark all multiples of 3 (6, 9, 12, 15, ..., 30) as "not prime". Note that some, like 6, will already be marked. That's okay.
  6. Continue the Process: The next unmarked number is 5 (since 4 was marked). Keep 5 and mark all its multiples (10, 15, 20, 25, 30).
  7. The Stopping Point: You only need to repeat this process for numbers up to the square root of your limit. The square root of 30 is about 5.47, so after we process the multiples of 5, we can stop. Why? Because any composite number less than 30 must have a prime factor less than or equal to 5.
  8. Collect the Results: All the numbers that are still marked as "potentially prime" are the actual primes. In our case: 2, 3, 5, 7, 11, 13, 17, 19, 23, 29.

This systematic elimination is what makes the Sieve so powerful. It avoids redundant checks and processes the entire range of numbers in a single, coordinated sweep.

Visualizing the Sieve's Logic Flow

Here is an ASCII art diagram illustrating the fundamental logic of the algorithm.

    ● Start (limit = N)
    │
    ▼
  ┌───────────────────────────┐
  │ Create boolean array      │
  │ `is_prime` of size N+1,   │
  │ all set to `true`.        │
  └────────────┬──────────────┘
               │
    ┌──────────┴──────────┐
    │ Mark 0 and 1 as     │
    │ `false`.            │
    └──────────┬──────────┘
               │
               ▼
  ┌───────────────────────────┐
  │ Loop `p` from 2 up to     │
  │ the square root of N.     │
  └────────────┬──────────────┘
               │
               ▼
        ◆ is_prime[p] is true?
       ╱         ╲
      Yes         No
      │           │
      ▼           └─────┐
┌─────────────────┐     │ (Skip, it's a
│ Loop `i` from   │     │  composite's
│ p*p up to N,    │     │  multiple)
│ step by p.      │     │
└──────┬──────────┘     │
       │                │
       ▼                │
┌─────────────────┐     │
│ Mark            │     │
│ is_prime[i] =   │     │
│ `false`.        │     │
└─────────────────┘     │
      │                 │
      └───────┬─────────┘
              │
              ▼ (End of outer loop)
  ┌───────────────────────────┐
  │ Collect all `i` where     │
  │ `is_prime[i]` is `true`.  │
  └────────────┬──────────────┘
               │
               ▼
           ● End

Implementing the Sieve of Eratosthenes in Crystal

Now, let's translate this logic into clean, idiomatic Crystal code. For our implementation, we will use a BitArray. A BitArray is a highly memory-efficient data structure where each element takes only a single bit of memory, making it perfect for representing our boolean `is_prime` flags for very large limits.

Here is the complete, well-commented solution from the kodikra.com curriculum.

# Implements the Sieve of Eratosthenes algorithm to find all primes up to a given limit.
# This solution is part of the exclusive kodikra.com learning curriculum.
module Sieve
  # Calculates prime numbers up to a given limit.
  #
  # @param limit [Int] The upper bound to find primes for.
  # @return [Array(Int32)] An array of all prime numbers up to the limit.
  def self.primes(limit : Int) : Array(Int32)
    # Return early for limits less than 2, as there are no primes.
    return [] of Int32 if limit < 2

    # A BitArray is used for memory efficiency. `true` means a number is potentially prime.
    # The size is `limit + 1` to use number indices directly (e.g., is_prime[5]).
    is_prime = BitArray.new(limit + 1, true)

    # 0 and 1 are not prime numbers by definition.
    is_prime[0] = false
    is_prime[1] = false

    # The main loop iterates from 2 up to the square root of the limit.
    # We only need to check for prime factors up to sqrt(limit).
    # Any composite number `n` will have a prime factor less than or equal to `sqrt(n)`.
    p = 2
    while p * p <= limit
      # If `is_prime[p]` is still true, then p is a prime number.
      if is_prime[p]
        # Now, mark all multiples of p as not prime.
        # We can start marking from p*p. Why?
        # Multiples of p less than p*p (e.g., 2*p, 3*p) would have already been
        # marked by smaller primes (2, 3, etc.).
        i = p * p
        while i <= limit
          is_prime[i] = false
          i += p
        end
      end
      p += 1
    end

    # Now, collect all the numbers that remained marked as prime.
    primes = [] of Int32
    (2..limit).each do |num|
      primes << num if is_prime[num]
    end

    primes
  end
end

How to Run This Code

To run this Crystal code, save it in a file named sieve.cr. You can then test it by creating another file, say runner.cr, to call the method.

runner.cr:

require "./sieve"

limit = 100
prime_numbers = Sieve.primes(limit)

puts "Prime numbers up to #{limit}:"
puts prime_numbers.inspect

Then, execute it from your terminal using the Crystal compiler:


$ crystal run runner.cr

This command will compile and run the program, and you should see the list of prime numbers up to 100 printed to your console.


Detailed Code Walkthrough

Let's dissect the Crystal implementation to understand every detail.

  1. Module and Method Definition:
    module Sieve
      def self.primes(limit : Int) : Array(Int32)
        # ...
      end
    end

    We define our logic inside a Sieve module. The method primes is a class method (self.primes) that accepts an integer limit and is type-annotated to return an Array(Int32). This leverages Crystal's strong type system for clarity and safety.

  2. Edge Case Handling:
    return [] of Int32 if limit < 2

    The smallest prime number is 2. If the limit is less than 2, there are no primes to find. We handle this edge case by returning an empty array immediately, preventing unnecessary computation.

  3. Initializing the Sieve:
    is_prime = BitArray.new(limit + 1, true)
    is_prime[0] = false
    is_prime[1] = false

    This is the heart of the setup. We create a BitArray of size limit + 1 so that the index of the array corresponds directly to the number (e.g., the primality of 10 is stored at is_prime[10]). We initialize all values to true, assuming all numbers are prime until proven otherwise. We then explicitly mark 0 and 1 as not prime according to mathematical definitions.

  4. The Outer Loop (Finding Primes):
    p = 2
    while p * p <= limit
      # ...
      p += 1
    end

    This loop iterates through potential prime numbers, starting with p = 2. The condition p * p <= limit is a crucial optimization. It ensures we only check for base primes up to the square root of the limit, which dramatically reduces the number of iterations.

  5. The Inner Loop (Marking Multiples):
    if is_prime[p]
      i = p * p
      while i <= limit
        is_prime[i] = false
        i += p
      end
    end

    If is_prime[p] is true, it means p has not been marked as a multiple of any smaller prime, so p itself must be prime. We then enter the inner loop to eliminate all of its multiples. The inner loop starts at i = p * p, another key optimization. All smaller multiples of p (like 2*p, 3*p) would have already been marked by the sieves of 2, 3, etc. This avoids redundant work.

  6. Collecting the Results:
    primes = [] of Int32
    (2..limit).each do |num|
      primes << num if is_prime[num]
    end
    primes

    After the loops complete, the is_prime BitArray accurately reflects the primality of every number up to the limit. We iterate from 2 to the limit one last time and collect all numbers for which the flag is still true into our final results array.

Visualizing the p*p Optimization

This diagram shows why starting the inner loop at p*p is so effective. When we process the prime p=5 with a limit of 50, we don't need to check 2*5, 3*5, or 4*5.

    ● Start Sieve for p=5
    │
    ▼
  ┌───────────────────────────┐
  │ is_prime[5] is `true`.    │
  │ Process its multiples.    │
  └────────────┬──────────────┘
               │
               ▼
        ◆ Check 2 * 5 = 10?
       ╱         ╲
      No          Yes
      │           │
      ▼           ▼
┌───────────────┐  (Already marked `false`
│ Skip. Why?    │   by the sieve for p=2)
└───────────────┘
      │
      ▼
        ◆ Check 3 * 5 = 15?
       ╱         ╲
      No          Yes
      │           │
      ▼           ▼
┌───────────────┐  (Already marked `false`
│ Skip. Why?    │   by the sieve for p=3)
└───────────────┘
      │
      ▼
┌───────────────────────────┐
│ Start inner loop at i=p*p │
│ which is 5 * 5 = 25.      │
└────────────┬──────────────┘
             │
             ▼
      ┌────────────────┐
      │ Mark 25, 30,   │
      │ 35, 40, 45, 50 │
      │ as `false`.    │
      └────────────────┘
             │
             ▼
         ● End Sieve for p=5

Pros, Cons, and Alternative Approaches

No algorithm is a silver bullet. The Sieve of Eratosthenes is fantastic, but it's essential to understand its trade-offs.

Pros & Cons of the Sieve of Eratosthenes

Pros (Advantages) Cons (Disadvantages)
Extremely Fast: Near-linear time complexity O(n log log n) makes it one of the fastest ways to find all primes up to a limit n. High Memory Usage: Requires an array of size n+1, which can be prohibitive for very large limits (e.g., finding primes up to 1012).
Simple to Implement: The core logic is straightforward and doesn't involve complex mathematics like division or modulo operations inside the main loop. Not Ideal for Single Primes: If you only need to check if one very large number is prime, trial division or the Miller-Rabin primality test is far more efficient.
Finds All Primes: It generates a complete list of primes in a range, not just one at a time. Static Range: The basic algorithm requires the upper limit to be known beforehand. It's not designed for finding primes on the fly in an infinite sequence.
Highly Optimizable: Techniques like the p*p start, wheel factorization, and segmented sieves can further improve performance and reduce memory footprint. Integer Size Limits: The limit is constrained by the maximum array size and integer type supported by the language and system memory.

Alternative Algorithms for Finding Primes

  • Trial Division: The most basic method. To test if a number n is prime, you divide it by all integers from 2 up to sqrt(n). It's simple to write but very slow for finding all primes in a large range. Best used for checking a single, relatively small number.
  • Sieve of Atkin: A more modern and complex algorithm that is an optimized version of the Sieve of Eratosthenes. It has a better theoretical time complexity of O(n / log log n) but is significantly harder to implement correctly. For most practical limits, a well-implemented Sieve of Eratosthenes is fast enough.
  • Segmented Sieve: An enhancement to the Sieve of Eratosthenes that reduces memory usage. It works by breaking the full number range into smaller "segments," applying the sieve to one segment at a time. This allows you to find primes up to a very large limit without needing a single, massive array.

For the scope of most programming challenges and applications, the classic Sieve of Eratosthenes provides the best balance of performance, simplicity, and efficiency, which is why it's a foundational algorithm in our Crystal curriculum at kodikra.com.


Frequently Asked Questions (FAQ)

1. Why is the algorithm called a "Sieve"?

It's named after a kitchen sieve, which is used to separate fine particles from coarse ones. The algorithm acts like a numerical sieve, starting with a block of all numbers and progressively filtering out the composite numbers until only the "fine" prime numbers are left.

2. What is the time and space complexity of the Sieve of Eratosthenes?

The time complexity is approximately O(n log log n), which is very close to linear time. The space complexity is O(n) because it requires an array of size proportional to the limit n to store the primality flags.

3. Why do we only need to iterate the outer loop up to the square root of the limit?

This is a critical optimization. Any composite number c can be factored into a * b. If both a and b were greater than sqrt(c), then their product a * b would be greater than c, which is a contradiction. Therefore, at least one prime factor of any composite number must be less than or equal to its square root. By sieving with all primes up to sqrt(limit), we guarantee that we have eliminated all composite numbers up to limit.

4. How does Crystal's BitArray help optimize the Sieve?

A standard boolean array (Array(Bool)) in most languages uses one full byte (8 bits) to store each true/false value. A BitArray is a specialized data structure that packs 8 boolean values into a single byte. This reduces the memory consumption of the sieve by a factor of 8, allowing you to calculate primes for a much larger limit within the same amount of RAM.

5. Can the Sieve be used to find primes in a specific range, like between 1,000,000 and 2,000,000?

Yes, but not with the basic implementation. This task requires a "Segmented Sieve." First, you would run a normal sieve up to the square root of the upper bound (e.g., sqrt(2,000,000)) to find all necessary base primes. Then, you would use those base primes to sieve the specific range (1,000,000 to 2,000,000) in a separate, smaller array representing just that segment.

6. Is the Sieve of Eratosthenes still used in practice today?

Absolutely. While cryptography relies on different methods for finding massive individual primes (like Miller-Rabin), the Sieve of Eratosthenes and its variants are still widely used in computational number theory, competitive programming, and for pre-calculating prime number tables for various mathematical applications where a complete list of primes in a certain range is needed.

7. What's the next step after mastering this algorithm?

After understanding the Sieve, a great next step is to explore more advanced number theory algorithms or tackle problems involving prime factorization. You could also try implementing a Segmented Sieve for a greater challenge. You can find more of these challenges in the Crystal 3 roadmap module on kodikra.com.


Conclusion: The Timeless Elegance of a Simple Idea

The Sieve of Eratosthenes is more than just a piece of code; it's a beautiful example of algorithmic thinking. It teaches us that often, the most effective solution isn't about brute force, but about changing our perspective on the problem. Instead of asking "Is this number prime?", the Sieve efficiently answers "What numbers are definitely not prime?".

By implementing this in Crystal, you've not only solved a classic problem but also practiced using efficient data structures like BitArray and writing performant, type-safe code. You now possess a powerful tool for any task involving prime number generation and a deeper appreciation for an algorithm that has stood the test of over two thousand years.

Ready to tackle the next challenge? Continue your journey through our exclusive Crystal learning path to master more algorithms and data structures. For a deeper dive into the language itself, explore our complete Crystal guide and unlock your full potential as a developer.

Disclaimer: The code in this article has been verified against Crystal 1.12.x. As the language evolves, syntax or standard library features may change. Always consult the official Crystal documentation for the latest information.


Published by Kodikra — Your trusted Crystal learning resource.