Running Time & Space Complexity Modified Mergesort - algorithm

This is a homework question for a data structures and algorithms course. I'm not looking to have anyone do my homework for me. Just hoping someone can tell me if I'm approaching this appropriately.
public static void sort(int[] a) {
sort(a, 0, a.length - 1);
}
private static void sort(int[] a, int lo, int hi) {
if (hi <= lo) return;
int[] aux = new int[a.length];
int mid = (hi + lo) / 2;
sort(a, lo, mid);
sort(a, mid + 1, hi);
merge(a, lo, mid, hi, aux);
}
private static void merge(int[] a, int lo, int mid,
int hi, int[] aux) {
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++)
aux[k] = a[k];
for (int k = lo; k <= hi; k++) {
if (i > mid)
a[k] = aux[j++];
else if (j > hi)
a[k] = aux[i++];
else if(aux[j] < aux[i])
a[k] = aux[j++];
else
a[k] = aux[i++];
}
}
The only difference between this implementation and the typical implementation (that has been given in our class), is that the aux array is redefined on every recursive call to sort, versus only being defined once in the public sort method in the typical case. The typical case has running time O(nlog(n)), and a space complexity of O(n).
My task is to determine the running time and space complexity of the shown modified implementation of mergesort. As far as I can tell, the running time is not changed, so it is still O(nlog(n)), and the space complexity is also O(nlog(n)). My logic in coming to this conclusion is that the sort method allocates an array of size n each time it is called, and it is called a total of log(n) times.
So far I've had a difficult time wrapping my head around space and time complexities. Am I thinking about this one correctly, or am I way off?
Any pointers greatly appreciated.

You are right, the space complexity is O(n*logn), but it needs some clarification.
My logic in coming to this conclusion is that the sort method
allocates an array of size n each time it is called, and it is called
a total of log(n) times.
Actually, sort is called a total of n times during the mergesort, but the maximum recursion depth is logn during the process. Let's draw a tree of recursive calls:
sort
/ \
sort sort
/\ /\
sort sort sort sort
...
On each level, each sort function performs half of the work of its parent. So, on level 0 (root node), sort has n iterations, on level 1 each of two sorts has n/2 running time (together it is n), on level 2 each of four sorts has n/4 etc. Combined, each level does n iterations, and since the depth of the tree is log n, you get O(n*logn) time complexity.
However, in your case aux allocates n elements, regardless of the current depth, and there are n invocations of sort, so at the first glance one could conclude that the space complexity is O(n*n) = O(n^2). It is not because once we are finished with each recursive call, allocated space is freed, and there are maximum of logn recursive calls.
Notice that aux unnecessary allocates array of n elements each time. You can improve the algorithm if you declare aux in the merge function and set the appropriate length, something like this:
private static void merge(int[] a, int lo, int mid, int hi) {
int[] aux = new int[hi - lo + 1];
int i = lo, j = mid + 1;
for (int k = lo; k <= hi; k++)
aux[k - lo] = a[k];
for (int k = lo; k <= hi; k++) {
if (i > mid)
a[k] = aux[j++ - lo];
else if (j > hi)
a[k] = aux[i++ - lo];
else if(aux[j - lo] < aux[i - lo])
a[k] = aux[j++ - lo];
else
a[k] = aux[i++ - lo];
}
}
HTH.

Related

Merge sort gives poor efficiency and isn't affected by compiler optimizations

