Sorting Algorithms in Ruby

Share this article

Algorithms on blue business binder

Sometimes, I think Ruby makes it too easy for us. Just doing [1,18,4,72].sort gives us a nice sorted list. But what does Ruby actually do under the hood?

The point of understanding sorting algorithms has very little to do with the actual act of sorting. Rather, the different algorithms are great examples of various techniques that can be applied to large set of problems. In this article, we’ll take a look at some of these algorithms and the underlying ideas, as well as implementations in Ruby.

The Problem

Before we start out, let’s get on the same page about the problem we’re trying to solve. The input to our algorithm will be an array of arbitrary length consisting of integers (not necessarily positive). Our algorithm should return a version of this array sorted in ascending order. If we want descending order, we can either reverse the resulting array or change the algorithms presented slightly (e.g. the comparison operator used).

Bubble Sort

Let’s first look at a really simple (and also pretty slow) sorting algorithm known as “Bubble sort”. The idea is pretty simple: Walk through the list and put two adjacent elements in descending order. But, here’s the kicker: We have to repeatedly walk through the list until there are no longer any swaps to make, meaning, the list is sorted.

The Ruby implementation can be written easily from the hand-wavy description:

def bubble_sort(array)
  n = array.length
  loop do
    swapped = false

    (n-1).times do |i|
      if array[i] > array[i+1]
        array[i], array[i+1] = array[i+1], array[i]
        swapped = true
      end
    end

    break if not swapped
  end

  array
end

We maintain a variable called swapped, which is true if any swaps were made in the pass through the array. If, at the end of a walk through the array, swapped is false (i.e. no swaps have been performed), we are done and return a sorted list. Essentially, swapped = true is the termination condition for the forever loop.

I mentioned at the outset that BubbleSort is a pretty slow algorithm, but what exactly do I mean by that? BubbleSort is bad at scaling, i.e. making our input size larger by a little bit results in a pretty large increase in the running time of the algorithm. What exactly is the relationship between input size and relationship? Well, let’s think about it.

The worst possible situation for BubbleSort occurs when the input array is in descending order, because this means we have to perform the maximum number of swaps. In fact, in the worst case, if we have n elements in the input array, we have to perform \[ c * n^2 \] swaps, for some positive real constant c (i.e. c doesn’t change when n does). Assuming that each swap takes a constant amount of time (i.e. the time taken to perform a swap is unaffected by the input size), the running time of BubbleSort increases quadratically with respect to the input size. So, we can say we can say BubbleSort is \[O(n^2)\] which is equivalent to saying the running time scales quadratically.

We can also think about the amount of memory in order to perform the sort. Since the sort is “in place”, we don’t have to allocate any additional memory. Thus, we say that BubbleSort’s space complexity is \[O(1)\] i.e. constant at most.

Merge Sort

It turns out we can do quite a bit better when it comes to sorting. A technique called “divide and conquer” takes the original problem and produces solutions for smaller pieces of it, which are together in order to create the final solution. In our case, this is the problem statement:

Sort the array A of length n

What if we did this instead:

Sort the left of half of A and the right of A, then combine them.

We can actually split each of the halves into further halves (quarters of the original part) and continue recursively. Turns out, we’ll eventually reach an array of size 1, which is automatically sorted. Then, take all of the pieces and finally combine them to give the equivalent of A.sort. However, the combining process is a bit involved.

Say we’re given two sorted arrays, like [1, 3, 5] and [2, 4, 6, 7] and want to combine them into one sorted array. Here’s how to do that, in this case. Take a look at the first elements of both arrays and put the smaller one into the result final result. After one run of this, we have this in the final result:

[1]

Repeat the process except move along the array, making the next element the “first element”. In this case, make 3 the “first element” of [1, 3, 5] and compare it to 2 from [2, 4, 6, 7]. We keep doing this until we’ve exhausted one of the lists. At this point, the final result looks like this:

[1, 2, 3, 4, 5]

Then, just push on the rest of the second list to get a combined, nicely sorted list:

[1,2,3,4,5,6,7]

We can use the procedure described with the example as our “merge” or “combine” step. So, here’s an outline of mergesort for an array A with length n:

Mergesort(A):
1. return A if n == 1
2. left = left half of A
3. right = right half of A
4. sorted_left = Mergesort(left)
5. sorted_right = Mergesort(right)
6. return merge(sorted_left, sorted_right)

Let’s take a look at the Ruby rendition of this:

def mergesort(array)
  def merge(left_sorted, right_sorted)
    res = []
    l = 0
    r = 0

    loop do
      break if r >= right_sorted.length and l >= left_sorted.length

      if r >= right_sorted.length or (l < left_sorted.length and left_sorted[l] < right_sorted[r])
        res << left_sorted[l]
        l += 1
      else
        res << right_sorted[r]
        r += 1
      end
    end

    return res
  end

  def mergesort_iter(array_sliced)
    return array_sliced if array_sliced.length <= 1

    mid = array_sliced.length/2 - 1
    left_sorted = mergesort_iter(array_sliced[0..mid])
    right_sorted = mergesort_iter(array_sliced[mid+1..-1])
    return merge(left_sorted, right_sorted)
  end

  mergesort_iter(array)
end

The main procedure of interest here is merge. Essentially, move along the two halves, while adding on the smaller value to the end of res until we have exhausted one of the halves. At this point, simply pile on what remains of the other list. And, that’s MergeSort for you!

What’s the point?

So, what is the benefit of implementing a significantly more complicated algorithm than good old BubbleSort? It is all in the numbers.

Without getting into the details too much (they involve solving a recurrence), we’ll say that mergesort is \[ O(n log_2{n}) \] It turns out that this is *a lot* better than \[O(n^2)\] Let’s make another simplification. Say MergeSort uses \[n /log_2(n)\] operations and BubbleSort uses \[n^2\] operations to sort an array of length n If we set n = 1,000,000 then MergeSort requires roughly \[6 \cdot 10^7\] operations whereas BubbleSort weighs in 17,000 times worse at \[1 \cdot 10 ^ {12}\] operations.

Divide and Conquer

MergeSort shows us a nice strategy: divide and conquer. It turns out there are a lot of problems which can be solved by breaking them into subproblems and the combining the resulting “subsolutions”. It takes skill and practice to identify cases where you can use divide and conquer, but MergeSort is a great example to use as a springboard.

The take home points are: If you have some sort of trivial base case (in MergeSort, a list of length 1) or are somehow able to break your problem down to this base case and have some kind of idea to combine solutions, divide and conquer might be an avenue to explore.

Wasting Memory Like No Tomorrow

But wait, we can do even better than MergeSort! But, this time we’ll have to spend, um, a bit more memory. We’ll get straight to the Ruby:

def mark_sort(array)
  array_max = array.max
  array_min = array.min
  markings = [0] * (array_max - array_min + 1)
  array.each do |a|
    markings[a - array_min] += 1
  end

  res = []
  markings.length.times do |i|
    markings[i].times do
      res << i + array_min;
    end
  end

  res
end

p mark_sort([3,2,19,18,17])

We have an array called markings representing the number of times a certain number occurs in array. Each index of markings corresponds to a number in the range array.max and array.min, inclusive. We initialize markings with 0s and walk through the array, incrementing a number’s marking when we see it occur in the array. Finally, just print out the numbers seen the right number of times. We’re making a few passes of the array, but we never make a “pass inside a pass”, i.e. a nested loop. So, our algorithm performs \[ c * n \] operations for some constant c for an array of length n

We can say that it is \[O(n)\] in other words, blazing fast. But, it isn’t always useful, since we have to use a potentially giant piece of memory. For example, if we were given [1,1000000] as an input, we’d have to allocate a million element array! But, if we know our ranges will be small or have lots of memory to waste, this algorithm can come in handy.

Wrapping It Up

I hope you’ve enjoyed this tour of sorting algorithms in Ruby. Drop questions in the comments below!

Frequently Asked Questions (FAQs) about Sorting Algorithms in Ruby

What are the different types of sorting algorithms available in Ruby?

