Prime Factors in Common-lisp: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

Prime Factors in Common Lisp: The Ultimate Guide from Zero to Hero

Prime factorization is the process of breaking down a composite number into its essential prime number components. This guide provides a comprehensive walkthrough of how to compute prime factors efficiently in Common Lisp, explaining the core algorithm, providing a complete code solution, and exploring its practical applications in software development.


The Quest for Digital Atoms: Why Prime Numbers Matter

Have you ever stared at a number, say 60, and wondered what it's truly made of? Not in a physical sense, but mathematically. You might know it's 6 times 10, or 2 times 30. But what if you keep breaking it down until you can't anymore? You'd be left with a unique set of "atomic" numbers: 2, 2, 3, and 5. These are its prime factors, the fundamental building blocks that cannot be divided further.

For many developers, especially those new to a language as powerful and distinct as Common Lisp, translating mathematical concepts like this into functional, elegant code can feel like a daunting puzzle. You might understand the logic on a whiteboard, but implementing it with recursion, loops, and idiomatic Lisp syntax is another challenge entirely.

This article is your map through that challenge. We will not only solve the prime factors problem but also demystify the process, turning abstract theory into concrete, understandable code. You'll learn the "what," "why," and "how" of prime factorization, and by the end, you'll have a robust Common Lisp function and the confidence to tackle similar algorithmic tasks.


What is Prime Factorization? The Bedrock of Number Theory

Before we dive into code, we must solidify our understanding of the core concepts. Prime factorization rests on two simple ideas: prime numbers and the fundamental theorem of arithmetic.

Defining a Prime Number

A prime number is a natural number greater than 1 that has no positive divisors other than 1 and itself. Think of them as indivisible integers.

  • Examples of prime numbers: 2, 3, 5, 7, 11, 13, 17...
  • Examples of non-prime (composite) numbers: 4 (2x2), 6 (2x3), 9 (3x3), 10 (2x5)...

Note that 1 is not a prime number by definition. The number 2 is also unique as it is the only even prime number.

The Fundamental Theorem of Arithmetic

This theorem is the cornerstone of our entire task. It states that every integer greater than 1 is either a prime number itself or can be represented as a unique product of prime numbers. This uniqueness is key; the prime factors of 60 will always be {2, 2, 3, 5}, regardless of how you find them.

Our goal is to write a program that takes any natural number and produces this unique list of prime factors. For example:

  • Input: 90
  • Process: 90 ÷ 2 = 45 → 45 ÷ 3 = 15 → 15 ÷ 3 = 5 → 5 ÷ 5 = 1
  • Output: (2 3 3 5)

Why is Prime Factorization Important in Programming?

This isn't just an academic exercise. Prime factorization is a fundamental concept that underpins many advanced areas of computer science and software engineering. Mastering it is a crucial step in the Module 3 curriculum for Common Lisp on kodikra.

  • Cryptography: The security of widely used encryption algorithms like RSA relies on the fact that it is computationally very difficult to find the prime factors of extremely large numbers. Your online banking and secure communications depend on this principle.
  • Algorithm Optimization: Many problems in competitive programming and algorithm design can be simplified by analyzing the prime factors of the numbers involved. This includes problems related to divisors, greatest common divisor (GCD), and least common multiple (LCM).
  • Mathematical Libraries: Building robust scientific and mathematical software packages often requires functions for number theory, and prime factorization is a core component.
  • Project Euler & Puzzles: It's a classic problem that appears in many coding challenges and interviews, making it an excellent way to practice your problem-solving skills with the Common Lisp language.

How to Calculate Prime Factors: The Trial Division Algorithm

The most intuitive and common method for finding prime factors is called Trial Division. The logic is straightforward and mimics how you might solve it with pen and paper. We systematically try to divide our number by small prime candidates.

