14 min read

Print All Divisors of a Number in C++

Print All Divisors of a Number in C++ is a foundational number theory and mathematical algorithmic problem frequently asked in technical interviews and online assessments at companies such as Amazon, Goldman Sachs, Microsoft, and Google. Given a positive integer N, the objective is to find and print all integers d that divide N completely (such that N % d == 0) in strictly increasing order. While an intuitive Brute Force approach takes linear O(N) time, understanding the Mathematical Symmetry of Divisor Pairs allows us to reduce execution time to Optimal O(√N) runtime without requiring any expensive sorting routines. In this comprehensive guide, we will trace the complete algorithmic evolution: from naive scanning to unordered square-root pairing and optimal two-pass sorted factor generation, complete with runnable C++ code, step by step dry runs, CPU division latency analysis, and a high throughput 64-bit performance benchmark lab with โ€œTry it yourselfโ€ challenges after every section.

๐Ÿ”น Mathematical Foundations: The Symmetry of Divisor Pairs

A divisor (or factor) of a positive integer N is any integer d such that the remainder of N / d is zero (N % d == 0).

The foundational insight of number theory is that divisors always appear in symmetric pairs. If an integer d divides N, then its quotient q = N / d is also guaranteed to divide N:

If d * q = N, then both 'd' and 'q' are divisors of N.

Let’s examine the divisors of N = 36:

Divisor Pairs for N = 36:
  1  * 36 = 36  -> Pair (1, 36)
  2  * 18 = 36  -> Pair (2, 18)
  3  * 12 = 36  -> Pair (3, 12)
  4  *  9 = 36  -> Pair (4, 9)
  6  *  6 = 36  -> Pair (6, 6)   <-- Square Root Point (sqrt(36) = 6)
-------------------------------------------------- (Symmetric Reflection)
  9  *  4 = 36  -> Pair (9, 4)   (Duplicate)
 12  *  3 = 36  -> Pair (12, 3)  (Duplicate)
 18  *  2 = 36  -> Pair (18, 2)  (Duplicate)
 36  *  1 = 36  -> Pair (36, 1)  (Duplicate)
  • The Square Root Bound: For any pair d * q = N, it is mathematically impossible for both d and q to be strictly greater than √N (because if d > √N and q > √N, then d * q > √N * √N = N, a contradiction).
  • Fundamental Invariant: At least one factor in every divisor pair must be less than or equal to √N. Therefore, searching up to √N discovers 100% of all divisors of N!

Try it yourself

  • List all divisor pairs for N = 100 up to √100 = 10.
  • How many divisors does a prime number P have? Where do its divisor pairs lie?

๐Ÿ”น Approach 1: Brute Force (Linear Scan from 1 to N)

Intuition & Reasoning: The most naive approach is to test every single integer i from 1 to N using the modulo operator (N % i == 0). If i divides N with no remainder, print i.

#include <iostream>
#include <vector>

using namespace std;

// Brute Force: Linear Scan from 1 to N
vector<int> print_divisors_brute_force(int n) {
    vector<int> divisors;
    
    for (int i = 1; i <= n; ++i) {
        if (n % i == 0) {
            divisors.push_back(i);
        }
    }
    
    return divisors;
}

int main() {
    int n = 36;
    cout << "--- Brute Force Divisor Generation ---\n";
    cout << "Divisors of " << n << ": ";
    auto divisors = print_divisors_brute_force(n);
    for (int d : divisors) {
        cout << d << " ";
    }
    cout << "\n";
    return 0;
}

Output

--- Brute Force Divisor Generation ---
Divisors of 36: 1 2 3 4 6 9 12 18 36 

Takeaway: The linear scan correctly generates all divisors in sorted order. However, it evaluates 36 modulo iterations for a small number like 36, and will perform 1,000,000,000 iterations for N = 109.

Complexity Analysis & The Bottleneck:

  • Time Complexity (TC): O(N): The loop executes exactly N times, performing an integer division/modulo operation on every iteration.
  • Space Complexity (SC): O(1) auxiliary space (excluding the output container storing the divisors).
  • The Bottleneck: For N = 109, O(N) requires 1 billion modulo operations, taking ~1.2 seconds of CPU time. For 64-bit integers (N = 1012), brute force would take over 20 minutes to complete!

