How to analyze O(n) of recursive functions
Learn how to analyze the time complexity of recursive functions and understand their behavior in terms of Big O notation.
when it comes to analyzing the time complexity of recursive functions, we use three main methods:
- Substitution method
- Recursion tree method
- Master theorem
before diving into the methods, we first need to derive the recurrence relation of the function.
A recurrence relation is an equation that defines a sequence of values based on previous values. For example, if we have a recursive function that calls itself twice, we can express its time complexity as , where is the time complexity of the function for input size .
How to derive the recurrence relation?
Let's analyze the following recursive function:
int binarySearch(int arr[], int left, int right, int target) {
if (right >= left) {
int mid = left + (right - left) / 2;
if (arr[mid] == target)
return mid;
if (arr[mid] > target)
return binarySearch(arr, left, mid - 1, target); // recursive call for the left half
return binarySearch(arr, mid + 1, right, target); // recursive call for the right half
}
return -1;
}Notice that the function call itself on line numbers 7 and 8. That's the recursive call. Each time we call binarySearch, it internally calls itself with a smaller portion of the array, which creates a new stack frame for each call.
Even though we cannot see any loops or iterative constructs in the code, we cannot say its
Analyzing the time complexity
Lets investigate one line at a time.
-
Line 1:
int binarySearch(int arr[], int left, int right, int target)
Let's assume the input size isn, which is the number of elements in the array. and The time this function would take to run is denoted as . -
Line 2:
if (right >= left)
Constant time. Notice if the condition isfalse, The would be because it will return -1 immediately.
We can assume when the left and right pointers are equal, the function will take constant time to execute. So we can say that . -
Line 3,4,5,6:
int mid = left + (right - left) / 2; if (arr[mid] == target) return mid; if (arr[mid] > target)These lines also take constant time, , because they perform a fixed number of operations regardless of the input size.
-
Line 7:
return binarySearch(arr, left, mid - 1, target);
This is where it gets interesting. This includes a recursive call. So we will assume this would take the same time as when the input size is .
But notice the input size for this call is , as it operates on the left half of the array. Therefore, the time complexity for this call can be expressed as . -
Line 8:
return binarySearch(arr, mid + 1, right, target);
Similar to line 7, this also includes a recursive call. The input size for this call is also , as it operates on the right half of the array. Therefore, the time complexity for this call can also be expressed as .
Now let's see the total time complexity with the code, represent constant times:
int binarySearch(int arr[], int left, int right, int target) {
if (right >= left) {
int mid = left + (right - left) / 2; // C1
if (arr[mid] == target) // C2
return mid; // C3
if (arr[mid] > target) // C4
return binarySearch(arr, left, mid - 1, target); // T(n/2)
return binarySearch(arr, mid + 1, right, target); // T(n/2)
}
return -1; // C5
}Now let's analyze cases of the function:
-
:
-
:
We have officially derived the recurrence relation formula.
Notice even though we have two recursive calls in the code, only one of them will be executed at a time. So we can say that the time complexity of the function is .
Analyze time complexiting using methods
Substitution method
There are two ways to solve the recurrence relation using the substitution method:
- Guess and check: We make an educated guess about the form of the solution and then use mathematical induction to verify our guess.
- Iterative substitution: We repeatedly substitute the recurrence relation into itself until we can identify a pattern or reach a base case.
Guess and check method
We assume our function as an upper bound .
- Hypothesis: for all , where is a positive constant.
We prove this using mathematical induction.
Hypothesis for the current case: for all .
-
Base case: (usually )
Notice that is a pitfall!. The base case says , but our proof says . This is a common trap. This does not mean our hypothesis is wrong. Notice our hypothesis says it needs to be true for all .
So we can choose a different base case, for example .Now we have a valid base case.
-
Inductive step: Assume the hypothesis holds for all , we need to show it holds for .
Now that we have shown that the hypothesis holds for , we can conclude that . That wraps up the guess and check method. We have successfully analyzed the time complexity of the recursive function using the substitution method.
Iterative substitution method
We iteratively substitute the recurrence relation to find a pattern or until hit the base case.
Substituting equation into the original equation, we get:
where is the number of times we have substituted the recurrence relation.
Now we need to find the value of when we hit the base case. The base case is when , so we set and solve for :
We also know . Now lets consider , iteration.
Now we analyzed the time complexity using the iterative approach. But notice the subtle naunce that we let which is not an integer. In practice, we would take the to ensure that is an integer. However, since we are interested in the asymptotic behavior of the function, this does not affect our analysis, and we can still conclude that .
Recursion tree method
A recursion tree is a visual representation of all the recursive calls made. Each node represents the cost of a single subproblem, and we sum the costs of all nodes to get the total time complexity.
For our recurrence relation , let's denote the constant time as . Since the function is called only once per execution with half the input size, the "tree" doesn't branch out—it forms a single straight path.
graph TD
A(("T(n)")) -->|"Work: c"| B(("T(n/2)"))
B -->|"Work: c"| C(("T(n/4)"))
C -.->|"Work: c"| D(("..."))
D -.->|"Work: c"| E(("T(1)"))Let's break down the total cost:
- Depth of the tree: The input size halves at each step (). We reach the base case when , which gives a depth of .
- Cost per level: Because there are no branching recursive calls, there is only node at each level. The work done at each node is a constant .
- Total Work: We multiply the number of levels by the cost per level:
Therefore, the total time complexity is .