2

What is the worst case time complexity (Big O notation) of the following function for positive integers?

def rec_mul(a:int, b:int) -> int:
        if b == 1:
            return a
        
        if a == 1:
            return b
        
        else:
            return a + rec_mul(a, b-1)

I think it's O(n) but my friend claims it's O(2^n)

My argument: The function recurs at any case b times, therefor the complexity is O(b) = O(n)

His argument: since there are n bits, a\b value can be no more than (2^n)-1, therefor the max number of calls will be O(2^n)

  • 1
    What is your *n*? – Berthur Feb 14 '23 at 13:59
  • It's O(b) in the worst case. – AboAmmar Feb 14 '23 at 14:02
  • it is O(n) n == b in worst case which a is not 1. and by the way your function should be in infinite loop if b is less or equal to zero – amirhm Feb 14 '23 at 14:04
  • 1
    For reasons I get into in my answer, it's rarely useful to choose `n = b`, though, as it "artificially" underestimates the growth rate of the runtime as `a` and/or `b` increase. The input *size* should not be confused with the input's *magnitude*. – chepner Feb 14 '23 at 15:19

3 Answers3

2

You are both right.

If we disregard the time complexity of addition (and you might discuss whether you have reason to do so or not) and count only the number of iterations, then you are both right because you define:

n = b

and your friend defines

n = log_2(b)

so the complexity is O(b) = O(2^log_2(b)).

Both definitions are valid and both can be practical. You look at the input values, your friend at the lengths of the input, in bits.

This is a good demonstration why big-O expressions mean nothing if you don't define the variables used in those expressions.

Berthur
  • 4,300
  • 2
  • 14
  • 28
1

Your friend and you can both be right, depending on what is n. Another way to say this is that your friend and you are both wrong, since you both forgot to specify what was n.

Your function takes an input that consists in two variables, a and b. These variables are numbers. If we express the complexity as a function of these numbers, it is really O(b log(ab)), because it consists in b iterations, and each iteration requires an addition of numbers of size up to ab, which takes log(ab) operations.

Now, you both chose to express the complexity in function of n rather than a or b. This is okay; we often do this; but an important question is: what is n?

Sometimes we think it's "obvious" what is n, so we forget to say it.

  • If you choose n = max(a, b) or n = a + b, then you are right, the complexity is O(n).
  • If you choose n to be the length of the input, then n is the number of bits needed to represent the two numbers a and b. In other words, n = log(a) + log(b). In that case, your friend is right, the complexity is O(2^n).

Since there is an ambiguity in the meaning of n, I would argue that it's meaningless to express the complexity as a function of n without specifying what n is. So, your friend and you are both wrong.

Stef
  • 13,242
  • 2
  • 17
  • 28
1

Background

A unary encoding of the input uses an alphabet of size 1: think tally marks. If the input is the number a, you need O(a) bits.

A binary encoding uses an alphabet of size 2: you get 0s and 1s. If the number is a, you need O(log_2 a) bits.

A trinary encoding uses an alphabet of size 3: you get 0s, 1s, and 2s. If the number is a, you need O(log_3 a) bits.

In general, a k-ary encoding uses an alphabet of size k: you get 0s, 1s, 2s, ..., and k-1s. If the number is a, you need O(log_k a) bits.

What does this have to do with complexity?

As you are aware, we ignore multiplicative constants inside big-oh notation. n, 2n, 3n, etc, are all O(n).

The same holds for logarithms. log_2 n, 2 log_2 n, 3 log_2 n, etc, are all O(log_2 n).

The key observation here is that the ratio log_k1 n / log_k2 n is a constant, no matter what k1 and k2 are... as long as they are greater than 1. That means f(log_k1 n) = O(log_k2 n) for all k1, k2 > 1.

This is important when comparing algorithms. As long as you use an "efficient" encoding (i.e., not a unary encoding), it doesn't matter what base you use: you can simply say f(n) = O(lg n) without specifying the base. This allows us to compare runtime of algorithms without worrying about the exact encoding you use.

So n = b (which implies a unary encoding) is typically never used. Binary encoding is simplest, and doesn't provide a non-constant speed-up over any other encoding, so we usually just assume binary encoding.

That means we almost always assume that n = lg a + lg b as the input size, not n = a + b. A unary encoding is the only one that suggests linear growth, rather than exponential growth, as the values of a and b increase.


One area, though, where unary encodings are used is in distinguishing between strong NP-completeness and weak NP-completeness. Without getting into the theory, if a problem is NP-complete, we don't expect any algorithm to have a polynomial running time, that is, one bounded by O(n**k) for some constant k when using an efficient encoring.

But some algorithms do become polynomial if we allow a unary encoding. If a problem that is otherwise NP-complete becomes polynomial when using an unary encoding, we call that a weakly NP-complete problem. It's still slow, but it is in some sense "faster" than an algorithm where the size of the numbers doesn't matter.

chepner
  • 497,756
  • 71
  • 530
  • 681
  • While all that is true, it is important especially for a beginner to understand that you can describe time complexity in terms of different things. As long as it is communicated what your variables represent, it is not incorrect. And while time complexity tends to be defined w.r.t. input size specifically, it is common to allow some freedom which allows for abstractions that better help convey the complexity to the reader. An example being the complexity of depth-first-search in a graph often being expressed as *O(|V| + |E|)*, without commenting on what encoding we happen to use for the graph. – Berthur Feb 14 '23 at 15:59
  • 1
    Yes, but I'd say it's equally (if not more) important to understand why you can't just pick whichever one makes your algorithm "look" faster. If you and I both code up an algorithm for the general TSP problem, and I use a binary encoding and get a runtime of `O(n**100)` and you use a unary encoding to get `O(n)`, only one of us gets to claim the [Clay Millennium Prize for proving that P = NP](https://www.claymath.org/millennium-problems/p-vs-np-problem). Nobody will *care* about your "linear-time" algorithm for such a poor choice of input encoding. – chepner Feb 14 '23 at 16:04
  • The `O(V + E)` issue is different, IMO. Here, you aren't playing with input sizes. You (and anyone reading) are well aware that `E = O(V**2)`, but that the performance depends on the exact number of edges in the graph, not on how you chose to encode the vertices and edges as input. – chepner Feb 14 '23 at 16:05
  • Granted, it is important to understand that defining your variables differently will not make your algorithm faster. And defining input size to an arithmetic algorithm by actual input *size* and not magnitude is certainly both the standard and more useful. However, I still think the also the other answers are both correct and relevant (not sure if you claim otherwise as I don't know who downvoted them), as OP in their question seems to have as a premise that the time complexity is either the one or the other, by absolute truth. – Berthur Feb 14 '23 at 16:43
  • ...the other answers just clarify this misconception, which I think is the source of OP's confusion. – Berthur Feb 14 '23 at 16:43
  • I downvoted, because they don't emphasize that you *don't* just pick one or the other as long as you clarify what you mean. Nobody publishing a paper or presenting a new algorithm would make the choice to say that `n = b`, even with the clarification that they are using the magnitude as the "size" of the problem. The friend's argument is correct, and if you were to use `O(n)` as an answer on an exam, you would not be marked correct, even if you stated your assumption, because your assumption is not *useful*. – chepner Feb 14 '23 at 16:47