Try it yourself

  • What is the largest divisor of N that is strictly less than N? (Hint: At most N / 2. Why does checking numbers between N / 2 + 1 and N – 1 always yield 0 divisors?).
  • If you optimize the loop to run up to n / 2 and push n at the end, what is the new time complexity? (Still O(N) because N / 2 = 0.5N).

๐Ÿ”น Approach 2: Better (Square Root Pair Collection + Sorting)

Optimization Intuition: Instead of looping up to N, we loop only up to √N (written as i * i <= n). When we discover that n % i == 0, we simultaneously record both divisor i and its symmetric counterpart n / i.

Special Edge Case (Perfect Squares): If N is a perfect square (e.g. N = 36 at i = 6), i and n / i are identical (6 == 36 / 6). We must ensure we only push i once to avoid duplicate entries!

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// Better: Loop up to sqrt(N) + Sort
vector<int> print_divisors_sqrt_sort(int n) {
    vector<int> divisors;

    // Loop up to sqrt(n)
    for (int i = 1; i * i <= n; ++i) {
        if (n % i == 0) {
            divisors.push_back(i); // Push smaller divisor
            
            // Push symmetric quotient if not a perfect square root
            if (i != n / i) {
                divisors.push_back(n / i);
            }
        }
    }

    // Since divisors were inserted out-of-order, sort them
    sort(divisors.begin(), divisors.end());
    return divisors;
}

int main() {
    int n = 36;
    cout << "--- Sqrt Pairing with std::sort ---\n";
    auto divisors = print_divisors_sqrt_sort(n);
    cout << "Divisors of " << n << ": ";
    for (int d : divisors) {
        cout << d << " ";
    }
    cout << "\n";
    return 0;
}

Output

--- Sqrt Pairing with std::sort ---
Divisors of 36: 1 2 3 4 6 9 12 18 36 

Takeaway: The square root iteration reduces loop passes for N = 36 from 36 down to just 6 iterations (and for N = 109 from 1,000,000,000 down to ~31,622 iterations)!

Step by Step Trace of Divisor Pairs for N = 36:

Iteration 1 (i = 1): 36 % 1 == 0 -> Push 1, Push (36 / 1 = 36)
Iteration 2 (i = 2): 36 % 2 == 0 -> Push 2, Push (36 / 2 = 18)
Iteration 3 (i = 3): 36 % 3 == 0 -> Push 3, Push (36 / 3 = 12)
Iteration 4 (i = 4): 36 % 4 == 0 -> Push 4, Push (36 / 4 = 9)
Iteration 5 (i = 5): 36 % 5 != 0 -> No action
Iteration 6 (i = 6): 36 % 6 == 0 -> i == n/i (6 == 6) -> Push 6 ONCE!

Unsorted Vector: [1, 36, 2, 18, 3, 12, 4, 9, 6]
After std::sort: [1, 2, 3, 4, 6, 9, 12, 18, 36]

Complexity Analysis & Limitations:

  • Time Complexity (TC): O(√N + D ยท log D): The loop runs in O(√N). Sorting D divisors (where D is the total number of divisors of N) takes O(D ยท log D). Since D is very small compared to N (for N ≤ 109, maximum D = 1,344), this runs rapidly in ~0.05ms.
  • Space Complexity (SC): O(D) to store divisors in the vector.
  • The Inefficiency: Calling std::sort is technically unnecessary. We can generate divisors in strictly sorted order directly in O(√N) time with ZERO sorting overhead.

Try it yourself

  • Why is i * i <= n preferred over i <= sqrt(n) in loop conditions? (Hint: sqrt() is an expensive floating-point function and may introduce precision errors for large integers).
  • What is the risk of writing i * i <= n if n is a 64-bit integer near LLONG_MAX? (Hint: i * i can overflow a 32-bit integer).

๐Ÿ”น Approach 3: Optimal (Two-Pass O(√N) Sorted Generation without Sorting)

The Core Algorithmic Insight: Notice how the small divisors and large quotient divisors behave:

  • Pass 1 (Ascending Small Divisors): As i increments from 1 up to √N, all valid divisors i are discovered in strictly ascending order (e.g. 1, 2, 3, 4, 6).
  • Pass 2 (Ascending Large Divisors): As i decrements from √N down to 1, the quotient counterparts N / i are discovered in strictly ascending order (e.g. 9, 12, 18, 36)!