Let's break down the algorithm using our example, N = 60.

  1. Start with the smallest prime divisor: Our first candidate is d = 2.
  2. Check for divisibility: Is 60 divisible by 2? Yes. So, we add 2 to our list of factors and update our number.
    • Factors: (2)
    • Number N becomes: 60 / 2 = 30
  3. Repeat with the same divisor: Is 30 divisible by 2? Yes. Add 2 to our list again and update the number.
    • Factors: (2 2)
    • Number N becomes: 30 / 2 = 15
  4. Continue until not divisible: Is 15 divisible by 2? No. We are done with the divisor 2.
  5. Increment the divisor: Move to the next candidate divisor, d = 3.
  6. Check for divisibility: Is 15 divisible by 3? Yes. Add 3 to our list and update the number.
    • Factors: (2 2 3)
    • Number N becomes: 15 / 3 = 5
  7. Repeat: Is 5 divisible by 3? No. We are done with 3.
  8. Increment and continue: The next candidate would be 4, but we can skip it since it's not prime (it's composed of 2s, which we've already factored out). So we move to d = 5. Is 5 divisible by 5? Yes.
    • Factors: (2 2 3 5)
    • Number N becomes: 5 / 5 = 1
  9. Termination Condition: Once our number N becomes 1, the process is complete. The list of factors we've collected is the final answer.

An Important Optimization

A crucial optimization is that we only need to test divisors up to the square root of the current number N. Why? If a number N has a factor d that is larger than its square root, then it must also have a corresponding factor N/d that is smaller than its square root. Since we are testing divisors in increasing order, we would have already found the smaller factor. If we reach the square root and haven't found any factors, the remaining number N must itself be prime.


Where This Is Implemented: The Common Lisp Solution

Now, let's translate the trial division algorithm into elegant and idiomatic Common Lisp code. We will use a recursive helper function to maintain the state (the current number, the current divisor, and the accumulated factors).

The Complete Code

Here is a well-commented, complete solution. This code is part of the exclusive learning materials at kodikra.com.

;;; This code is part of the kodikra.com exclusive curriculum.