While trying to measure the time various sorting algorithms require to sort a random array of unsigned integers, I've obtained some peculiar behavior regarding top-down Merge sort that doesn't seem to be caused by bad implementation.
On arrays of length up to 1 million values, Merge sort behaves a lot worst than random-pivot Quicksort and even Shell sort. This is unexpected so I've tried with multiple online implementations of Merge sort but the result still seems to be about the same.
Graph 1, optimizations ON
This is the implementation I used for these graphs:
void merge(int *array, int l, int m, int r) {
int i, j, k, nl, nr;
nl = m - l + 1; nr = r - m;
int *larr = new int[nl], *rarr = new int[nr];
for (i = 0; i < nl; i++)
larr[i] = array[l + i];
for (j = 0; j < nr; j++)
rarr[j] = array[m + 1 + j];
i = 0; j = 0; k = l;
while (i < nl && j < nr) {
if (larr[i] <= rarr[j]) {
array[k] = larr[i];
i++;
}
else {
array[k] = rarr[j];
j++;
}
k++;
}
while (i < nl) {
array[k] = larr[i];
i++; k++;
}
while (j < nr) {
array[k] = rarr[j];
j++; k++;
}
delete[] larr;
delete[] rarr;
}
void mergeSort(int *array, int l, int r) {
if (l < r) {
int m = l + (r - l) / 2;
mergeSort(array, l, m);
mergeSort(array, m + 1, r);
merge(array, l, m, r);
}
}
I have also tried to remove compiler optimizations (VisualC++15), favoring size instead of speed and this seem to have affected all the other algorithms instead of Merge sort. Nonetheless, it still got the worst time.
Graph 2, optimizations OFF
The only time Merge sort didn't give the worst time was on a test with arrays of 15 million elements where it got just a slightly better performance than Heap sort, but still far from the others.
The values that I plot are the averages of 100 tests with random arrays so I don't think this is just a particular case. I also don't think the use of dynamic memory in Merge sort is the cause of these results, 16GB of RAM are plenty for these tests and everything else.
Does anybody know why Merge sort behaves so badly and why compiler optimizations don't seem to affect Merge sort?

First missing Integer approach's time complexity

I want to understand the time complexity of my below algorithm, which is an acceptable answer for the famous first missing integer problem:
public int firstMissingPositive(int[] A) {
int l = A.length;
int i = 0;
while (i < l) {
int j = A[i];
while (j > 0 && j <= l) {
int k = A[j - 1];
A[j - 1] = Integer.MAX_VALUE;
j = k;
}
i++;
}
for (i = 0; i < l; i++) {
if (A[i] != Integer.MAX_VALUE)
break;
}
return i + 1;
}
Observations and findings:
Looking at the loop structure I thought that the complexity should be more than n as I may visit every element more than twice in some cases. But to my surprise, the solution got accepted. I am not able to understand the complexity.
You are probably looking at the nested loops and thinking O(N2), but it's not that simple.
Every iteration of the inner loop changes an item in A to Integer.MAX_VALUE, and there are only N items, so there cannot be more than N iterations of the inner loop in total.
The total time is therefore O(N).

Kth largest number, why the runtime of this is O(n) not O(nlogn)

