How to better analyze runtime of complex nested loops? - runtime

When you have nested for-loops where the amount of loops for the nested one changes each time, what is the easiest approach to analyze the total runtime? It's hard for me to conceptualize how to factor in the changing max value since I've only ever analyzed nested loops where the max was out of N, which led to a pretty simple O(n^2) runtime. Should I make a summation and use that?
For Example:
int val = 1;
for (int i = 0; i < n; i++) {
for (int j = 0; j < val; j++) {
val++;
}
}
My intuition tells me this is 2^n, but I have no practical way of really proving that

In general, to find the time complexity of loops, you need to find how many times they execute, as a function of the input. Sometimes it is straightforward, sometimes it is not. You may end up with complex mathematical expressions, and in some cases you may not be able to decide at all.
As for your example, your outer loop would clearly run exactly n times. Your inner loop, however, checks its loop condition j < val, which the first time is true because j = 0 and val = 1. Then, it increments val by 1 on each iteration so it will always be true that j < val. Therefore we notice that it is an infinite loop, and your program thus runs in O(∞).
(As a side note, in practice, depending on the language of implementation, eventually val may overflow and become smaller than j, which will cause the loop to finish. In this case, it only depends on the integer size you are using.)

Related

What counts as an operation in algorithms?

So I've just started learning algorithms and data structures, and I've read about Big O and how it portrays complexity of algorithms based on how the number of operations required scales
But what actually counts as an operation? In this bubble sort, does each iteration of the for loop count as an operation, or only when an if statement is triggered, or all of them?
And since there are so many different algorithms of all kinds, how do you immediately identify what would count as an "operation" happening in the algorithm's code?
function bubbleSort(array) {
for (let i = 0; i < array.length; i++) {
for (let j = 0; j < array.length; j++) {
if (array[j + 1] < array[j]) {
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
}
}
}
return array
}
You can count anything as an operation that will execute within a constant amount of time, independent of input. In other words, operations that have a constant time complexity.
If we assume your input consists of fixed-size integers (like 32-bit, 64 bit), then all of the following can be considered such elementary operations:
i++
j < array.length
array[j + 1] < array[j]
let tmp = array[j]
...
But that also means you can take several of such operations together and still consider them an elementary operation. So this is also an elementary operation:
if (array[j + 1] < array[j]) {
let tmp = array[j]
array[j] = array[j+1]
array[j+1] = tmp
}
So, don't concentrate on breaking down operations into smaller operations, and those again into even smaller operations, when you are already certain that the larger operation is O(1).
Usually, everything that happens is a single operation. This is one of the reason we don't actually count the exact number of them, but instead use asymptotic notations (big O and big Theta).
However, sometimes you are interested about one kind of operation only. A common example is algorithms that use IO. Since IO is significantly more time consuming than anything happening on the CPU, you often just "count" the number of IO operations instead. In these cases, you often actually care about exact number of times an IO occurs, and can't use only asymptotic notations.

What is the time complexity of nested loop over single loop with same task

