0

I discovered this issue by implemented the algorithm from this post: Example of O(n!)?

I tried counting the operations though, and the algorithm loops 64 times when calculating 4! (which is 24).

Is there something I'm missing? Or does this algorithm really just have a run-time complexity that is not O(n!)

var operations = 0;

function factorial(n) {
    for (let i = 0; i < n; i++) {
        operations++;
        factorial(n - 1);
    }
}

console.log(factorial(4))
console.log('operations: ', operations) // prints 64 operations
Hays Stanford
  • 93
  • 1
  • 8
  • 3
    O(n!) need not mean n! itself. It means it is proportional to n!. Factorials grow quickly, so it's not practical to go very high at all, but may be you want to table n versus operations versus n! and see if that makes the relationship believable. – Jeremy Kahan Feb 16 '20 at 04:44

3 Answers3

2

There is probably a good theoretical analysis, but here is an empirical one. messed around in Sheets I ran the program for numbers 0 to 12 and tracked operations. Notice that very quickly the operations tend to be really close to n times the previous entry. And the ratio of operations to n! appears to tend to e. Thus, if the limit of operations is e (or some constant) times n!, we are O(n!). We need not = n! to make that so.

Jeremy Kahan
  • 3,796
  • 1
  • 10
  • 23
  • This is making a bit more sense, considering Big O tends to be a range rather than the actual run-time in all cases for an algorithm. – Hays Stanford Feb 16 '20 at 05:26
0

You're counting loop operations on every recursion level (i.e. counting function calls except for the outermost one), instead of counting the innermost function calls. Therefore you're getting a much higher count than the plain factorial you were expecting:

1                         = 1
2 + 2*1                   = 4
3 + 3*2 + 3*2*1           = 15
4 + 4*3 + 4*3*2 + 4*3*2*1 = 64
…

The expansions for arbitrary n can be found in https://oeis.org/A007526. These are still limited by O(n!) though - see the closed formulas in the link, e.g. floor(e*n! - 1) or n! * Sum_{k=0..n-1} 1/k!, which are clearly dominated by the n! term. (Counting all invocations gives you https://oeis.org/A000522, n! * Sum_{k=0..n} 1/k!).

To get exactly a factorial, you'd need to count only the innermost function calls:

var operations = 0;

function factorial(n) {
    if (n == 0) operations++;
    for (let i = 0; i < n; i++) {
        factorial(n - 1);
    }
}

console.log(factorial(4))
console.log('operations: ', operations) // prints 64 operations
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • This just counts the number of `factorial(n)` calls when `n == 0`. That only happens at the end of the for loop, after a number of operations have been completed. – Hays Stanford Feb 16 '20 at 05:23
  • @HaysStanford Yes. And since those innermost calls are the most numerous, they dominate runtime complexity. I just wanted to show this because there are exactly `n!` of them, as this seems to have been the number you were looking for. – Bergi Feb 16 '20 at 05:26
-1

The factorial function should be:

var operations = 0;

function factorial(n) {
    operation++;
    if (n === 0) return 1;
    return n*factorial(n - 1);
}

console.log(factorial(4))
console.log('operations: ', operations) // prints 24 operations

And it's O(n). The example is about O(n!), which is a time complexity depend on n!, not n! itself.

thanhdx
  • 608
  • 4
  • 16
  • This just calculate the factorial, its runtime complexity is not O(n!) – Hays Stanford Feb 16 '20 at 04:32
  • @HaysStanford I thought you're confusing if the factorial function is O(n!). I edited the answer. The function in your example is not named factorial. – thanhdx Feb 16 '20 at 05:17