I came across kth largest number problem in Leetcode
Input: [3,2,1,5,6,4] and k = 2, Output: 5
Suggested Solution:
public int findKthLargest(int[] nums, int k) {
shuffle(nums);
k = nums.length - k;
int lo = 0;
int hi = nums.length - 1;
while (lo < hi) {
final int j = partition(nums, lo, hi);
if(j < k) {
lo = j + 1;
} else if (j > k) {
hi = j - 1;
} else {
break;
}
}
return nums[k];
}
private int partition(int[] a, int lo, int hi) {
int i = lo;
int j = hi + 1;
while(true) {
while(i < hi && less(a[++i], a[lo]));
while(j > lo && less(a[lo], a[--j]));
if(i >= j) {
break;
}
exch(a, i, j);
}
exch(a, lo, j);
return j;
}
private void exch(int[] a, int i, int j) {
final int tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
private boolean less(int v, int w) {
return v < w;
}
Doesn't partition take O(n) and the while loop in the main function take O(log n) so it should be O(nlog n)? This looks like it uses Quicksort but the runtime for quicksort is O(nlogn). If quicksort takes O(n), this makes sense but it does not. Please help me understand what is going on?
This is a randomized algorithm that has average/expected O(n) runtime. This is because after randomly shuffling the input list, we typically have pivots good enough to expect that after each partition function call if we don't find the target yet we reduce our list (to be search next) roughly by half. This means even though if we not lucky and have to continuously call partition function we continuously keep reducing our list's size by half, therefore the average runtime is still only O(n) since O(n) + O(n/2) + O(n/4) + ... + O(1) is still O(n).

How Does This String Permutation Works

Need help understanding the correctness of the second swap call.
/* Function to print permutations of string
This function takes three parameters:
1. String
2. Starting index of the string
3. Ending index of the string. */
void permute(char *a, int i, int n)
{
int j;
if (i == n)
printf("%s\n", a);
else
{
for (j = i; j <= n; j++)
{
swap((a+i), (a+j));
permute(a, i+1, n);
swap((a+i), (a+j)); // how does this work here?
}
}
}
It seems like the second swap is to undo the first swap. But I don't see a proof why the in-between permute call would preserve that the original *(a+i) would remain at a+j.
Notes:
[1] Code was found at http://www.geeksforgeeks.org/write-a-c-program-to-print-all-permutations-of-a-given-string/
Proposition: For all a of length > n (so that n is a valid index) and 0 <= i <= n, when
permute(a, i, n)
returns, a is the same as when permute was called.
Proof: (Induction start) If i == n, then permute(a, n, n); only prints the string and doesn't change it, hence the proposition holds in that case.
(Induction hypothesis) Let 0 <= k < n, and the enoncé of the proposition hold for all k < i <= n.
Then in the loop
for (j = i; j <= n; j++)
{
swap((a+i), (a+j));
permute(a, i+1, n);
swap((a+i), (a+j)); // how does this work here?
}
for each j, the recursive call to permute doesn't change the contents [more precisely, it undoes all changes intermediately done] per the hypothesis. Therefore, before the second swap, the contents of a[i] and a[j] are exactly what they were after the first swap, hence at the end of the loop body, the contents of a are exactly what they were when the loop body was entered.

Efficient Mergesort Confusion

I have been reading a mergesort example (the efficient one) since yesterday and I still can't understand how it works despite looking at the code:
private static void sort(int[] list) {
a = list;
int n = a.length;
// according to variant either/or:
b = new int[n];
b = new int[(n + 1) / 2];
mergesort(0, n - 1);
}
private static void mergesort(int first, int last) {
if (first < last) {
int mid = (first + last) / 2;
mergesort(first, mid);
mergesort(mid + 1, last);
merge(first, mid, last);
}
}
No problem understanding the algorithm up until this point but the confusion is in the following method:
private static void merge(int first, int mid, int last) {
int i, j, k;
i = 0;
j = first;
while (j <= mid)
b[i++] = a[j++]; // *j's value is now mid*
i = 0; // *i is reset to 0, nothing's been done to j*
k = first;
// *before entering the following while loop, j still carries mid's value*
while (k < j && j <= last)
if (b[i] <= a[j])
a[k++] = b[i++];
else
a[k++] = a[j++];
// copy back remaining elements of first half (if any)
while (k < j)
a[k++] = b[i++];
}
Entering the second while loop while (k < j && j <= last) is where I don't understand how this sorting works. From what I understood, the first half of the array a is already copied to the auxiliary array b, and now we want to arrange the entire array by comparing a[j++] (the second half) to the auxiliary array b[i++] so that we can get the smaller array element and place it in array a to sort the array in ascending order.
But why while (k < j && j <= last)? k < j sounds logical enough because we need to get all the values back from the auxiliary array but why j <= last? And why can't we just do while (k <= last) ?
And also, could somebody please affirm that my understanding of j's value in the above code is correct?
k < j denotes that auxillary array b still contains elements
j <= last denotes that the second part of a still contains elements
We cannot use k <= last here, because we may access array a indexes beyond the border, when j becomes last+1
Too long for comments, added here:
This variant is useful when available memory is limited (large dataset). It is mentioned in some tutorials (I've met it in J.Bucknall book about algorithms in Delphi). It is stable ( if (b[i] <= a[j]) holds stability. It is usually not faster, because it is better not to copy data at all , but, for example, 'trigger' source and destination array (pointers) at every stage

Resources