Earth to Abigail

View Original

Understanding Selection And Insertion Sort By Creating Audio Representations With Ruby And Sonic Pi

In the first article I published related to sorting, we explored bubble sort while learning the basics of Sonic Pi. In this week’s post, we’ll dive deeper into our audio representation experiment and explore two more common sorting algorithms: selection sort and insertion sort.

You can find the code for all the tutorials in this series on Github.

Specifically, we’ll explore these algorithms according to the observation made by Professor Graham Hutton in the video below.

First, I’d like to encourage you to read the tutorial on bubble sort, especially if you don’t have any experience using Sonic Pi, and come back to this article after.

Theory Behind Selection and Insertion Sort

Both selection and insertion sort have a time complexity of O(n2), which is the same as bubble sort.

But wait a second… Didn’t I say in the first post on sorting that bubble sort was the least efficient sorting algorithm? Why are selection and insertion sort considered more efficient than bubble sort if they have the same time complexity?

Wikipedia gives us a very clear explanation:

Among quadratic sorting algorithms (sorting algorithms with a simple average-case of Θ(n2)), selection sort almost always outperforms bubble sort and gnome sort. Insertion sort is very similar in that after the kth iteration, the first k elements in the array are in sorted order. Insertion sort's advantage is that it only scans as many elements as it needs in order to place the k + 1st element, while selection sort must scan all remaining elements to find the k + 1st element.

Simple calculation shows that insertion sort will therefore usually perform about half as many comparisons as selection sort, although it can perform just as many or far fewer depending on the order the array was in prior to sorting. It can be seen as an advantage for some real-time applications that selection sort will perform identically regardless of the order of the array, while insertion sort's running time can vary considerably. However, this is more often an advantage for insertion sort in that it runs much more efficiently if the array is already sorted or "close to sorted."

What we can understand from this is that the efficiency of these algorithms comes from the number of comparisons made, not necessarily from optimizing the number of iterations it takes to sort the list.

Wikipedia also informs us that usually, insertion sort needs even less comparisons than selection sort and therefore outperforms selection sort in most cases. This is something we’ll definitely want to monitor in our sound experiment.

Selection Sort

This algorithm goes through each element of the array and saves the value of the current element and it’s position as minimum value and minimum index. It then loops over all the remaining elements of the array and compares them to the current element. If it finds an element smaller then the one saved as minimum, it replaces the minimum value and the minimum index by the element it found and it’s index.

Once the loop is completed, it checks whether the current minimum’s index is different than the index of the current element. If it is, it swaps the value of the current element with the value located at the minimum index.

Here is a pseudocode representation of selection sort:

See this content in the original post

Insertion Sort

This algorithm is extremely similar to selection sort. The main difference with the previous algorithm is the way it defines the subarrays.

Insertion sort loops over the whole array and saves each current element as a key. It then looks at the elements in previous positions in the array and loops over them. If these elements are bigger then the current key, their value is overwritten with the value of the element right next to it.

Once it finds a place where the value of the element is smaller than the key, it swaps the value of the element that is right next to it with the value of the current key, thus “inserting” the value at the appropriate place in the array.

Here is a pseudocode representation of insertion sort:

See this content in the original post

“Insertion and selection sort are the same if you look at them correctly.”

In this video, Professor Graham explains that these two algorithms are actually the same if you paint a correct picture of them. His observation is that both these algorithms have a similar mechanism but one of them “selects” the correct value according to columns and the other “inserts” the value at the correct position according to rows.

I thought this was quite an interesting observation and the picture he demonstrated in the video is so clear and straight forward that I wondered if it would be possible to demonstrate that same observation through sounds.

So let’s get started!

Digging Deeply Into Both Sorting Algorithms

Extracting Information From The Algorithms

Let’s start by transferring those concepts into Ruby code inside SonicPi and printing the results:

Selection sort implementation in Ruby

Insertion sort implementation in Ruby.

Now is the time to decide what are the elements that are interesting for us to track in the execution of the algorithms. These are the elements I settled for:

  • total number of swaps for each algorithm

  • each subarray we are looking at

  • number of overwrites or replacements inside each subarray iteration

  • the time of execution (for comparison between algorithms)

Ultimately, we also want to give an auditive sense of wether we are sorting according to columns (selection sort) or rows (insertion sort).

Interesting fact:

In order to really wrap my head around how each algorithm works, it was important for me to log a lot of information during the execution, especially to see what happens inside the loop of the subarrays. To do that inside Sonic Pi, I had to “slow down” the execution by using Sonic Pi’s sleep function. Otherwise, the program executes too quickly and the print statements don’t appear in Sonic Pi ‘s console.

Likewise, in order to properly benchmark the time of execution of each algorithm, I suggest running the code from your terminal first before transferring the code into Sonic Pi.

Here is the code and a picture of what the printed data looks like inside Sonic Pi for both algorithms:

See this content in the original post

Logging the data for selection sort.

See this content in the original post

Logging the data for Insertion Sort.

It’s interesting to note that, in our case, it seems like selection sort outperforms insertion sort in the amount of total swaps and comparisons it needs to make in order to sort the array.

Mapping Data To Frequencies and Audio Parameters

Great! We are now ready to start mapping all this data to sounds and make an audio representation of both algorithms that highlight their similarities as well as their differences.

Start Tracking Data With Sounds Through Sonic Pi

To create our unsorted array of notes, I chose to use the built-in hirajoshi scale. The following code creates an unsorted array of unique values of length 16.

Creating an unsorted array using the hirajoshi scale built in Sonic Pi.