Pass 1: i goes 1 -> 6 (Ascending small factors)
  Discovers: 1, 2, 3, 4, 6

Pass 2: i goes 6 -> 1 (Decreasing i produces increasing N / i!)
  i = 4 -> 36 / 4 = 9
  i = 3 -> 36 / 3 = 12
  i = 2 -> 36 / 2 = 18
  i = 1 -> 36 / 1 = 36

Concatenated Result: [1, 2, 3, 4, 6, 9, 12, 18, 36] (Naturally Sorted!)
#include <iostream>
#include <vector>

using namespace std;

// Optimal: O(sqrt(N)) Time and O(1) Auxiliary Space (Naturally Sorted)
vector<long long> print_divisors_optimal(long long n) {
    vector<long long> divisors;

    // Pass 1: Collect small divisors in increasing order (1 to sqrt(n))
    for (long long i = 1; i * i <= n; ++i) {
        if (n % i == 0) {
            divisors.push_back(i);
        }
    }

    // Pass 2: Collect large counterpart divisors in increasing order
    // Start from the integer square root and count downwards
    for (long long i = static_cast<long long>(divisors.size()) - 1; i >= 0; --i) {
        long long small_divisor = divisors[i];
        long long large_divisor = n / small_divisor;

        // Avoid duplicate inclusion for perfect square roots (e.g. 6 == 36/6)
        if (small_divisor != large_divisor) {
            divisors.push_back(large_divisor);
        }
    }

    return divisors;
}

int main() {
    long long n1 = 36;
    long long n2 = 100;
    long long n3 = 37; // Prime number

    cout << "--- Optimal Two-Pass Divisor Generation ---\n";
    
    auto d1 = print_divisors_optimal(n1);
    cout << "Divisors of " << n1 << ": ";
    for (long long x : d1) cout << x << " ";
    cout << "\n";

    auto d2 = print_divisors_optimal(n2);
    cout << "Divisors of " << n2 << ": ";
    for (long long x : d2) cout << x << " ";
    cout << "\n";

    auto d3 = print_divisors_optimal(n3);
    cout << "Divisors of " << n3 << ": ";
    for (long long x : d3) cout << x << " ";
    cout << "\n";

    return 0;
}

Output

--- Optimal Two-Pass Divisor Generation ---
Divisors of 36: 1 2 3 4 6 9 12 18 36 
Divisors of 100: 1 2 4 5 10 20 25 50 100 
Divisors of 37: 1 37 

Takeaway: Two-Pass generation produces strictly sorted divisors in guaranteed O(√N) time with zero calls to std::sort and zero dynamic comparator allocations.

Complexity Analysis (Proof of Optimum):

  • Time Complexity (TC): O(√N): Pass 1 performs at most √N iterations. Pass 2 iterates over the small divisors already collected (at most √N / 2 elements). Total operations = O(√N).
    • For N = 1,000,000 (106): executes ≤ 1,000 iterations.
    • For N = 1,000,000,000 (109): executes ≤ 31,622 iterations.
    • For N = 1,000,000,000,000 (1012): executes ≤ 1,000,000 iterations (taking ~1.5ms).
  • Space Complexity (SC): O(1) auxiliary memory (aside from the output vector storing the D divisors).

Try it yourself

  • Step through the execution of print_divisors_optimal on N = 1. What does it return?
  • Why does Pass 2 iterate backwards over divisors (i = size - 1 down to 0)? What would happen if it iterated forwards?

๐Ÿ”น Hardware Mechanics: CPU Division Latency and Branch Prediction

From an instruction-level perspective, optimizing number theory code requires understanding CPU arithmetic execution units:

1. Integer Division (IDIV) Latency

Unlike addition, subtraction, or bitwise operations which execute in a single CPU cycle (0.3 nanoseconds), the x86 IDIV instruction is the slowest primitive ALU instruction on modern CPUs, taking 15 to 40 CPU clock cycles per operation. In our optimal O(√N) algorithm, reducing iterations from 109 down to 31,622 eliminates millions of wasted IDIV cycles.

2. Strength Reduction: Multiplication vs Square Root

Writing i * i <= n utilizes integer multiplication (IMUL), which has a single-cycle latency and pipeline throughput of 1 operation/cycle. Calling sqrt(n) invokes the hardware floating-point square root instruction (SQRTSD), incurring register conversion overhead between general-purpose registers (GPRs) and SSE/AVX vector registers.

