Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
42: Big-O Notation with Ruby Examples
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).
There are no comments for now.