So I got into a friendly argument with the lecturer on data structures, regarding the time complexity of a heapsorting function. I hadn't previously seen a heapsort algorithm before but I was familiar with the structure of min and max heaps and the standard enqueue/dequeue functions, so I decided to take a stab at it. Now memory-wise, it's worse than Wikipedia's (O(n) extra memory required compared to O(1)), but I have assumed that memory isn't an issue and therefore I will only focus on the time complexity.
Pseudocode(ish) for some integer array with size N:
int[] toSort = new int[N];
MinHeap heap = new MinHeap(toSort.length);//Standard binary minimum heap
for(int i : toSort) {
heap.enqueue(i);
}
for(int i = 0; i < toSort.length; i++) {
toSort[i] = heap.dequeue();
}
The lecturer told me that this is still O(N*log2 N) time complexity, I would like to dispute that.
Now, let's start by looking at the enqueue-function. What's the worst case scenario? That would be O(log2 N) i.e. moving the element to the leaf level. From here, it's easy to see how one can say that the worst case time complexity for the entire function would be O(N*log2 N) as you can derive it from 2*O(N)*(log2 N) based on the standard operations with Big-O notation. However, since O(log2 N) is the worst case scenario i.e. the upper bound for enqueue (and dequeue), one can write the exact worst case time complexity as T(N) = a*log2(b*N) + T0(N), where a,b are positive non-zero constants and T0(N) is a collection of terms whose time complexity is less than O(log2 N). Note that for N=0, T(0) = 1, since enqueue on an empty heap is constant. It also avoids the singularity.
What I've done here is try to recreate the exact time function while at the same time accounting for information that is lost when using Big-O notation. Note that the dequeue function has an exact time function similar in structure to the one mentioned.
What is the exact worst case time complexity of the first for-loop? Let's call it T1(N).
T1(N) = T(0) + T(1) + T(2) + T(3) + ... T(N-2) + T(N-1) (Note that there is no T(N)-term as we would have nothing more to add.)
This can be rewritten:
T1(N) = i=0ΣN-1 T(i) = 1 + i=1ΣN-1 T(i) = 1 + i=1ΣN-1 (a*log2(b*i) + T0(i))
= 1 + a*i=1ΣN-1 log2(b*i) + i=1ΣN-1T0(i) (Using the logarithmic identity log(a*b) = log(a) + log(b), we get the following)
= 1 + a*log2(i=1∏N-1b*i) + i=1ΣN-1T0(i)
= 1 + a*log2(bN-1*i=1∏N-1i) + i=1ΣN-1T0(i) (Identity: i=1∏nn = n!)
= 1 + a*log2(bN-1*(N-1)!) + i=1ΣN-1T0(i) (Let T*(N) = i=1ΣN-1T0(i))
= 1 + a*log2(bN-1*(N-1)!) + T*(N)
= 1 + a*(log2(bN-1) + log2((N-1)!)) + T*(N) (Identity: loga(b) = logc(d)/logc(a))
= 1 + a*(logb(bN-1)/logb(2) + log2((N-1)!)) + T*(N) (Identity: loga(ab) = b)
= 1 + a*((N-1)/logb(2) + log2((N-1)!)) + T*(N)
= 1 + a(N-1)/logb(2) + a*log2((N-1)!) + T*(N)
Now what happens to T1(N) when N grows large?
O(1 + a(N-1)/logb(2) + a*log2((N-1)!) + T*(N)) (constant terms are dropped due to relative size)
= O(a(N-1)/logb(2) + a*log2((N-1)!) + T*(N))
= O(a*N/logb(2) + a*log2(N!) + T*(N)) (constants coefficients are dropped as the order of magnitude is still the same)
= O(N + log2(N!) + T*(N))
Which of these last terms have the largest order of magnitude? We know that log2(N!) grows faster than N, so N is dropped. What about T*(N)? It has an upper bound of (N-1)*T0(N), and
T0(N) is the collection of terms with an order of magnitude smaller than log2(n) so it can be reasoned that log2(N!) has a larger order of magnitude.
Thus, O(log2 N!) is the time complexity for the first loop and, WLOG, second loop.
Worst case then, this sorting function has a time complexity of 2*O(log2 N!) = O(log2 N!), which is better than O(N*log2 N)
Now I'm no computer scientist nor am I a computer engineer (yet), so I ask you all: Am I onto something, or am I just narrowing the complexity to a more exact value? Or is there an error in my reasoning?