This repository currently only features a few select sorting algorithms. They are:
- Insertion Sort
- Selection Sort
- Merge Sort
- Heap Sort
- Smooth Sort
- Tim Sort
In hindsight, smooth sort was the most difficult to implement, but it also brought me the most insights and enjoyment. Because the implementations of the other algorithms are already widely-known, I place an emphasis on explaining the mechanism of smooth sort below and combine the others into a single section: Other Algorithms. However, I've elected to devote a separate section for Heap Sort; this precedes my discussion of smooth sort (it'll become clear why I decided to explain this too).
The following report will also outline the methods that were used to benchmark the sorting algorithms. After much research, I decided to procedurally generate custom data sets (still using the struct Record
provided by the starter code). These were created using a number of parameters (particularly, the number of records (
Compiling the program is relatively straightforward. Just use gcc
and type the command gcc main.c -o main
. The resulting executable main
can then be run. For linux users, I do leave another small but important note as the math library is not linked by default: if you are using a Linux device, run gcc main.c -o main -lm
instead.
Depending on what exactly you want to run, the program accepts a number of different command-line arguments. They are listed below.
-
algos
, the algorithms to run, default isinsertion,selection,heap,merge,smooth,tim
. -
N
, the number of records to sort, default is100
. -
P
, the shuffling amount, default is1.0
. -
cycles
, the number of cycles per run, default is1
. -
runs
, the number of runs per$(N, P)$ , default is1
. -
out
, the name of the file to write the results to, default isoutput.csv
; of course, you may specify directories within the path.
So for example you could run the executable with the command main algos=smooth,heap,merge N=500,5000,50000 P=0.005,0.05,0.5,1.0 cycles=5 runs=2
. This performs two runs for each pair of ./main N=1000,2000 P=1.0
and ./main P=1.0 N=2000,1000
would run the same routines. However, note that strictly no spaces are allowed when inputting the comma-separated values.
If you want to run the algorithms with the default datasets provided by the starter code, you can also specify the debug
argument; instead of calling ./main
, you run ./main debug
. The resulting process will execute each of the algorithms ten times for all of the starter datasets. The output of this process is not saved to a file, although the printed text can be piped into one if this is desired.
Note that for all tests, the integrity of the sorted arrays are automatically verified. It is ensured that (1) they are in the right order and (2) they have no missing entries. For more information on the specific implementation of these, head over to Comparisons and Individual Analyses.
Because of the way the program is structured, it is possible to import sortingalgorithms.c
into your own main function and test each of the following functions with your own data:
Record_insertionSort(Record *records, int n)
Record_selectionSort(Record *records, int n)
Record_heapSort(Record *records, int n)
Record_mergeSort(Record *records, int n)
Record_smoothSort(Record *records, int n)
Record_timSort(Record *records, int n)
Note how all the functions preserve the same signature to facilitate testing. Functions that use recursion (such as merge sort) have been abstracted into their own files to avoid succumbing to spaghetti code.
To gather the frequency counts on your own, simply call the corresponding Record_get<sorter>SortFreq()
function. For instance, after calling Record_heapSort(records, N)
for some variables records
and N
, I can then do something like printf("heap sort frequency count: %ld", Record_getHeapSortFreq())
. The frequency count value is always of type long
.
IMPORTANT REMINDERSPlease keep the following in mind to avoid any problems running your custom tests:
- When importing
sortingalgorithms.c
and using any of theRecord_<sorter>Sort()
functions, DO NOT FORGET TO RUNRecord_initSorters()
at the start of the main function (with no parameters). This function has to be run once before any of the sorting algorithms can work.- DO NOT MOVE any of the files around to prevent breaking any of the imports.
Some might notice that the project contains a few Python files. These can be ignored and were simply used to automate the running of the C program. It allowed the possibility to perform batch tests without having to type the commands one after the other (this was necessary for my device because creating a bulk command that ran the C executable for more than a few minutes in a single process caused it to be killed by the OS). Additionally, another Python script was also utilized to generate some of the visuals of this report (particularly those that graph data; the other illustrations were created in Figma).
Heap sort has been described as "selection sort using the right data structure". While that was not something that made sense to me a year ago, it was something that clicked during this project. Both of them select the smallest (or largest) element within an array and move them to their proper location; however, heap sort does this in
Heap sort treats the array differently: instead of viewing it as a sequential list of elements, heap sort visualizes it in the form of a tree, with the associations between parent nodes and child nodes outlined in a simple manner. Every
But what benefit does a tree have over a sequential view of an array of elements? Because of the structure of a the tree (which in the case of heap sort is actually a max-heap), we are guaranteed to know that every element is greater than all its descendants. This invariant allows us to shift an element to its right place within the structure without having to compare it to every single element in the array; the motion associated with performing this action is vertical along the tree, and thus only depends on the height of the tree. This is wonderful because the height of a binary tree is always around
As a brief outline of the actual heap sort algorithm (which I believe we must recall for the sake of understanding smooth sort), I present the following pseudocode:
# The first step involves converting the array into a valid max-heap
# This operation takes nlogn steps.
for i = (array.length - 1) to i = 0:
# Consider the subtree rooted at i, and sift the root down
# Because we're starting with the leaves of the tree, this guarantees a valid max-heap at the end
siftDown(array[i])
# The second step is the actual sorting
# We consider slices of the original array in decreasing length
# Again, this entire operation takes nlogn
for i = (array.length - 1) to i = 0:
# Swap the largest element and bring it to the end of the current subarray
swap(0, i);
# Sift the new root of the max-heap
# Since the max-heap was originally valid, we only need to sift the root to fix it
siftDown(array[0]);
All in all, given that the two operations take
It is also possible to optimize heap sort by starting the heapifying process at i = pow(2, (int) log2(array.length)) - 1
(this is just the smallest power of siftDown()
on them. Nevertheless, even though the version of heap sort in this project does not use this optimization, heap sort is generally the fastest among the six sorting algorithms that were chosen (at least according to the implementations of this project). For more information about the analyses and results, refer to the section on Comparisons and Individual Analyses.
One important thing to note was that a different optimization was used to speed up heap sort. Originally, I managed to implement the algorithm exclusively using swaps. When sifting elements down the tree, a swap would occur at every level when it was possible. This meant copying two different records unto two different locations. However, upon saving the root in a temp variable and only "shifting" child nodes up instead of performing swaps (much like insertion sort shifts adjacent elements rather than swapping to the end), heap sort ran about twice as fast as it did in the initial runs (although sadly I did not save the data for the initial implementation of heap sort I had). This makes sense given the fact that the act of copying data was cut in half.
Anyway, on to the fun part...
The reason I decided to explain heap sort prior to smooth sort is because the two algorithms rely on the same fundamental idea: visualizing an array in a manner that differs from its linear structure. However, smooth sort attempts to remedy a certain problem with heap sort: the largest element of the array is always at the root (the beginning) of the array, when ultimately it must end up at the opposite end. This means that regardless of the initial state of our array,
Smooth sort, on the other hand, takes an unorthodox approach. For one, it doesn't create a single tree but rather a forest of max-heaps. For another, it builds these trees such that their root nodes are on the right. This entails way less computations for lists that are close to being sorted. It also means that smooth sort, in the best case, is
But how does it work, exactly?
Before we discuss the underlying structure of the max-heaps created by smooth sort, I wish to bring forth the idea of the Leonardo numbers. The Leonardo numbers are just an integer sequence defined by the recurrence:
To the acute reader, this might seem rather similar to the Fibonacci sequence, aside from the
Now consider for a moment trees with
Whereas heap sort uses a single complete binary tree to represent the array, smooth sort uses a forest of Leonardo trees instead.
Generating the forest of Leonardo trees for smooth sort is more straightforward than it sounds. The illustration below will likely do better to explain the process, but I will try to outline it regardless. The process relies on the fact that any number can be expressed as the sum of a subset of the Leonardo numbers. We can prove this using mathematical induction, though again, we refuse to wander beyond the scope of this report.
We begin by starting at the left of the array and proceed rightwards until the last element is reached. During each iteration, the current element is either (1) added as a new tree on its own (a singleton), or (2) added as the root of a new tree consisting of the two rightmost trees in the forest. In the second case, the new tree is heapified to ensure it is a valid max-heap and a siftDown()
function is called (note that like heap sort, the siftDown()
we use for smooth sort doesn't rely on swaps but shifts). We can see the progression of the process below, where the newly added node is highlighted for each iteration.
The exact rules for deciding whether or not to append the next element as a root node or as a singleton are simple: if the two rightmost Leonardo trees in the forest have adjacent orders (one if of order unsigned int
to keep track of the orders of the Leonardo trees currently in the forest: a unsigned long
). For the purposes of this project, this is more than enough.
Before we can proceed with the actual sorting, we need to cover another subroutine of smooth sort. After it generates the forest of max-heaps, it must guarantee that the root of the rightmost tree is the greatest element in the array. This allows it to remove that node from the forest and place it into the sorted portion of the array (which is where it happens to be already). But for this to be true, the roots of all the trees in the forest must be sorted in ascending order. Sorting the roots of the Leonardo trees represents the second intermediate stage of the algorithm.
Note that we perform an insertion sort for each of the roots starting from the leftmost tree. At the end of each pass, the last modified tree is heapified (and the rest are left as is: if the root of a max-heap is replaced by a greater element, the max-heap property must still be satisfied by that heap). Eventually, the roots will be in ascending order.
Once the roots have been sorted, we can begin removing the greatest element of the forest until the array is fully sorted. Since the greatest element must always be the root of the rightmost tree, there is no need to move this element; rather, it is the structure of the heaps that change with each iteration.
During each root removal, either one of two things will happen: (1) the root leaves behind no subtrees because it was a singleton, in which case we can proceed to remove the next root, or (2) the root exposes two new subtrees, the roots of which must be rearranged with the roots of the existing trees on their left. In the second case, we repeat the process illustrated in the previous subsection to maintain the property that all the roots within the forest are in ascending order; however, instead of doing this for all the roots, we only do this for the roots of the exposed subtrees (the rest of the roots should still be in order). Eventually, we reach the end of the array and arrive at a sorted list.
Note that for each iteration, performing insertion sort on the new roots (if there are any) will take at most
No illustrations exist for this part of the algorithm, as the other illustrations suffice to provide an explanation for this already. Follow the first stage of smooth sort in reverse, and it should be easy to visualize how root removal looks; repeat the process outlined by the second stage, and understanding how each iteration sorts new roots should become clear.
It is left as an exercise to the reader to understand how smooth sort approaches linear time for nearly-sorted lists... (or you can ask me in person).
Insertion sort and selection sort are both
Selection sort always performs the same number of comparisons, regardless of the state of the array. This means that when the given array is already sorted, selection sort won't even know about it until it's compared every pair of elements together. Insertion sort, on the other hand, can run in
Selection sort "selects" the smallest (or greatest) element left in the unsorted portion of the array. Insertion sort "inserts" the current element into its correct location within the sorted portion of the array.
Merge sort takes a divide-and-conquer approach to sorting an array. Given any array, it splits it into two new arrays of half the size and calls the routine on those arrays. Eventually, arrays of size 1 will be left; these arrays are considered sorted by default. When sorted arrays are encountered, merging them can occur. Merging two arrays happens by repeatedly selecting the smaller of the leftmost elements in each array and pushing this unto the sorted array (note that the "sorted array" here refers to an auxiliary piece of memory; other implementations of merge sort exist with less space consumption, although these come with the cost of increasing the time complexity of the algorithm). Eventually, all the inner function calls resolve into sorted subarrays, and a sorted version of the array is gradually created. The final step of the algorithm (according to the implementation of this project) is to copy the sorted version of the array onto the original.
Merge sort has a time complexity of
Now I won't bother going in-depth with tim sort; it's not really the main algorithm I chose anyway. Nevertheless, I feel like it deserves a special mention. The original publication outlining tim sort actually takes inspiration from another academic paper which led me down a rabbit hole of information theory. This eventually helped me realize my ideas on how to benchmark the sorting algorithms.
Some caveats with the implementation of tim sort used by this project: it's not adaptive, and it's not completely faithful to the original. For one, the run size doesn't change based on the number of records, although in practice it should adapt to try and minimize the number of resulting merges utilized by the algorithm. Aside from this, the algorithm also performs merges only after defining the runs, as opposed to performing them when merge criteria are satisfied. Nevertheless, contrary to these oversimplifications, I still refer to this version of the algorithm as tim sort, since a lot of the implementations found online follow this pattern (despite their apparent irreverence to the original).
There is a well-known shuffling algorithm that generates a permutation of the elements of a sequence in a reliably-random way. By this, we mean to say that every possible permutation is equally likely, and the shuffling process does not favor any single one. This is called the Fisher-Yates algorithm.
It's actually simpler than it sounds: you traverse an array of elements starting from the last element. At every element
for k = (array.length - 1) to k = 0:
k' = randomNumber(0, k)
swap(array[k], array[k'])
Despite it's simplicity, it reliably selects a permutation of the original array in a uniform manner (assuming your random function for choosing a number between each
To perform shuffling for this project, a slight variation of the original algorithm is used. Whereas the original Fisher-Yates always executes a shuffle for each element, the variation used here only performs swaps probabilitically. A swap happens
for k = (array.length - 1) to k = 0:
k' = randomNumber(0, k)
t = randomFloat(0, 1)
if t <= P and t > 0:
swap(array[k], array[k'])
In the implementation, things are notated a bit differently and we have
When performing a shuffle on data, it's helpful to know just how much of a shuffle we were able to do. To help us measure this idea of "shuffling", we come up with two metrics, the first of which is entropy.
Entropy is often associated with the idea of disorder. Fundamentally, the concept of "disorder" is not too far from the concept of "messing up" a deck of cards, although context necessitates that we refer to the second case as information entropy. Interestingly enough, both information theory and statistical mechanics have characterized their notions of entropy in much the same way. We focus on the formulation Claude Shannon provided for information theory (who impressively came up with the form independent of any knowledge of statistical mechanics):
gives the entropy of a discrete random variable
When
So how can we use this to analyze the "shuffledness" of our arrays? Entropy manifests in our sequences of records when it becomes hard to expect which record comes after a given record. In a sorted list (or a list with some structure to it), this is easy because the records form a sort of pattern. Thus, when traversing the array, at any given point, it is reasonable to have an expectation for what the next record might be (it won't be anything surprising). However, when an array is shuffled really well, it becomes really hard to tell what record might come next, and so things are "uncertain" and "surprising".
To approach this more rigorously, we define a random variable
where
Do note that in order for this approach to work, we adjust negative differences to fall within the range
Another useful idea to help us assess the "shuffledness" of an array is correlation. Using the same function
However, when measuring shuffledness alone, the measure of correlation tends to be a bit superfluous in that we don't need to know whether or not the array was in reverse or the right order. Either way, the array wouldn't be considered "well-shuffled" (it might not be sorted, but it isn't that jumbled up). To give us a better sense of shuffledness alone, we can square the value of the correlation coefficient. This particular number is used a lot in inferential statistics and has a special name: the coefficient of determination. Although this number has a few different interpretations, we ignore most of these as they do not apply to our framework, though we use it for its relevance in determining the disorder within our datasets.
Nevertheless, both the coefficient of correlation and determination will be used to assess the performances of the different algorithms. The latter will supplement our measurements of entropy, while the former will give us insights into how the algorithms react to data that has a bias for being sorted in the correct order or the reverse order.
This section discusses the two different methods used to compare and analyze the different algorithms. The first uses the provided datasets for the project; there are seven of these, and all algorithms we're allowed to run on them ten times. The second involves a testing framework specifically coded for this project. Do note, however, that flexibility was considered in designing this system, and the framework may be used to benchmark other sorting algorithms (even ones that don't use the Record
data structure) so long as they follow the interfaces required by the framework.
This test was relatively straightforward. To ensure the reliability of the measured durations, each algorithm was run ten times on all of the provided datasets. The results were then recorded unto a text file (by piping the actual output of the executable) and are summarized in the table below. Note that all the values depicted here represent the average duration taken by the algorithm across the ten different attempts of sorting each dataset (and thus contain an extra significant figure).
Dataset / Algorithm | Insertion Sort | Selection Sort | Heap Sort | Merge Sort | Smooth Sort | Tim Sort |
---|---|---|---|---|---|---|
random100.txt |
|
|
|
|
|
|
random25000.txt |
|
|
|
|
|
|
random50000.txt |
|
|
|
|
|
|
random75000.txt |
|
|
|
|
|
|
random100000.txt |
|
|
|
|
|
|
almostsorted.txt |
|
|
|
|
|
|
totallyreversed.txt |
|
|
|
|
|
|
As expected, both totallyreversed.txt
is likely due to the elevated number of swaps performed by selection sort. On the other hand, when looking at the best case scenario (sorting the almostsorted.txt
dataset), selection sort unfortunately does worse off for some reason. However, insertion sort finishes in about half a minute. This makes sense as insertion sort is expected to run in
For the
For the simpler algorithms, measuring frequency counts was pretty straightforward. Algorithms like insertion sort and selection sort were the easiest to configure for this, since they had no subroutines. Merge sort, tim sort, and heap sort were slightly more difficult because of the recursions they employed, but they were still relatively manageable. The real difficulty came with smooth sort. Smooth sort used so many helper functions I had to decide which ones to include and which ones to regard as just "math operations". I could follow the rule of incrementing the counter at the start of every loop and the start of every function call, but I had to choose which one of these made sense to count. For instance, one of the loops in smooth sort did nothing but iterate through the bits of a single unsigned int
and performed some basic arithmetic on them. It didn't make sense to count the loop as a sequence of many different instructions because the loop as a whole just did a single math operation (or to put it another way, the math operation I was performing with a loop probably wouldn't use an explicit loop in other languages). In the end, I struck a compromise and made sure that the functions and loops that were meant to iterate through records were counted. This was how I defined the frequency counts for all the algorithms.
Dataset / Algorithm | Insertion Sort | Selection Sort | Heap Sort | Merge Sort | Smooth Sort | Tim Sort |
---|---|---|---|---|---|---|
random100.txt |
||||||
random25000.txt |
||||||
random50000.txt |
||||||
random75000.txt |
||||||
random100000.txt |
||||||
almostsorted.txt |
||||||
totallyreversed.txt |
As we've mentioned, selection sort will perform the same number of iterations regardless of the state of the original array. Thus, we can see the constant frequency count exhibited by selection sort. Merge sort has the same property, although do note that this does not mean it performs the same number of swaps regardless of the state of the array. With the way the frequency counting was set up, it just means that it performs the same number of iterations through the array.
When looking at the other sorting algorithms, we can see that they perform a varying number of iterations. Smooth sort seems to have the largest change in this: when dealing with arrays that are close to being sorted, the number of iterations needed by smooth sort decreases significantly. While heap sort had a similar property with regard to execution times, it does not seem to have much of an improvement when considering the frequency count produced by the algorithm. Again, we will tackle why this is the case in the proceeding sections.
Before we proceed to the collected data, we must first discuss the methods that were used to verify the integrity and validity of the sorted arrays. Initially, this was done by checking the order of the elements in the final array. If the array had its elements in a nondecreasing (or nonincreasing) arrangement, then the function would say the array was sorted. However, after debugging the more complicated algorithms, I encountered problems that made me realize this check was insufficient. Sometimes, due to logical bugs, certain entries would be duplicated and would overwrite other entries. It was then possible to have a sorted collection of elements but with some of the original entries missing in the final array.
To remedy this, I implemented a binary search that checked if every element in the original array was also present in the final array. If the first check was positive (the array was in the correct order), then the checker would proceed to execute the binary search. All in all, this meant that the verification of the sortedness of the array will always take at most
The custom testing framework allows us to execute a number of different runs, each of which perform a set of specific cycles. In this case, a run refers to a specific shuffling of records for a given
# This performs a test for a given N and a given P
set value of N
set value of P
suffle_data = 1d array storing information about shuffles # init to all 0s
times = 2d array to store execution times # init to all 0s
freqs = 2d array to store freq counts # init to all 0s
# Testing algorithm for a given N and P
for run in runs:
shuffle = generateNewShuffle(N, P)
shuffle_data[run] = getShuffleParameters(shuffle)
# Perform the required number of cycles
for cycle in cycles:
# Do the sort for each algorithm
for algo in algorithms:
tosort = createShuffleCopy(shuffle)
start = getStartTime()
algo.sort(tosort)
end = getEndTime()
# Get the measurements for this cycle
t = end - start
f = algo.freqCount
times[algorithm][run] += t
freqs[algorithm][run] += f
# Get the average of all cycles for this run
times[algorithm][run] /= cycles
freqs[algorithm][run] /= cycles
Note that when we save the results of a run, we're also saving the values of
For all the data collection that was performed using the framework, the number of cycles and runs were always set to
Here are a few reflections I've had the privilege of noting down following the completion of the project. Note that these are not in chronological order.
This is probably one of the funniest bugs I've ever encountered... it's even better because it didn't come from a problem with my code.
When I was implementing smooth sort, I found myself needing a way to compute the logarithm of a number with respect to base 2. This was necessary in order to read off the bits contained in a standard unsigned int
(or rather, their indices) without the need for a loop. Initially, the solution I came up with used the log()
function of the C library, which took logarithms in base log2()
existed, I computed my logarithms by doing the usual base switch:
This was working all fine and well (which I guess one should expect since the formula is mathematically correct) until at some point I decided to run my program on Windows. Up until then I'd been working within a Linux environment, and I wasn't expecting to have to deal with platform differences because I wasn't touching anything that low-level. But for some reason, despite the same code running on both my devices, smooth sort failed to sort my arrays on Windows when it successfully did on Linux. Although I now know that the reason for this was the logarithm formula I used above, it took me an hour or two to debug the whole mess and isolate the problem. It's honestly a miracle I found the problem that fast, because my smooth sort implementation had so many components I could've just as well spent an entire day figuring that one out.
It turns out that doing (int) log(8) / log(2)
on Windows gave... log(x) / log(2)
would work fine for just about any value of log2()
. If only I'd known about the function at the beginning, the problem would've never occurred... but is it my fault? Perhaps, perhaps not. It's either bad documentation on the part of the C Library or bad documentation comprehension on my part.
Initially, heap sort actually wasn't the fastest algorithm; there was no clear winner, but tim sort and merge sort seemed to dominate for the most part. That was until I realized all the algorithms that used swaps could be optimized. If I copied insertion sort and stored the current element in a temp variable, I could theoreitcally cut the execution times of the algorithms in half (as they would have to copy half the amount of data per shift vs. per swap). This actually ended up being true and sped up heap sort so much that it overtook every other algorithm after that. Do note that I did the same optimization for smooth sort, but it was already kind of slow to begin with, so even though it ran twice as fast, it was still slower than its
I'd spent about ten days on the entire project at that point when suddenly, the day before the submission, I reread the specifications and realized I needed to do the frequency counts for each algorithm (I had no idea that was required). I was about to lose it when I realized I'd created structs for each of the sorting algorithms, so that storing state for each of them (such as frequency count) would be no biggie. The function signature would remain practically the same, and only an extra struct member would need to be created. While it did take about half an hour to set up (and I had to redo all the tests again), the fact that it took only half an hour to set up was a relief. I felt kinda happy realizing the way I code these days gives my programs a leeway for flexibility.
It's amazing how many things I ended up realizing towards the end of the project.
Initially, all my sorting algorithms had their "sorter" structs passed to them by value. This was fine for most of the algorithms because they only had to copy the struct once (when they were calling the main routine of the algorithm). However, smooth sort had many subroutines, and each one was passed a copy of the struct. I somehow overlooked the problem, and for the last week or so I'd been using a heavily underoptimized version of smooth sort; it's even worse because the sorter struct for smooth sort was the largest out of all the algorithms... then, by mere serendipity, I was forced to refactor my sorting algorithms to measure their frequency counts.
While I was doing this, I had to update the function signatures to accept a pointer to the sorter struct instead of a copy of it; this would allow me to update the struct member holding the value of the curent frequency count for each algorithm. However, upon doing so, I noticed an abrupt change in the speed of smooth sort; it was actually able to compete with the other algorithms. Keep in mind that while I was doing the refactoring, I was also changing a number of other things, so the reason for this speed boost remained a mystery to me for a few minutes. Eventually, I realized what was going on and kept note of it here.
Towards the latter half of the testing phase, I realized how much more valuable it would be to measure the coefficient of correlation and not just the coefficient of determination. While the initial idea was to use the latter metric (because I thought measuring 'shuffledness' was sufficient), I later realized that knowing the 'bias' of a dataset (whether it tends to be in the right order or in reverse) would just be as insightful, since some of the algorithms obviously perform differently based on this. In this case, correlation would definitely give us more insights to work with.
Due to the delay of this realization, I had to restart all the tests I had done at that point. All in all, counting mishaps and dry-runs, I probably left my laptop on for a total of at least 48 hours running tests on the algorithms. In case you're curious, my humble potato has an Intel i3-6006u
processor, so it was definitely up for the task.
I'm saying this with a few hours left before the submission of the project... I don't have time to rerun all the tests with the optimization I wish to conduct. Nevertheless, I might decide to do this even after the project is over.
Basically, one of the existing subroutines within the current implementation was a left over from my initial draft of smooth sort. During the root sorting phase, my smooth sort used to need to compare the nodes of the trees with their children in order to insert the nodes correctly. This was because root sorting was carried out during the heapification phase, and so newly inserted roots would not necessarily belong to heapified trees at that moment. However, upon revising the algorithm a few days ago, I moved the root sorting phase to the latter half of the algorithm, so that roots were sorted during the actual sorting phase. This made it unnecessary to make the comparisons with the children of each root. Thus, if these extraneous comparisons are removed, there is a possibility for smooth sort to become much more efficient (although only by a small amount).
On the other hand, we can place the root sorting subroutine back into the first half of the algorithm and do without the sorting phase completely, since heapifying in this manner would actually create the sorted array already. Whether or not this will be any faster, I do not know.
|\ _,,,---,,_
ZZZzz /,`.-'`' -. ;-;;,_
|,4- ) )-,_. ,\ ( `'-'
'---''(_/--' `-'\_)