(defun prime-factors (n)
  "Computes the prime factors of a given natural number N.
   Returns a list of prime factors in ascending order."
  (labels ((get-factors (num divisor factors)
             (cond
               ;; Base case: If the number is 1, we are done. Return the accumulated factors.
               ((= num 1) (reverse factors))

               ;; Optimization: If divisor^2 > num, the remaining num must be prime.
               ;; Add it to the list and terminate.
               ((> (* divisor divisor) num) (reverse (cons num factors)))

               ;; Recursive step 1: If num is divisible by the current divisor...
               ((zerop (rem num divisor))
                ;; ...continue factoring the new quotient (num / divisor) with the same divisor,
                ;; adding the current divisor to our list of factors.
                (get-factors (/ num divisor) divisor (cons divisor factors)))

               ;; Recursive step 2: If not divisible, try the next divisor.
               (t (get-factors num (if (= divisor 2) 3 (+ divisor 2)) factors)))))

    ;; Initial call to the recursive helper function.
    ;; Start with the number n, the first prime divisor 2, and an empty list of factors.
    (get-factors n 2 '())))

Running the Code in a REPL

To use this code, save it as a file (e.g., prime-factors.lisp). Then, start your Common Lisp implementation (like SBCL) and execute the following commands in the REPL (Read-Eval-Print Loop).


$ sbcl
* (load "prime-factors.lisp")
; T
* (prime-factors 60)
; (2 2 3 5)
* (prime-factors 901255)
; (5 17 10603)
* (prime-factors 93819012551)
; (93819012551)
* (quit)

Visualizing the Algorithm Logic

Here is a modern ASCII art diagram illustrating the flow of our trial division logic within the recursive helper function.

    ● Start with (num, divisor, factors)
    │
    ▼
  ┌─────────────────┐
  │ Check num = 1 ? │
  └────────┬────────┘
           │
          Yes ⟶ ● Return reversed factors & End
           │
           No
           │
           ▼
  ┌───────────────────────────┐
  │ Check divisor² > num ?    │
  └────────────┬──────────────┘
               │
              Yes ⟶ ● Add num to factors, return & End
               │
               No
               │
               ▼
    ◆ Is num divisible by divisor?
   ╱                             ╲
  Yes                             No
  │                               │
  ▼                               ▼
┌─────────────────────────┐     ┌───────────────────────────────┐
│ num = num / divisor     │     │ if divisor = 2, next is 3     │
│ Add divisor to factors  │     │ else, next is divisor + 2     │
└──────────┬──────────────┘     └───────────────┬───────────────┘
           │                                    │
           └───────────┬────────────────────────┘
                       │
                       ▼
      Call recursively with new state
                       │
                       ● Loop back to start

Who Benefits From This: A Deep Code Walkthrough

Understanding the code line by line is crucial for true mastery. Let's dissect our prime-factors function.

The Main Function: `(defun prime-factors (n))`

This is our public-facing function. It takes one argument, n, the number we want to factor. Its only job is to kick off the factorization process by calling an internal helper function with the correct initial values.

The Local Helper: `(labels ((get-factors ...)))`

The labels special form is a powerful feature in Common Lisp. It allows us to define local functions that are only visible within the scope of the main function. This is perfect for creating recursive helpers without polluting the global namespace.

Our helper, get-factors, takes three arguments:

  • num: The current number we are trying to factor. This value decreases as we find factors.
  • divisor: The current candidate we are testing for divisibility.
  • factors: A list that accumulates the prime factors we've found so far.

The Core Logic: `(cond ...)`

The cond macro is Lisp's version of a multi-branch if-then-else or a switch statement. It evaluates each clause in order and executes the code for the first one whose condition is true.

Clause 1: The Base Case

((= num 1) (reverse factors))

This is our primary termination condition. If the number we're factoring has been reduced to 1, it means we have found all its prime factors. We use (reverse factors) because we built the list by adding new factors to the front (using cons), so we need to reverse it at the end to get the correct ascending order.

Clause 2: The Square Root Optimization

((> (* divisor divisor) num) (reverse (cons num factors)))

This implements the optimization we discussed. If the square of our current divisor is greater than the remaining num, it's impossible for num to have any smaller factors. Therefore, the remaining num must be a prime number itself. We add it to our list of factors and terminate.

Clause 3: Found a Factor

((zerop (rem num divisor))
 (get-factors (/ num divisor) divisor (cons divisor factors)))

Here, (rem num divisor) calculates the remainder of num divided by divisor. The zerop function checks if this remainder is zero. If it is, num is perfectly divisible by divisor. We then make a recursive call:

  • The new number is (/ num divisor).
  • We use the same divisor again, because a number can have repeated factors (like 12 = 2 * 2 * 3).
  • We add the found divisor to our list with (cons divisor factors).

Clause 4: No Factor Found, Move to Next

(t (get-factors num (if (= divisor 2) 3 (+ divisor 2)) factors))

The t condition acts as a catch-all "else" block. If none of the previous conditions were met, it means num is not divisible by the current divisor. We need to find the next divisor to test. This line contains a small but clever optimization:

  • (if (= divisor 2) 3 (+ divisor 2)): If the current divisor is 2, the next one to test is 3. After that, we can skip all even numbers entirely by just adding 2 to the current odd divisor (3 -> 5 -> 7 -> 9...). We don't need to worry about testing non-prime odds like 9, because we would have already factored out their prime components (in this case, 3).

The Initial Call

(get-factors n 2 '())

Finally, the main function body makes the first call to our helper, starting the process with the original number n, the first prime divisor 2, and an empty list of factors '().


Exploring Alternative Approaches

While trial division is effective and easy to understand, it's not the only method. For very large numbers, more sophisticated algorithms are used. It's valuable to be aware of them.

Sieve of Eratosthenes + Trial Division

A common alternative strategy involves two steps:

  1. Generate Primes: First, use an algorithm like the Sieve of Eratosthenes to generate a list of all prime numbers up to the square root of the input number.
  2. Trial Division with Primes Only: Then, perform trial division using only the numbers from your generated prime list. This avoids testing composite numbers like 4, 6, 9, etc., making it more efficient.

This approach is faster if you need to factorize many numbers within a certain range, as the sieve needs to be run only once.

Conceptual Flow: Trial Division vs. Sieve-Based Factoring

This diagram shows the conceptual difference. The Sieve-based method has a pre-computation step before it begins factoring.

    ● Input Number N

    ├─ Path 1: Direct Trial Division ───────────────────────
    │
    │   Test Divisor d = 2
    │   Test Divisor d = 3
    │   Test Divisor d = 5 (skipping 4)
    │   Test Divisor d = 7 (skipping 6)
    │   ... and so on
    │
    └────────────────────────────────────────────────────────

    ├─ Path 2: Sieve-Based Approach ────────────────────────
    │
    │  ┌───────────────────────────────┐
    │  │ 1. Pre-computation:           │
    │  │    Run Sieve up to sqrt(N)    │
    │  └───────────────┬───────────────┘
    │                  │
    │                  ▼
    │         [2, 3, 5, 7, 11, ...] (List of Primes)
    │                  │
    │  ┌───────────────┴───────────────┐
    │  │ 2. Factoring:                 │
    │  │    Test with 2 from list      │
    │  │    Test with 3 from list      │
    │  │    Test with 5 from list      │
    │  └───────────────────────────────┘
    │
    └────────────────────────────────────────────────────────

    ▼
 ● Output: List of Prime Factors

Pros and Cons of Trial Division

For most practical purposes and coding challenges, the optimized trial division method is more than sufficient. Here's a quick summary of its trade-offs.

Pros Cons
  • Simple to Understand: The logic is intuitive and follows a clear, step-by-step process.
  • Easy to Implement: Requires minimal code and no complex data structures.
  • Memory Efficient: Uses very little memory as it doesn't need to store a large list of primes.
  • Very Fast for Small Numbers: Extremely effective for numbers with small prime factors.
  • Inefficient for Large Primes: Performance degrades significantly if the number is a product of two very large primes (the basis of RSA).
  • Slower than Sieve-based Methods: For factoring many numbers, the repeated work of testing non-prime divisors makes it slower than pre-computing primes.
  • Not Suitable for Cryptography: Cannot be used to break modern encryption due to its exponential time complexity for worst-case inputs.

Frequently Asked Questions (FAQ)

1. What is the difference between a prime number and a prime factor?

A prime number is a number greater than 1 that is only divisible by 1 and itself (e.g., 5, 7, 13). A prime factor is a prime number that, when multiplied with other prime factors, produces a specific composite number. For the number 12, the prime factors are 2, 2, and 3. Here, 2 and 3 are both prime numbers.

2. Why does the algorithm only need to check divisors up to the square root of the number?

This is a key optimization. If a number N has a divisor d that is larger than sqrt(N), then the corresponding divisor N/d must be smaller than sqrt(N). Since our algorithm checks divisors in increasing order, we would have already found the smaller divisor. If we reach sqrt(N) without finding any divisors, the remaining number must be prime.

3. Is recursion the only way to solve this in Common Lisp?

No, you can absolutely solve this iteratively using constructs like loop or do. The recursive solution with a local helper function is often considered more idiomatic and elegant in functional languages like Lisp, especially when using tail-call optimization, which many Lisp implementations provide to prevent stack overflow.

4. How does this algorithm handle very large numbers?

Common Lisp has built-in support for arbitrary-precision integers (bignums), so the code will work correctly without overflowing. However, the performance will degrade. For cryptographically large numbers (hundreds of digits long), this trial division algorithm would take an impractically long time to run.

5. What happens if the input is 1 or a prime number itself?

Our code handles these cases gracefully. If the input is 1, the first condition (= num 1) is immediately met, and it returns an empty list, which is correct. If the input is a prime number (e.g., 17), the loop will run until the divisor squared exceeds the number, at which point the second condition will trigger, correctly returning a list containing just the number itself: (17).

6. Are there built-in Common Lisp libraries for prime factorization?

The Common Lisp standard itself does not specify a prime factorization function. However, there are many third-party libraries available through systems like Quicklisp that provide extensive mathematical and number theory functionalities, often with highly optimized algorithms. This exercise from the kodikra learning path is designed to teach you how to build such a function from first principles.


Conclusion: From Theory to Practical Skill

We've journeyed from the fundamental definition of a prime number to a fully functional and optimized prime factorization algorithm in Common Lisp. You've seen how the simple, elegant logic of trial division can be translated into powerful recursive code, a hallmark of the Lisp programming style.

More importantly, you now understand the "why" behind the code—the importance of prime factorization in fields like cryptography and the logic behind key optimizations. This knowledge transforms a simple coding exercise into a durable skill in your developer toolkit.

This problem is a fantastic example of the challenges you'll encounter in our Common Lisp learning path. As you continue your journey, you'll build on these foundational concepts to tackle even more complex and rewarding problems. To learn more about the language itself, be sure to visit our comprehensive Common Lisp language guide.

Disclaimer: The code provided in this article is written for educational purposes and has been tested with modern Common Lisp implementations like SBCL 2.4.x. The core logic is portable across standard-compliant Lisp environments.


Published by Kodikra — Your trusted Common-lisp learning resource.