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

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.

Related

How to better analyze runtime of complex nested loops?

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.)

Time Complexity of Finding All Possible Sub Array of an Array

As per the implementation of finding all possible sub-array from a given array as follows:
public class AllPossibleSubArray {
public static void main(String[] args) {
int[] arr = { 1, 2, 3 };
List<List<Integer>> result = new ArrayList<>();
for (int len = 0; len <= arr.length; len++) {
if (len == 0) {
result.add(new ArrayList<>());
} else {
for (int i = 0; i < arr.length - len + 1; i++) {
List<Integer> temp = new ArrayList<>();
for (int j = i; j < i + len; j++) {
temp.add(arr[j]);
}
result.add(temp);
}
}
}
result.forEach(System.out::println);
}
As per my understanding that time complexity would be O(N^3) as there are three FOR loop.
But this problem is nothing but a power set i.e. finding all possible sub set from a given set. From various forum on web, time complexity of power set is 2^N (Binomial expansion) which is not same as O(N^3).
Am i missing some fundamental ?
But this problem is nothing but a power set i.e. finding all possible sub set from a given set.
That's not correct.
The code that you've posted only finds contiguous subarrays, meaning the list of all elements from one index to another index.
The power set, by contrast, would also include discontiguous subsequences, meaning ones that include two elements without including all of the elements between them.
I should also note that there are only O(n2) subarrays, and if you find a different way to represent them, you can find them in O(n2) time rather than O(n3) time as the code that you've posted does. (Specifically, you need a representation that allows you to reuse the shared parts of the lists, rather than having to copy all required elements every time.) By contrast, if you stick with the representation in your code where each list has a distinct copy, finding all subsets would actually require O(n·2n) time, rather than just O(2n) time.
Like the post above said the optimal time complexity for getting all the possible subarrays in an array is O(N^2). I would also like to point out that by definition a subarray is contiguous. check the following link for the definition of a sub array.
https://www.google.com/url?sa=t&source=web&rct=j&url=https://www.geeksforgeeks.org/subarraysubstring-vs-subsequence-and-programs-to-generate-them/amp/&ved=2ahUKEwiI8bOC9I76AhWRSvEDHWubCNAQFnoECAcQBQ&usg=AOvVaw1i1BkpzTxhLqu0mfVhCN87

Number of Primitive operations in IF Statement

I am new to Design and Analysis of Algorithms. I have a nested loop and and if statement.I am unable to determine the primitive operations being done in if statement. The statements are as follows:
for (i=0;i<n;i++)
for(j=0;j<n;j++)
if(i!=j and A[i]==A[j])
duplicate=true
break;
if(duplicate)
break;
i am determining the No of operations in if statement as follows:
Accessing array element 2 Times
comparing i and J
Comparing A[i] and A[j]
Comparing AND Operator
all this is being done N times. Am i right in guessing the number of primitive operations in if statement? if not then please help me correct this. Thanks
Technically, we can't determine how many times this will happen since it depends on the contents of the array. Once a duplicate is found, the whole thing terminates. We'd have to see the complete contents of all of the elements from A[0] through A[n-1] to provide an exact number.
What you can say is that, at most (with no duplicates in A[]), the entire code snippet will run n2 times. Consequently, the rest of this assumes no duplicates:
Comparing i and j
This comparison will occur n2 times (again, assuming no duplicates to break the loops).
Accessing array element 2 times
Comparing AND operator
Comparing A[i] and A[j]
These other operations only happen if i != j is true, because most modern languages "short circuit". When that first condition fails (in other words, when i and j are the same) the rest of the if condition is ignored. The AND is never processed, the array elements are not accessed, and the array elements are not compared.
Therefore, these will run n2-n times. The -n is because each j loop will always match the i iterator for exactly one value, short circuiting the rest of the if condition.
I'm going to just assume a C-like language and correctly format the code as if this were a C# snippet:
// n and A[] initialized elsewhere
bool duplicate = false;
for(int i = 0; i < n; i++)
{
for(int j = 0; j < n; j++)
{
if(i != j && A[i] == A[j])
{
duplicate = true;
break;
}
}
if(duplicate)
{
break;
}
}

Give an analyze an O(n) algorithm to check for nuts/bolts matches in sorted arrays

I have a collection of n nuts, and a collection of n bolts, both arranged in increasing order of size. I need to give an O(n) algorithm to check if
a nut and bolt have the same size. Nut sizes are in an array NUTS[1..n]
bolt sizes are in an array BOLTS[1...n]
I don't need to find all matches, just stop the algorithm even if one match is found. Here what my psuedocode looks like so far
for each n in NUTS
for each b in BOLTS
if BOLTS[i] == NUTS[i]
break;
So for each nut, I'm searching through all the bolts. This is O(n^2), How would I make this O(n)? My understanding is I would probably need to have just one for loop to do this. Sorry my understanding is a little shaky
You can step through the two arrays in the same loop, but using two separate index variables. You always increment the index variable for which the array value is smaller. If neither is smaller, they're equal and you've found a match.
i = 0;
j = 0;
while (i < n and j < n) {
if (NUTS[i] < BOLTS[j]) {
i++;
} else if (NUTS[i] > BOLTS[j]) {
j++;
} else {
return true; // found a match
}
}
return false; // found nothing
The best case is when NUTS[0] == BOLTS[0], because then you enter the final else clause and return true immediately.
The worst case is when you never enter the final else clause, because then you have to iterate the loop all the way to the end. This happens if you always take the first or second if clause, incrementing either i or j. In the worst case you alternate between incrementing i and j, making both of them grow as slowly as possible. This leads to a worst case of 2*n steps before at least one variable exceeds n.
Therefore this algorithm is in O(2*n), which is O(n).
The pseudo-code shared will take O(n^2) in worst case, which is when there is no matching pair.
However you can speed this up to O(nlogn) by using binary search, as the arrays are in sorted order.
foreach A in nuts:
if binarysearch ( Bolts, Bolts+n , A)
break;
Also you can speed this upto O(n) as well using two pointers one in each array and increment them accordingly.
Bolt_index = Nut_index = 0
while ( Bolt_Index < Bolts.size && Nut_Index < Nuts.Size)
while( Bolt_Index < Bolts.Size && Bolts[Bolt_index] < Nuts[Nut_Index] )
Bolt_index++ ;
while( Nut_Index < Nuts.Size && Nuts[Nut_index] < Bolts[Bolt_Index] )
Nut_index++ ;
if Bolt_Index< Bolts.Size && Nut_Index < Nuts.Size && Bolts[Bolt_Index] == Nuts[Nut_Index]
Match_found// break
else
Nut_Index++ // increment either Nut_Index or Bolt_Index

Why should Insertion Sort be used after threshold crossover in Merge Sort

I have read everywhere that for divide and conquer sorting algorithms like Merge-Sort and Quicksort, instead of recursing until only a single element is left, it is better to shift to Insertion-Sort when a certain threshold, say 30 elements, is reached. That is fine, but why only Insertion-Sort? Why not Bubble-Sort or Selection-Sort, both of which has similar O(N^2) performance? Insertion-Sort should come handy only when many elements are pre-sorted (although that advantage should also come with Bubble-Sort), but otherwise, why should it be more efficient than the other two?
And secondly, at this link, in the 2nd answer and its accompanying comments, it says that O(N log N) performs poorly compared to O(N^2) upto a certain N. How come? N^2 should always perform worse than N log N, since N > log N for all N >= 2, right?
If you bail out of each branch of your divide-and-conquer Quicksort when it hits the threshold, your data looks like this:
[the least 30-ish elements, not in order] [the next 30-ish ] ... [last 30-ish]
Insertion sort has the rather pleasing property that you can call it just once on that whole array, and it performs essentially the same as it does if you call it once for each block of 30. So instead of calling it in your loop, you have the option to call it last. This might not be faster, especially since it pulls the whole data through cache an extra time, but depending how the code is structured it might be convenient.
Neither bubble sort nor selection sort has this property, so I think the answer might quite simply be "convenience". If someone suspects selection sort might be better then the burden of proof lies on them to "prove" that it's faster.
Note that this use of insertion sort also has a drawback -- if you do it this way and there's a bug in your partition code then provided it doesn't lose any elements, just partition them incorrectly, you'll never notice.
Edit: apparently this modification is by Sedgewick, who wrote his PhD on QuickSort in 1975. It was analyzed more recently by Musser (the inventor of Introsort). Reference https://en.wikipedia.org/wiki/Introsort
Musser also considered the effect on caches of Sedgewick's delayed
small sorting, where small ranges are sorted at the end in a single
pass of insertion sort. He reported that it could double the number of
cache misses, but that its performance with double-ended queues was
significantly better and should be retained for template libraries, in
part because the gain in other cases from doing the sorts immediately
was not great.
In any case, I don't think the general advice is "whatever you do, don't use selection sort". The advice is, "insertion sort beats Quicksort for inputs up to a surprisingly non-tiny size", and this is pretty easy to prove to yourself when you're implementing a Quicksort. If you come up with another sort that demonstrably beats insertion sort on the same small arrays, none of those academic sources is telling you not to use it. I suppose the surprise is that the advice is consistently towards insertion sort, rather than each source choosing its own favorite (introductory teachers have a frankly astonishing fondness for bubble sort -- I wouldn't mind if I never hear of it again). Insertion sort is generally thought of as "the right answer" for small data. The issue isn't whether it "should be" fast, it's whether it actually is or not, and I've never particularly noticed any benchmarks dispelling this idea.
One place to look for such data would be in the development and adoption of Timsort. I'm pretty sure Tim Peters chose insertion for a reason: he wasn't offering general advice, he was optimizing a library for real use.
Insertion sort is faster in practice, than bubblesort at least. Their asympotic running time is the same, but insertion sort has better constants (fewer/cheaper operations per iteration). Most notably, it requires only a linear number of swaps of pairs of elements, and in each inner loop it performs comparisons between each of n/2 elements and a "fixed" element that can be stores in a register (while bubble sort has to read values from memory). I.e. insertion sort does less work in its inner loop than bubble sort.
The answer claims that 10000 n lg n > 10 n² for "reasonable" n. This is true up to about 14000 elements.
I am surprised no-one's mentioned the simple fact that insertion sort is simply much faster for "almost" sorted data. That's the reason it's used.
The easier one first: why insertion sort over selection sort? Because insertion sort is in O(n) for optimal input sequences, i.e. if the sequence is already sorted. Selection sort is always in O(n^2).
Why insertion sort over bubble sort? Both need only a single pass for already sorted input sequences, but insertion sort degrades better. To be more specific, insertion sort usually performs better with a small number of inversion than bubble sort does. Source This can be explained because bubble sort always iterates over N-i elements in pass i while insertion sort works more like "find" and only needs to iterate over (N-i)/2 elements in average (in pass N-i-1) to find the insertion position. So, insertion sort is expected to be about two times faster than insertion sort on average.
Here is an empirical proof the insertion sort is faster then bubble sort (for 30 elements, on my machine, the attached implementation, using java...).
I ran the attached code, and found out that the bubble sort ran on average of 6338.515 ns, while insertion took 3601.0
I used wilcoxon signed test to check the probability that this is a mistake and they should actually be the same - but the result is below the range of the numerical error (and effectively P_VALUE ~= 0)
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int j = i;
while (j > 0 && arr[j-1] > arr[j]) {
swap(arr, j, j-1);
j--;
}
}
}
public static void bubbleSort(int[] arr) {
for (int i = 0 ; i < arr.length; i++) {
boolean bool = false;
for (int j = 0; j < arr.length - i ; j++) {
if (j + 1 < arr.length && arr[j] > arr[j+1]) {
bool = true;
swap(arr,j,j+1);
}
}
if (!bool) break;
}
}
public static void main(String... args) throws Exception {
Random r = new Random(1);
int SIZE = 30;
int N = 1000;
int[] arr = new int[SIZE];
int[] millisBubble = new int[N];
int[] millisInsertion = new int[N];
System.out.println("start");
//warm up:
for (int t = 0; t < 100; t++) {
insertionSort(arr);
}
for (int t = 0; t < N; t++) {
arr = generateRandom(r, SIZE);
int[] tempArr = Arrays.copyOf(arr, arr.length);
long start = System.nanoTime();
insertionSort(tempArr);
millisInsertion[t] = (int)(System.nanoTime()-start);
tempArr = Arrays.copyOf(arr, arr.length);
start = System.nanoTime();
bubbleSort(tempArr);
millisBubble[t] = (int)(System.nanoTime()-start);
}
int sum1 = 0;
for (int x : millisBubble) {
System.out.println(x);
sum1 += x;
}
System.out.println("end of bubble. AVG = " + ((double)sum1)/millisBubble.length);
int sum2 = 0;
for (int x : millisInsertion) {
System.out.println(x);
sum2 += x;
}
System.out.println("end of insertion. AVG = " + ((double)sum2)/millisInsertion.length);
System.out.println("bubble took " + ((double)sum1)/millisBubble.length + " while insertion took " + ((double)sum2)/millisBubble.length);
}
private static int[] generateRandom(Random r, int size) {
int[] arr = new int[size];
for (int i = 0 ; i < size; i++)
arr[i] = r.nextInt(size);
return arr;
}
EDIT:
(1) optimizing the bubble sort (updated above) reduced the total time taking to bubble sort to: 6043.806 not enough to make a significant change. Wilcoxon test is still conclusive: Insertion sort is faster.
(2) I also added a selection sort test (code attached) and compared it against insertion. The results are: selection took 4748.35 while insertion took 3540.114.
P_VALUE for wilcoxon is still below the range of numerical error (effectively ~=0)
code for selection sort used:
public static void selectionSort(int[] arr) {
for (int i = 0; i < arr.length ; i++) {
int min = arr[i];
int minElm = i;
for (int j = i+1; j < arr.length ; j++) {
if (arr[j] < min) {
min = arr[j];
minElm = j;
}
}
swap(arr,i,minElm);
}
}
EDIT: As IVlad points out in a comment, selection sort does only n swaps (and therefore only 3n writes) for any dataset, so insertion sort is very unlikely to beat it on account of doing fewer swaps -- but it will likely do substantially fewer comparisons. The reasoning below better fits a comparison with bubble sort, which will do a similar number of comparisons but many more swaps (and thus many more writes) on average.
One reason why insertion sort tends to be faster than the other O(n^2) algorithms like bubble sort and selection sort is because in the latter algorithms, every single data movement requires a swap, which can be up to 3 times as many memory copies as are necessary if the other end of the swap needs to be swapped again later.
With insertion sort OTOH, if the next element to be inserted isn't already the largest element, it can be saved into a temporary location, and all lower elements shunted forward by starting from the right and using single data copies (i.e. without swaps). This opens up a gap to put the original element.
C code for insertion-sorting integers without using swaps:
void insertion_sort(int *v, int n) {
int i = 1;
while (i < n) {
int temp = v[i]; // Save the current element here
int j = i;
// Shunt everything forwards
while (j > 0 && v[j - 1] > temp) {
v[j] = v[j - 1]; // Look ma, no swaps! :)
--j;
}
v[j] = temp;
++i;
}
}

Resources