Try it yourself

  • Inspect the assembly generated for i * i <= n on Compiler Explorer (Godbolt) under -O3 optimization.
  • How can you avoid computing n / small_divisor in Pass 2 if you store pairs in a flat array?

๐Ÿ”น Lab: High Throughput Divisor Benchmark (N up to 1012)

Let’s benchmark our three approaches across multiple orders of magnitude: from N = 10,000,000 (107) up to N = 1,000,000,000,000 (1012).

#include <iostream>
#include <vector>
#include <chrono>
#include <iomanip>
#include <algorithm>

using namespace std;

// 1. Brute Force
void bench_brute(long long n) {
    volatile long long count = 0;
    for (long long i = 1; i <= n; ++i) {
        if (n % i == 0) ++count;
    }
}

// 2. Sqrt + Sort
void bench_sqrt_sort(long long n) {
    vector<long long> d;
    for (long long i = 1; i * i <= n; ++i) {
        if (n % i == 0) {
            d.push_back(i);
            if (i != n / i) d.push_back(n / i);
        }
    }
    sort(d.begin(), d.end());
}

// 3. Optimal Two-Pass (No Sort)
void bench_optimal(long long n) {
    vector<long long> d;
    for (long long i = 1; i * i <= n; ++i) {
        if (n % i == 0) d.push_back(i);
    }
    for (long long i = static_cast<long long>(d.size()) - 1; i >= 0; --i) {
        if (d[i] != n / d[i]) d.push_back(n / d[i]);
    }
}

int main() {
    cout << "--- CppMonk Divisor Performance Benchmark Lab ---\n\n";

    // Test Case 1: N = 10,000,000 (10^7)
    long long n1 = 10000000;
    cout << "1. Small Input: N = " << n1 << "\n";
    {
        auto start = chrono::high_resolution_clock::now();
        bench_brute(n1);
        auto end = chrono::high_resolution_clock::now();
        double ms = chrono::duration_cast<chrono::microseconds>(end - start).count() / 1000.0;
        cout << "   Brute Force O(N):           " << ms << " ms\n";
    }
    {
        auto start = chrono::high_resolution_clock::now();
        bench_optimal(n1);
        auto end = chrono::high_resolution_clock::now();
        double us = chrono::duration_cast<chrono::nanoseconds>(end - start).count() / 1000.0;
        cout << "   Optimal Two-Pass O(sqrt(N)): " << us << " us\n\n";
    }

    // Test Case 2: N = 1,000,000,000,000 (10^12 - 64-bit integer)
    long long n2 = 1000000000000LL;
    cout << "2. Massive 64-Bit Input: N = " << n2 << "\n";
    cout << "   Brute Force O(N):           [Skipped - would take ~25 minutes!]\n";
    {
        auto start = chrono::high_resolution_clock::now();
        bench_sqrt_sort(n2);
        auto end = chrono::high_resolution_clock::now();
        double ms = chrono::duration_cast<chrono::microseconds>(end - start).count() / 1000.0;
        cout << "   Sqrt + Sort:                " << ms << " ms\n";
    }
    {
        auto start = chrono::high_resolution_clock::now();
        bench_optimal(n2);
        auto end = chrono::high_resolution_clock::now();
        double ms = chrono::duration_cast<chrono::microseconds>(end - start).count() / 1000.0;
        cout << "   Optimal Two-Pass O(sqrt(N)): " << ms << " ms\n";
    }

    return 0;
}

Output

--- CppMonk Divisor Performance Benchmark Lab ---

1. Small Input: N = 10000000
   Brute Force O(N):           11.42 ms
   Optimal Two-Pass O(sqrt(N)): 3.12 us

2. Massive 64-Bit Input: N = 1000000000000
   Brute Force O(N):           [Skipped - would take ~25 minutes!]
   Sqrt + Sort:                1.84 ms
   Optimal Two-Pass O(sqrt(N)): 1.18 ms

Takeaway: For N = 107, Optimal Two-Pass runs in 3.12 microseconds (a 3,660x speedup over linear scan). For N = 1012, it completes in just 1.18 milliseconds!

Try it yourself

  • Benchmark a highly composite number like N = 735,134,400 (which has 1,344 divisors) and observe the sort duration vs two-pass generation.
  • How does compiler optimization -O3 transform the modulo loop into reciprocal multiplication?

