If you want to analyze these algorithms you need to define //dostuff, as that can really change the outcome. Let's suppose dostuff requires a constant O(1) number of operations.
Here are some examples with this new notation:
For your first example, the linear traversal: this is correct!
O(N):
for (int i = 0; i < myArray.length; i++) {
myArray[i] += 1;
}
Why is it linear (O(n))? As we add additional elements to the input (array) the amount of operations happening increases proportional to the number of elements we add.
So if it takes one operation to increment an integer somewhere in memory, we can model the work the loop does with f(x) = 5x = 5 additional operations. For 20 additional elements, we do 20 additional operations. A single pass of an array tends to be linear. So are algorithms like bucket sort, which are able to exploit the structure of data to do a sort in one single pass of an array.
Your second example would also be correct and looks like this:
O(N^2):
for (int i = 0; i < myArray.length; i++) {
for (int j = 0; j < myArray.length; j++) {
myArray[i][j] += 1;
}
}
In this case, for every additional element in the first array, i, we have to process ALL of j. Adding 1 to i actually adds (length of j) to j. Thus, you are correct! This pattern is O(n^2), or in our example it is actually O(i * j) (or n^2 if i == j, which is often the case with matrix operations or a square data structure.
Your third example is where things change depending on dostuff; If the code is as written and do stuff is a constant, it is actually only O(n) because we have 2 passes of an array of size n, and 2n reduces to n. The loops being outside each other isn't the key factor which can produce 2^n code; here is an example of a function which is 2^n:
var fibonacci = function (n) {
if (n == 1 || n == 2) {
return 1;
}
else {
return (fibonacci(n-2) + fibonacci(n-1));
}
}
This function is 2^n, because each call to the function produces TWO additional calls to the function (Fibonacci). Every time we call the function, the amount of work we have to do doubles! This grows super quickly, like cutting the head off of a hydra and having two new ones sprout each time!
For your final example, if you are using an nlgn sort like merge-sort you are correct that this code will be O(nlgn). However, you can exploit the structure of the data to develop faster sorts in specific situations (such as over a known, limited range of values like from 1-100.) You are correct in thinking, however, that the highest order code dominates; so if a O(nlgn) sort is next to any operation that takes less than O(nlgn) time, the total time complexity will be O(nlgn).
In JavaScript (in Firefox at least) the default sort in Array.prototype.sort() is indeed MergeSort, so you are looking at O(nlgn) for your final scenario.
2N
in big-O notation.