Skip to Content
Course content

42: Big-O Notation with Ruby Examples

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time reviewing code for junior engineers, and there is one misconception that comes up almost every single time we hit the topic of performance: the belief that Big-O notation is just a fancy way of describing how many seconds a piece of code takes to run.

It's a tempting way to think about it. You run your code, look at the terminal, and see "0.02 seconds." You think, "Great, it's fast!" But that number is lying to you. It's telling you how that code performed on your specific CPU, with your current memory load, using a specific size of input data. If you move that code to a slower server or increase your dataset from 10 items to 10,000, that "0.02 seconds" could balloon into several minutes.

Stop using a stopwatch; start counting growth

Big-O isn't about time; it's about scalability. It describes how the number of operations grows as the input size (which we call n) grows. To see why this matters, look at these two Ruby methods that do the exact same thing: check if an array contains any duplicate values.

def has_duplicates_slow?(arr)
  arr.each_with_index do |val, i|
    # We check every subsequent element for a match
    (i + 1...arr.length).each do |j|
      return true if val == arr[j]
    end
  end
  false
end

def has_duplicates_fast?(arr)
  seen = {}
  arr.each do |val|
    return true if seen.key?(val)
    seen[val] = true
  end
  false
end

If you run both of these with an array of 5 elements, they will both feel instantaneous. You won't even be able to measure the difference. But the first method has a "nested loop." For every element, it potentially iterates over every other element. That's n * n, or O(n²). The second method uses a Hash, and checking a key in a Hash is (effectively) a constant-time operation. It only iterates through the list once. That's O(n).

If you give these methods a list of 100,000 items, the O(n) version will finish in a fraction of a second. The O(n²) version will likely make your laptop fan spin up and might not finish for an hour. That is the difference Big-O captures.

Identifying the "Instant" and the "Linear"

When you're analyzing your Ruby code, you'll encounter a few common patterns. The gold standard is O(1), or Constant Time. This means no matter how big your input is, the time taken remains the same. A perfect example in Ruby is accessing a value in a Hash by its key:

user_data = { "id" => 1, "name" => "Alice", "email" => "alice@example.com" }
# This is O(1). It doesn't matter if the hash has 3 keys or 3 million.
puts user_data["name"]

Then we have O(n), or Linear Time. This is what happens when you have a single loop over a collection. The time it takes grows in direct proportion to the size of the collection. If 10 items take 10ms, 100 items will take roughly 100ms.

# This is O(n) because we might have to check every single element
def find_user_by_email(users, target_email)
  users.each do |user|
    return user if user.email == target_email
  end
  nil
end

The hidden traps in Ruby's built-in methods

Here is where I see most experienced-looking Rubyists trip up. They think they've written an O(n) method because they only see one loop, but they've called a Ruby method inside that loop that is also O(n). This accidentally turns their code into O(n²).

Take a look at this common pattern:

def clean_duplicates(list)
  unique_items = []
  list.each do |item|
    # .include? is a linear search! It loops through unique_items internally.
    unique_items << item unless unique_items.include?(item)
  end
  unique_items
end

On the surface, it's one .each loop. But unique_items.include?(item) is actually another loop hidden under the hood. You are looping inside a loop. If your input list has 10,000 items, you are potentially performing 100 million checks. To fix this, you'd use a Set or a Hash, because their lookup time is O(1), bringing the whole operation back down to O(n).




📋 Practical Task

Optimizing the Guest List Filter

You are building a check-in system for a conference. You have an array of all_guests (everyone who registered) and an array of vip_guests. You need to write a method that returns a list of all guests who are not VIPs.

The current implementation is incredibly slow as the guest list grows. Your task is to refactor the filter_non_vips method to improve its Big-O complexity from O(n * m) to O(n + m).

# ALL_GUESTS and VIP_GUESTS are arrays of strings
all_guests = ["Alice", "Bob", "Charlie", "David", "Eve", "Frank"]
vip_guests = ["Bob", "Eve"]

def filter_non_vips(all_guests, vip_guests)
  non_vips = []
  
  all_guests.each do |guest|
    # This .include? call is the performance bottleneck!
    unless vip_guests.include?(guest)
      non_vips << guest
    end
  end
  
  non_vips
end

p filter_non_vips(all_guests, vip_guests) 
# Expected output: ["Alice", "Charlie", "David", "Frank"]

Requirement: Refactor the method using a Ruby Hash or Set to ensure that checking if a guest is a VIP happens in constant time O(1) rather than linear time O(m).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.