๐Ÿ”น Key Pitfalls & Common Interview Traps

  • Trap 1: Integer Overflow in i * i: If N is a 64-bit integer (long long), writing int i = 1; i * i <= n; will cause i * i to overflow 32-bit signed limits at i = 46,341, creating an infinite loop. Always declare long long i = 1;.
  • Trap 2: Double-Counting the Perfect Square Root: For N = 36, when i = 6, n / i = 6. Failing to check if (i != n / i) will push 6 twice into the output vector.
  • Trap 3: Starting Loop at i = 0: Writing for (int i = 0; ...) causes an immediate hardware Division by Zero Exception (SIGFPE) on n % i. Divisor loops MUST always begin at i = 1.
  • Trap 4: Using Floating-Point sqrt() with Casting Issues: Writing for (int i = 1; i <= (int)sqrt(n); ++i) can suffer from precision truncation (e.g. sqrt(49) evaluating to 6.9999999 and casting to 6, missing the root factor 7). i * i <= n is purely integer-exact.

Try it yourself

  • Write a test case for N = 49 and verify that 7 is added exactly once.
  • Construct an algorithm to check whether N is a Prime Number in O(√N) using the divisor counting loop.

๐Ÿ”น Comparison of All Approaches

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Approach                            โ”‚ Time Complexity  โ”‚ Space Complexity โ”‚ Output Ordering        โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ 1. Brute Force (1 to N)             โ”‚ O(N)             โ”‚ O(1)             โ”‚ Naturally Sorted       โ”‚
โ”‚ 2. Sqrt Pairing + std::sort         โ”‚ O(โˆšN + D log D)  โ”‚ O(D)             โ”‚ Requires sorting       โ”‚
โ”‚ 3. Optimal Two-Pass Sqrt Generation โ”‚ O(โˆšN)            โ”‚ O(1)             โ”‚ Naturally Sorted       โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

๐Ÿ”น FAQs on Divisor Generation in C++

Q1: What is the optimal time complexity to find all divisors of N?
The optimal time complexity is O(√N). By exploiting the symmetry of divisor pairs (where if d divides N, then N / d also divides N), we only need to test numbers up to √N.

Q2: Why does an odd number of divisors imply that N is a perfect square?
Because divisors always come in distinct pairs (d, N/d), every non-square number has an even number of divisors. Only when N is a perfect square does the pair collapse into a single identical value (d == N/d), resulting in an odd total divisor count.

Q3: How do we calculate the Sum of All Divisors for N?
During the square root loop, whenever n % i == 0, add i to the running sum, and if i != n / i, also add n / i. This computes the sum of divisors in O(√N) time.

Q4: What is a Prime Number in terms of divisors?
A prime number is an integer strictly greater than 1 that has exactly two positive divisors: 1 and itself (N).

Q5: How many divisors can a 32-bit integer have at most?
For numbers up to 2 × 109, the highly composite number 735,134,400 has the maximum number of divisors: exactly 1,344 divisors.

Q6: Can we find divisors faster than O(√N) for multiple queries?
Yes. If answering multiple queries across a range [1..N], we can precompute prime factors using the Sieve of Eratosthenes in O(N log log N) time, enabling divisor generation for any query in O(D) time via prime factorization combinatorics.

Q7: What happens when N = 0 or N is negative?
By definition, divisor problems are defined over positive integers (N ≥ 1). For negative integers, divisors are the positive divisors along with their negative counterparts. Division by zero is undefined.

Q8: What real world systems use divisor algorithms?
Divisor computation powers RSA asymmetric cryptography (key generation and factor finding), digital signal processing (FFT radix decomposition), matrix tile block partitioning in GPU compute shaders, and memory page size allocation.

๐Ÿ”น Wrapping Up

Printing All Divisors in C++ is an elegant showcase of mathematical symmetry overcoming brute force computational limits. By replacing linear O(N) iteration with the two-pass square-root invariant, we accelerate factor discovery by thousands of times while generating strictly sorted output with zero sorting overhead. To continue expanding your number theory and algorithmic toolkit, explore our deep dives on Count Digits in C++, Reverse Number in C++, and the complete Google DSA Sheet. Practice the โ€œTry it yourselfโ€ challenges above to make these number-theoretic insights second nature!

Leave a Comment