LeetCode Solutions in Ruby: Patterns and Tips

Ruby works great for LeetCode. The syntax stays out of your way. Built-in methods handle the heavy lifting. Here is what I have learned grinding through hundreds of problems.

Why Ruby Shines for Algorithm Practice

Ruby's Enumerable module is your best friend. Methods like each_with_index, select, map, and reduce replace boilerplate loops. Hash lookups run in O(1). Arrays behave like stacks out of the box. String manipulation is terse and expressive compared to Java or C++.

The tradeoff? Ruby runs slower than C++ or Java. LeetCode's time limits usually accommodate this, but you need correct algorithmic complexity. A brute-force O(n^2) that passes in C++ might time out in Ruby. Focus on getting the algorithm right first, then lean on Ruby's standard library to keep your implementation tight.

Pattern 1: Hash Maps for O(1) Lookups

The classic Two Sum problem shows why hashes matter.

Problem: Find two numbers in an array that add up to a target. Return their indices.

def two_sum(nums, target)
  seen = {}
  nums.each_with_index do |num, i|
    complement = target - num
    return [seen[complement], i] if seen.key?(complement)
    seen[num] = i
  end
end

two_sum([2, 7, 11, 15], 9)  # => [0, 1]

The trick: store each number as you go. Check if its complement exists. One pass, O(n) time.

Use key? instead of checking for nil. It handles edge cases where the value itself is nil. This kind of clean, idiomatic approach is also central to Ruby design patterns that matter in production code.

Pattern 2: Stacks for Matching Problems

Parentheses validation comes up everywhere. Stacks make it simple.

Problem: Check if brackets are balanced and properly nested.

def valid_parentheses?(s)
  stack = []
  pairs = { ')' => '(', '}' => '{', ']' => '[' }

  s.each_char do |char|
    if pairs.key?(char)
      return false if stack.pop != pairs[char]
    else
      stack.push(char)
    end
  end

  stack.empty?
end

valid_parentheses?("()[]{}")  # => true
valid_parentheses?("(]")      # => false

Ruby arrays work as stacks. push and pop do what you expect. No need for a separate Stack class.

The hash maps closing brackets to openers. Clean and readable. Time complexity is O(n) — one pass through the string.

Pattern 3: Binary Search

Binary search cuts O(n) lookups to O(log n). Ruby has bsearch built into Array, but writing it by hand is a must-know for interviews.

Problem: Find the index of a target in a sorted array, or -1 if not found.

def binary_search(nums, target)
  left, right = 0, nums.length - 1

  while left <= right
    mid = left + (right - left) / 2
    if nums[mid] == target
      return mid
    elsif nums[mid] < target
      left = mid + 1
    else
      right = mid - 1
    end
  end

  -1
end

binary_search([1, 3, 5, 7, 9, 11], 7)  # => 3
binary_search([1, 3, 5, 7, 9, 11], 4)  # => -1

Note left + (right - left) / 2 instead of (left + right) / 2. This avoids integer overflow in languages with fixed-width integers. Ruby handles big integers natively, but using this form is a good habit that interviewers notice.

For problems like "find the first position of target" or "find the insertion point," tweak what happens at nums[mid] == target — push right = mid - 1 to keep searching left, or left = mid + 1 to search right.

Pattern 4: Sliding Window

Sliding window problems ask you to find a subarray or substring that meets some condition. The window expands and contracts as you scan.

Problem: Find the length of the longest substring without repeating characters.

def length_of_longest_substring(s)
  seen = {}
  max_len = 0
  start = 0

  s.each_char.with_index do |char, i|
    if seen.key?(char) && seen[char] >= start
      start = seen[char] + 1
    end
    seen[char] = i
    max_len = [max_len, i - start + 1].max
  end

  max_len
end

length_of_longest_substring("abcabcbb")  # => 3
length_of_longest_substring("bbbbb")     # => 1
length_of_longest_substring("pwwkew")    # => 3

The start pointer marks the left edge of the window. When a duplicate appears, jump start past the previous occurrence. The hash tracks the last index of each character. One pass, O(n) time, O(min(n, alphabet_size)) space.

Pattern 5: Depth-First Search with Recursion

Tree and graph traversal problems dominate the medium/hard tiers. Ruby's blocks make DFS concise.

Problem: Find the maximum depth of a binary tree.

TreeNode = Struct.new(:val, :left, :right)

def max_depth(root)
  return 0 if root.nil?
  1 + [max_depth(root.left), max_depth(root.right)].max
end

# Build a small tree: [3, 9, 20, nil, nil, 15, 7]
root = TreeNode.new(3,
  TreeNode.new(9),
  TreeNode.new(20, TreeNode.new(15), TreeNode.new(7))
)

max_depth(root)  # => 3

Struct.new creates a lightweight node class in one line. No boilerplate constructors. For problems that give you adjacency lists, a hash of arrays works well:

graph = Hash.new { |h, k| h[k] = [] }
edges.each { |a, b| graph[a] << b; graph[b] << a }

Essential Data Structures in Ruby

Ruby's standard library covers most of what you need on LeetCode. Here is how common data structures map:

Array — doubles as stack (push/pop), queue (push/shift, though shift is O(n)), and dynamic array. For most problems, this is enough.

Hash — your go-to for O(1) lookups, frequency counting, and memoization. Hash.new(0) for counters, Hash.new { |h, k| h[k] = [] } for adjacency lists.

Setrequire 'set' gives you O(1) include? checks. Use it when you only care about membership, not counts.

require 'set'
seen = Set.new
nums.each do |n|
  return true if seen.include?(n)
  seen.add(n)
end

SortedSet / Manual BST — Ruby does not have a built-in balanced BST. For problems requiring ordered operations (find min, find successor), sort an array or use a heap. The SortedSet from the sorted_set gem is not available on LeetCode.

Heap / Priority Queue — Not in Ruby's stdlib. For problems that need one, implement a minimal binary heap:

class MinHeap
  def initialize = @data = []
  def size = @data.size
  def push(val)
    @data << val
    sift_up(@data.size - 1)
  end
  def pop
    swap(0, @data.size - 1)
    val = @data.pop
    sift_down(0)
    val
  end
  private
  def sift_up(i)
    while i > 0
      parent = (i - 1) / 2
      break if @data[parent] <= @data[i]
      swap(i, parent)
      i = parent
    end
  end
  def sift_down(i)
    while (child = 2 * i + 1) < @data.size
      child += 1 if child + 1 < @data.size && @data[child + 1] < @data[child]
      break if @data[i] <= @data[child]
      swap(i, child)
      i = child
    end
  end
  def swap(a, b) = @data[a], @data[b] = @data[b], @data[a]
end

This covers problems like "Kth Largest Element" or "Merge K Sorted Lists."

Time Complexity Cheat Sheet

Knowing these complexities for Ruby operations saves debugging time:

OperationTime
Array#push / Array#popO(1) amortized
Array#shift / Array#unshiftO(n)
Array#include?O(n)
Array#sortO(n log n)
Hash#[] / Hash#[]=O(1) average
Hash#key?O(1) average
Set#include?O(1) average
String#include?O(n)

The big trap: using Array#include? inside a loop gives you O(n^2). Switch to a Hash or Set and it drops to O(n).

Ruby Tricks That Save Time

Guard clauses with trailing conditionals:

return [] if nums.empty?
return [0] if nums.length == 1

Many of these techniques overlap with functional programming patterns in Ruby, especially when chaining Enumerable methods.

Destructuring in blocks:

hash.each { |key, value| puts "#{key}: #{value}" }

Default hash values:

counter = Hash.new(0)
nums.each { |n| counter[n] += 1 }

Ranges for slicing:

nums[1..-1]  # everything except first
nums[0...-1] # everything except last

Common Gotchas

Integer division: Ruby 2.x divides integers correctly. 7 / 2 gives 3, not 3.5. Use 7.0 / 2 or 7.fdiv(2) when you need floats.

Mutable default arguments: Never use mutable objects as defaults.

# Bad
def add_item(item, list = [])
  list << item
end

# Good
def add_item(item, list = nil)
  list ||= []
  list << item
end

Off-by-one errors: Ruby's times, upto, and downto help avoid these.

5.times { |i| puts i }      # 0, 1, 2, 3, 4
1.upto(5) { |i| puts i }    # 1, 2, 3, 4, 5

When Ruby Falls Short

Some problems need raw speed. Bit manipulation problems sometimes hit time limits. Heavy recursion can stack overflow earlier than in other languages — Ruby's default stack size is smaller than Java's.

For recursion-heavy problems, convert to an iterative approach with an explicit stack. For problems where Ruby consistently times out, consider translating your working Ruby solution to Python or Java. Getting the logic right in Ruby first matters more than the language you submit in. If performance trade-offs interest you, read about Ruby vs Elixir for a deeper comparison of language strengths.

Quick Reference

PatternRuby Method
Frequency countHash.new(0)
Find duplicatesarray.tally
Sort by custom key`array.sort_by {
Group elements`array.group_by {
First n elementsarray.take(n)
All match condition`array.all? {
Any match condition`array.any? {

Ruby makes algorithm practice enjoyable. The code reads like pseudocode. That clarity helps you focus on the actual problem-solving instead of fighting syntax.

Start with easy problems. Build your pattern recognition. Once you have the five core patterns down — hash maps, stacks, binary search, sliding window, and DFS — you can tackle 80% of LeetCode's medium tier. The language will feel natural within a week, and Ruby's expressiveness will make your solutions shorter and easier to debug than most alternatives.