So, what is the difference between given below two function in terms of performance and what is the time complexity of both function. It is doing exactly same task with two loop and single loop.
With TWO Loop.
RecipeModel returnRecipe(String? suggestion) {
for (int i = 0; i < _appData.recipeCategories!.length; i++) {
for (int j = 0; j < _appData.recipeCategories![i].recipes!.length; j++) {
if (_appData.recipeCategories![i].recipes![j].recipeName! ==
suggestion) {
return _appData.recipeCategories![i].recipes![j];
}
}
}
return recipe;
}
With Single loop
RecipeModel returnRecipe(String? suggestion) {
int recCategoriesLen = _appData.recipeCategories!.length;
int i = 0
for (int j = 0; j < _appData.recipeCategories![i].recipes!.length;) {
if (_appData.recipeCategories![i].recipes![j].recipeName! ==
suggestion) {
return _appData.recipeCategories![i].recipes![j];
}
j++
if (_appData.recipeCategories![i].recipes!.length == j && i < recCategoriesLen - 1) {
i++
j = 0
}
}
return recipe;
}
It's common for people, when first learning about big-O notation, to assume that big-O notation is calculated by looking at how many loops there are and then multiplying those loops together in some way. While that's often the case, the real guiding principle behind big-O analysis is to think through, conceptually, what it is that the loops are actually doing.
In your case, you have a 2D array of items indexed by i and j. The first version of the code explicitly enumerates all pairs of possible i's and j's, with the outer loop visiting all choices of i and the inner loop visiting all choices of j. The total work done is then proportional to the number of items visited.
The second loop does essentially the same thing, but less explicitly. Specifically, it still generates all possible combinations of i and j, except that there's just a single loop driving the changes to both i and j. Because you're still iterating over all choices, the amount of work done is likely to be pretty similar to what you started with. The actual performance will depend on what optimizations the compiler/interpreter does for you.
To actually reduce the amount of work you're doing, you'll need to find a fundamentally different strategy. Since you're asking the question "across all combinations of i and j, does this item exist?," you might want to store an auxiliary hash table (dictionary) that stores each recipe keyed by its name. That way, you don't need to loop over everything and can instead just do a dictionary lookup, which is much faster.

Runtime of this Program

I'm currently in an Intro to Java course and studying for a midterm. I came across this problem:
public void wug() {
int j = 0;
for (int i = 0; i < N; i += 1) {
for (; j < M; j += 1) {
if (bump(i, j))
break;
}
}
}
N and M are trivial, and are provided somewhere else.
The solution says the runtime if theta(M+N) for the worst case, and theta(N) for the best case. I understand the best case, but I thought the worst case was theta(N*M). Could someone explain why the worst case is theta(M+N)? I'm really shaky on algorithm complexity. Thank you!
Note that j is never reset, so the inner loop iterates at most M times. To get N*M iterations you'd have to reset the iterator to zero at the start of the loop.
Note that j isn't initialised in the inner loop so each execution of the inner loop continues from where the previous loop exited. Think through how that changes the different values that j takes as the program executes.
You'll gain better understanding of this code by setting it up in a debugger and single-stepping through it. That's because you're seeing what you think the code is doing, not what's actually happening. Single-stepping the code helps you to focus on the details that make the difference.

How to determine computational complexity for algorithms with nested loops?

After looking at this question, this article, and several other questions, I have still not been able to find a general way to determine the computational complexity of algorithms with looping variables dependent on the parent loop's variable. For example,
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
for (int k = i; k < j; k++) {
//one statement
}
}
}
I know that the first loop has a complexity of n, but the inner loops are confusing me. The second loop seems to be executed n-i times and the third loop seems to be executed j-i times. However, I'm not sure how to turn this into a regular Big-O statement. I don't think I can say O(n(n-i)(j-i)), so how can I get rid of the i and j variables here?
I know this is something on the order of n^3, but how can I show this? Do I need to use series?
Thanks for your help!
(If you were wondering, this is from a brute force implementation of the maximum sum contiguous subsequence problem.)
First loop hits N items on average.
Second loop hits N / 2 items on avarage
Third loop hits N / 4 items on average
O(N * N / 2 * N / 4) is about O((N^3)/8) is about O(N^3)

Efficiency of nested for-loops with vastly different counts

Given a is much larger than b, would
for (i = 0; i < a; i++)
for (k = 0 k < b; k++)
be faster than
for (i = 0; i < b; i++)
for (k = 0 k < a; k++)
It feels to me the former would be faster but I cannot seem to get my head around this.
Well it really depends on what your doing. It's hard to do runtime analysis without knowing what's being done. That being said, if your using this code to traverse through a large array, its more important to go through each column in each row rather than visa-versa.
[0][1][2]
[3][4][5]
[6][7][8]
is really [0][1][2][3][4][5][6][7][8] in memory.
Your computer's cache provides a greater advantage when memory access is close together, and going sequentially though memory rather than skipping through rows provide much more locality.
Starting a loop takes effort; there's the loop variable itself plus al the variables declared within the loop, which are all allocated memory and pushed onto the stack.
This means the fewer times you enter a loop the better, so loop over the smaller range in the outer loop.

Resources