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

 
bubble-sort-sonic-pi-ruby.jpg
 

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:

loop over each element in array
    minimum = current element
    minimum index = index of current element
    
    loop over each element in the rest of the array
        if element is smaller than minimum
            minimum = element
            minimum index = index of element
            
if minimum index not equal to index of the current element in whole array
    swap values at current index and minimum index
 

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:

loop over the whole array
    key = current element
    loop over array from first element while key is smaller then element
            element = array[index of element + 1]

    array[index of element + 1] = key
 

“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:

 

Logging the data for selection sort.

 
 

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.

# SELECTION sort
def selection_sort array
  arr = array.dup
  r = arr.length - 1
  swaps = 0
  
  # Play unsorted array once
  arr.each {|n| play n, release: 0.3; sleep 0.25}
  
  # Loop over the entire array once
  for i in 0..r
    # Current element is saved as minimum base value
    # Index of smallest value found saved with default value of current index
    min = arr[i]
    min_idx = i
    
    # Mark when we enter the loop
    in_thread do
      sample :bd_tek
    end
 ...
 
 # INSERTION Sort
def insertion_sort array
  arr = array.dup
  r = arr.length - 1
  swaps = 0
  
  # Play unsorted array once
  arr.each {|n| play n, release: 0.3; sleep 0.25}
  
  # Loop over the array
  for i in 1..r
    # Mark when we enter the loop
    sample :bd_tek
 ...

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.

  ...
    # loop over each subarray
    replacements = 0
    for j in (i + 1)..r 
      # if current element of subarray is smaller then min, 
      # replace min value and save current index
      if arr[j] < min
        min = arr[j]
        min_idx = j
        replacements += 1
      end
    end
    
    in_thread do
      replacements.times do
        sample :drum_cymbal_closed, rate: 2
        sleep 0.125
      end
    end
    
    if min_idx != i
      arr[i], arr[min_idx] = arr[min_idx], arr[i]
      # Play the note that was swapped with arr[i]      
      use_synth :pretty_bell
      play arr[min_idx] 
    else
      # Marks that no swap was needed
      sample :drum_snare_soft
    end
    
    # Play the selected note
    use_synth :tb303
    play arr[i], cutoff: 50, sustain: 0.5, decay: 0.3, release: 0.2
    sleep 1

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.

    ...
    overwrites = 0
    while j >= 0 and arr[j] > key
      arr[j + 1] = arr[j]
      j -= 1
      overwrites += 1
    end
    
    if arr[j+1] != key
      arr[j+1] = key
    else
      in_thread do
        # Marks that key was already at the right position
        sample :drum_snare_soft 
      end
    end
    
    # Indicates how many times we needed to overwrite values in orer to insert the key
    # at the right place in the array
    in_thread do
      overwrites.times do
        sample :drum_cymbal_closed, rate: 2
        sleep 0.125
      end
    end
    
    sub = arr[0..i]
    sub.each { |n|
      # If the note we are playing is the current key, use a different synth
      n === key ?
      (inserted = true; use_synth :tb303) :
      (inserted = false; use_synth :pretty_bell)
      # The difference in the parameters comes from the fact that :tb303 has a sustained sound
      # compared to :pretty_bell which is a percusive synth
      inserted ?
      (play n, cutoff: 50, sustain: 0.5, decay: 0.3, release: 0.5, amp: 0.4) :
      (play n, release: 0.3)
      sleep 0.25
    }
    sleep 0.5
  end
  ...

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

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

with_fx :reverb, room: 0.95 do
  # This provides a drone for ambiance
  live_loop :trembling_bass do
    use_synth :dsaw
    with_fx :slicer, phase: 0.125, smooth: 0.125 do
      play :g2, cutoff: rrand(60, 70), sustain: 4, decay: 1, release: 3, attack: 1, amp: 2.5
      sleep 8
    end
  end
  
  # This loops the calls to the sorting functions
  live_loop :sorting do
    selection = selection_sort unsorted_arr
    puts selection
    
    insertion = insertion_sort unsorted_arr
    puts insertion
  end
end

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.

    # INSERTION SORT
    ...
    note_swapped = false
    if arr[j+1] != key
      note_swapped = arr[j+1]
      arr[j+1] = key
      swaps += 1
    else
      # Marks that key was already at the right position
      sample :drum_splash_soft, rate: -1
    end
    
    in_thread do
      sleep 1
      overwrites.times do
        sample :drum_cymbal_closed, rate: 2, amp: 1.3
        sleep 0.125
      end
    end
   ...
    
    sub.each { |n|
      n === key ?
      (inserted = true; use_synth :tb303) :
      (inserted = false; use_synth :pretty_bell)
      
      if inserted
        play n, cutoff: 70, release: 0.3, amp: 0.5
        # If there was a swap play the swapped value
        if note_swapped
          play note_swapped, cutoff: 60, release: 0.3, amp: 0.3
        end
      end
    ...
        

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!

def sorted array, swaps
  in_thread do
    4.times do
      # Marks how many swaps the algorithm performed
      # Repeats on each iteration of the sorted array
      swaps.times do
        sample :elec_blip, amp: rrand(1.5, 2.0), pan: rrand(-1, 1)
        sleep 3.0 / swaps
      end
      sleep 1
    end
  end
  
  in_thread do
    16.times do
      in_thread do
        sample :bd_tek, amp: 3
        sleep 0.5
        sample :drum_snare_hard
        sleep 0.5
      end
      in_thread do
        4.times do
          sample :drum_cymbal_closed, rate: 2
          sleep 0.25
        end
      end
      
      8.times do
        sample :drum_cymbal_pedal, rate: 3 if one_in(3)
        sleep 0.125
      end
    end
  end
 

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

 

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!):

 

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! ❤️


Curious to hear how your code sounds like?

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.