Ruby provides several built-in sorting algorithms for arrays. The most commonly used ones are ‘sort’, ‘sort_by’, and ‘sort!’. The ‘sort’ method sorts an array in ascending order by default. The ‘sort_by’ method is used when you want to sort an array based on a specific attribute or condition. The ‘sort!’ method is similar to ‘sort’, but it modifies the original array instead of creating a new one. There are also more complex sorting algorithms like quicksort, mergesort, and heapsort that you can implement in Ruby.

How can I sort an array in descending order in Ruby?

To sort an array in descending order in Ruby, you can use the ‘sort’ method with a block. The block should return -1, 0, or 1 depending on whether the first element is less than, equal to, or greater than the second element. Here’s an example:

array = [5, 3, 8, 1, 4]
sorted_array = array.sort { |a, b| b <=> a }

In this code, ‘b <=> a’ will sort the array in descending order.

What is the time complexity of the ‘sort’ method in Ruby?

The ‘sort’ method in Ruby uses a quicksort algorithm under the hood. The average time complexity of quicksort is O(n log n), where n is the number of elements in the array. However, in the worst-case scenario, the time complexity can be O(n^2). This happens when the array is already sorted in ascending or descending order.

How can I sort a multidimensional array in Ruby?

To sort a multidimensional array in Ruby, you can use the ‘sort_by’ method with a block. The block should return the element you want to sort by. Here’s an example:

array = [[2, 'b'], [1, 'a'], [3, 'c']]
sorted_array = array.sort_by { |element| element[0] }

In this code, ‘element[0]’ will sort the array by the first element of each sub-array.

How can I sort an array of strings in Ruby?

To sort an array of strings in Ruby, you can use the ‘sort’ method without a block. This will sort the strings in alphabetical order. Here’s an example:

array = ['banana', 'apple', 'cherry']
sorted_array = array.sort

In this code, ‘sort’ will sort the array in alphabetical order.

How can I sort an array of objects in Ruby?

To sort an array of objects in Ruby, you can use the ‘sort_by’ method with a block. The block should return the attribute you want to sort by. Here’s an example:

class Fruit
attr_reader :name

def initialize(name)
@name = name
end
end

fruits = [Fruit.new('banana'), Fruit.new('apple'), Fruit.new('cherry')]
sorted_fruits = fruits.sort_by { |fruit| fruit.name }

In this code, ‘fruit.name’ will sort the array by the ‘name’ attribute of each fruit.

What is the difference between ‘sort’ and ‘sort!’ in Ruby?

The ‘sort’ method in Ruby returns a new array that is sorted, leaving the original array unchanged. On the other hand, the ‘sort!’ method sorts the original array in place, meaning that it changes the original array. Here’s an example:

array = [5, 3, 8, 1, 4]
sorted_array = array.sort
array.sort!

In this code, ‘sorted_array’ will be a new sorted array, while ‘array’ will be the original array sorted in place.

How can I sort a hash in Ruby?

To sort a hash in Ruby, you can use the ‘sort_by’ method with a block. The block should return the key or value you want to sort by. Here’s an example:

hash = { 'banana' => 2, 'apple' => 1, 'cherry' => 3 }
sorted_hash = hash.sort_by { |key, value| value }

In this code, ‘value’ will sort the hash by its values.

Can I use a custom comparison function with ‘sort’ in Ruby?

Yes, you can use a custom comparison function with ‘sort’ in Ruby. You just need to provide a block to the ‘sort’ method. The block should take two parameters and return -1, 0, or 1 depending on whether the first parameter is less than, equal to, or greater than the second parameter. Here’s an example:

array = [5, 3, 8, 1, 4]
sorted_array = array.sort { |a, b| a <=> b }

In this code, ‘a <=> b’ is a custom comparison function that sorts the array in ascending order.

How can I sort an array of numbers in Ruby?

To sort an array of numbers in Ruby, you can use the ‘sort’ method without a block. This will sort the numbers in ascending order. Here’s an example:

array = [5, 3, 8, 1, 4]
sorted_array = array.sort

In this code, ‘sort’ will sort the array in ascending order.

Dhaivat PandyaDhaivat Pandya
View Author

I'm a developer, math enthusiast and student.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week