Back to our sorting functions, let’s start by defining a reasonable speed for our audio representation to run:

use_bpm 80

Then, the first thing we need to do for both functions is make sure that we can hear the array at least once unsorted and once when sorted. The ruby each method allows us to do just that.

arr.each {|n| play n, release: 0.3; sleep 0.25}

We also need to make sure we create a copy of our unsorted array so that we’ll be able to loop function calls in our final piece (assuming we did a good job and our audio representation is nice enough that we’ll want to hear the functions run more than once!).

Next, in both functions, it would be nice to have a bass drum mark the start of a new iteration of the outer for loop.

See this content in the original post

If you understand how threads work in Sonic Pi, the reason why I used in_thread for the bass drum in selection sort but not in insertion sort should become clear when we walk through the next steps.

Now, let’s take each algorithm separately and map some more data.

For selection sort, I wanted to try and give the auditive impression that each subarray is a column of data. The effect I tried to get is a cascade of notes that end by the right value falling in place, like we saw in the picture drawn by Professor Graham.

Once the correct value is found, we will play that value, along with the one it’s been swapped with. We can use different synths to play each values so that we can clearly differentiate between the sorted element and the one it swapped positions with.

While this is happening, we also want to indicate how many times the algorithm needed to replace a value when iterating over the subarray. For that, I chose to use a short cymbal sound playing n times, n being the number of replacements we counted for that iteration. I also chose to use a soft snare drum sound to play in the case that no replacements were needed.

See this content in the original post

For insertion sort, the approach is slightly different. In this function, I want to hear the insertion of a note. Meaning, I want to iterate through each row (subarray) and hear the note that has been inserted in the right place when we arrive to it in the iteration. As previously for selection sort, we want to hear how many times a value needed to be overwritten in order for the key to be inserted in the right position.

In case the key is already at the appropriate position and no overwrites were needed, we will mark that event using the same snare drum sound as we used in selection sort.

See this content in the original post

We finish our program by calling and logging the results of both functions. Here is what your code should look like until now:

See this content in the original post

Now if you run this code, you should hear your first audio representation of selection and insertion sort!

Taking It To The Next Level And Turning The Audio Representation Into A Music Piece

Again, like in the bubble sort tutorial, the first version of our program is not necessarily the most pleasant to listen to. Let’s do some adjustments to our code to create a better audio experience 😎.

Adding A Bit Of “Vibe”

First, instead of just calling our functions, let’s wrap these calls inside a live_loop so that we can have the code running more than once. Then, let’s create another live_loop that will play a drone and give some bass to the whole piece. Finally, let’s wrap both of these inside a :reverb effect (I chose to put a lot of reverb because, well, I just love it).

See this content in the original post

This, to my taste, already sounds much better.

Next, let’s adjust the amount of sleep time the program executes before starting a new iteration. To make the audio easier to follow but still dynamic, I decided to fit every iteration inside two beats. This means that I need to subtract whatever time it took for hearing the subarray from 2.0 to have the correct value for the sleep function.

sleep 2.0 - (sub.length * 0.125)

Next, a few minor adjustments.

I decided to keep the same synth (:tb303) for playing both the sorted value and the one it swapped positions with but give each one a different cutoff and amplitude so that it’s still possible to differentiate them.

To play the swapped value in insertion sort, I had to modify my code by adding a new variable and an if statement. In this function, I also chose to add a sleep statement before playing the cymbals marking the overwrites to space out a little bit the various audio events.

Last detail, I decided to keep the :drum_snare_soft sound in selection sort but use a :drum_splash_sound in insertion sort and have both these sounds play in reverse to make them sound more interesting.

See this content in the original post

Now, you might’ve noticed that there is one piece of data we are monitoring that I didn’t yet map to audio in the code: the total amount of swaps. I chose to represent that information inside the sorted function.

Creating A Custom Sorted Function

As we previously did in the bubble sort tutorial, instead of just playing the sorted array once at the end of each function, let’s create a sorted function that will run once we have successfully sorted the list.

Like I mentioned, we are also going to use this function to map the last piece of data we didn’t do anything with yet. I chose to do that by using the :elec_blip2 sample and passing an extra argument to the sorted function. This function now takes an array as argument and a number representing the total amount of swaps.

Here is my sorted function but feel free to come up with your own!

See this content in the original post

With that, our audio representation is complete! Here is the final code:

See this content in the original post

Here’s an instagram post where I show how it should sound like if you followed exactly all the steps described above (don’t be shy to follow me on Instagram or drop me a comment!):

See this content in the original post

Conclusion

I had a lot of fun working on this article and it made me realize how many details I was overlooking when I first started to learn about computer science and these basic algorithms. This research allowed me to fill in quite a few gasps in my knowledge that I didn’t even know were there.

I’ll be posting a new video on my YouTube channel soon where I take these concepts and play around with them as I did for bubble sort. You can subscribe to my channel if you’d like to be notified when the video is out!

Next in this series, we’ll break away from sorting and examine recursion as a preparation to look at more advanced “divide and conquer” sorting algorithms: quick sort and merge sort.

If you enjoyed this post, don’t forget to press Like and share it! 🏆

Did you come up with creative ways to play around with selection and insertion sort? I’d love to hear it! Send me a message or post a link to your creation in the comment section below! ⬇️

Thank you for reading and happy coding! ❤️


See this content in the original post

Programming is not only useful to build applications, it’s also a powerful means of expression. When I started programming, I never thought I would one day merge both my passion for music and my passion for technology together.