🏃‍♂️ Solving LeetCode Problems the TDD Way (Test-First Ruby): The Two Sum Problem

Welcome to my new series where I combine the power of Ruby with the discipline of Test-Driven Development (TDD) to tackle popular algorithm problems from LeetCode! 🧑‍💻💎 Whether you’re a Ruby enthusiast looking to sharpen your problem-solving skills, or a developer curious about how TDD can transform the way you approach coding challenges, you’re in the right place. In each episode, I’ll walk through a classic algorithm problem, show how TDD guides my thinking, and share insights I gain along the way. Let’s dive in and discover how writing tests first can make us better, more thoughtful programmers – one problem at a time! 🚀

🎯 Why I chose this approach

When I decided to level up my algorithmic thinking, I could have simply jumped into solving problems and checking solutions afterward. But I chose a different path – Test-Driven Development with Ruby – and here’s why this combination is pure magic ✨. Learning algorithms through TDD forces me to think before I code, breaking down complex problems into small, testable behaviors. Instead of rushing to implement a solution, I first articulate what the function should do in various scenarios through tests.

This approach naturally leads me to discover edge cases I would have completely missed otherwise – like handling empty arrays, negative numbers, or boundary conditions that only surface when you’re forced to think about what could go wrong. Ruby’s expressive syntax makes writing these tests feel almost conversational, while the red-green-refactor cycle ensures I’m not just solving the problem, but solving it elegantly. Every failing test becomes a mini-puzzle to solve, every passing test builds confidence, and every refactor teaches me something new about both the problem domain and Ruby itself. It’s not just about getting the right answer – it’s about building a robust mental model of the problem while writing maintainable, well-tested code. 🚀

🎲 Episode 1: The Two Sum Problem

#####################################
#   Problem 1: The Two Sum Problem
#####################################

# Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.

# You may assume that each input would have exactly one solution, and you may not use the same element twice.

# You can return the answer in any order.
# Example 1:

# Input: nums = [2,7,11,15], target = 9
# Output: [0,1]
# Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].
# Example 2:

# Input: nums = [3,2,4], target = 6
# Output: [1,2]
# Example 3:

# Input: nums = [3,3], target = 6
# Output: [0,1]

# Constraints:
# Only one valid answer exists.

# We are not considering following concepts for now:
# 2 <= nums.length <= 104
# -109 <= nums[i] <= 109
# -109 <= target <= 109

# Follow-up: Can you come up with an algorithm that is less than O(n2) time complexity?

🔧 Setting up the TDD environment

Create a test file first and add the first test case.

mkdir two_sum
touch test_two_sum.rb
touch two_sum.rb
# frozen_string_literal: true

require 'minitest/autorun'
require_relative 'two_sum'

###############################
# This is the test case for finding the index of two numbers in an array
# such that adding both numbers should be equal to the target number provided
#
#  Ex:
#    two_sum(num, target)
#    num: [23, 4, 8, 92], tatget: 12
#    output: [1, 2] => index of the two numbers whose sum is equal to target
##############################
class TestTwoSum < Minitest::Test
  def setup
    ####
  end

  def test_array_is_an_empty_array
    assert_equal 'Provide an array with length 2 or more', two_sum([], 9)
  end
end

Create the problem file: two_sum.rb with empty method first.

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
end

❌ Red: Writing the failing test

Run the test:

ruby test_two_sum.rb

Run options: --seed 58910
# Running:
F
Finished in 0.008429s, 118.6380 runs/s, 118.6380 assertions/s.

  1) Failure:
TestTwoSum#test_array_is_an_empty_array [test_two_sum.rb:21]:
--- expected
+++ actual
@@ -1 +1 @@
-"Provide an array with length 2 or more"
+nil

1 runs, 1 assertions, 1 failures, 0 errors, 0 skips

✅ Green: Making it pass

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  'Provide an array with length 2 or more' if nums.empty?
end

♻️ Refactor: Optimizing the solution

❌
# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  return 'Provide an array with length 2 or more' if nums.empty?

  nums.each_with_index do |selected_num, selected_index|
    nums.each_with_index do |num, index|
      if selected_index != index
        sum = selected_num[selected_index] + num[index]
        return [selected_index, index] if sum == target
      end
    end
  end
end

❌
# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  return 'Provide an array with length 2 or more' if nums.empty?

  nums.each_with_index do |selected_num, selected_index|
    nums.each_with_index do |num, index|
      next if selected_index == index

      sum = selected_num[selected_index] + num[index]
      return [selected_index, index] if sum == target
    end
  end
end

✅ 
# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  return 'Provide an array with length 2 or more' if nums.empty?

  nums.each_with_index do |selected_num, selected_index|
    nums.each_with_index do |num, index|
      next if index <= selected_index

      return [selected_index, index] if selected_num + num == target
    end
  end
end

Final

# frozen_string_literal: true

require 'minitest/autorun'
require_relative 'two_sum'

###############################
# This is the test case for finding the index of two numbers in an array
# such that adding both numbers should be equal to the target number provided
#
#  Ex:
#    two_sum(num, target)
#    num: [23, 4, 8, 92], tatget: 12
#    output: [1, 2] => index of the two numbers whose sum is equal to target
##############################
class TestTwoSum < Minitest::Test
  def setup
    ####
  end

  def test_array_is_an_empty_array
    assert_equal 'Provide an array with length 2 or more elements', two_sum([], 9)
  end

  def test_array_with_length_one
    assert_equal 'Provide an array with length 2 or more elements', two_sum([9], 9)
  end

  def test_array_with_length_two
    assert_equal [0, 1], two_sum([9, 3], 12)
  end

  def test_array_with_length_three
    assert_equal [1, 2], two_sum([9, 3, 4], 7)
  end

  def test_array_with_length_four
    assert_equal [1, 3], two_sum([9, 3, 4, 8], 11)
  end

  def test_array_with_length_ten
    assert_equal [7, 8], two_sum([9, 3, 9, 8, 23, 20, 19, 5, 30, 14], 35)
  end
end

# Solution 1 ✅ 

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

def two_sum(nums, target)
  return 'Provide an array with length 2 or more elements' if nums.length < 2

  nums.each_with_index do |selected_num, selected_index|
    nums.each_with_index do |num, index|
      already_added = index <= selected_index
      next if already_added

      return [selected_index, index] if selected_num + num == target
    end
  end
end

Let us analyze the time complexity of Solution 1 ✅ algorithm:
Our current algorithm is not less than O(n^2) time complexity. In fact, it is exactly O(n^2). This means for an array of length n, you are potentially checking about n(n−1)/2 pairs, which is O(n^2).

🔍 Why?
  • You have two nested loops:
  • The outer loop iterates over each element (nums.each_with_index)
  • The inner loop iterates over each element after the current one (nums.each_with_index)
  • For each pair, you check if their sum equals the target.
♻️ Refactor: Try to find a solution below n(^2) time complexity
# Solution 2 ✅ 

#####################################
# Solution 2
# TwoSum.new([2,7,11,15], 9).indices
#####################################
class TwoSum
  def initialize(nums, target)
    @numbers_array = nums
    @target = target
  end

  # @return [index_1, index_2]
  def indices
    return 'Provide an array with length 2 or more elements' if @numbers_array.length < 2

    @numbers_array.each_with_index do |num1, index1|
      next if num1 > @target # number already greater than target

      remaining_array = @numbers_array[index1..(@numbers_array.length - 1)]
      num2 = find_number(@target - num1, remaining_array)

      return [index1, @numbers_array.index(num2)] if num2
    end
  end

  private

  def find_number(number, array)
    array.each do |num|
      return num if num == number
    end
    nil
  end
end

Let us analyze the time complexity of Solution 2 ✅ algorithm:

  1. In the indices method:
  • We have an outer loop that iterates through @numbers_array (O(n))
  • For each iteration:
    => Creating a new array slice remaining_array (O(n) operation)
    => Calling find_number which is O(n) as it iterates through the remaining array
    => Using @numbers_array.index(num2) which is another O(n) operation

So the total complexity is:

  • O(n) for the outer loop
  • For each iteration:
  • O(n) for array slicing
  • O(n) for find_number
  • O(n) for index lookup

This gives us:

O(n * (n + n + n)) = O(n * 3n) = O(3n²) = O(n²)

The main bottlenecks are:

  1. Creating a new array slice in each iteration
  2. Using index method to find the second number’s position
  3. Linear search in find_number

Solution 3 ✅

To make this truly O(n), we should:

# Use a hash map to store numbers and their indices

# Solution 3 ✅  - Use Hash Map

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

class TwoSum
  def initialize(nums, target)
    @numbers_array = nums
    @target = target
  end

  # @return [index_1, index_2]
  def indices
    return 'Provide an array with length 2 or more elements' if @numbers_array.length < 2

    hash = {}

    @numbers_array.each_with_index do |num, index|
      complement = @target - num

      # store first number to hash
      if index == 0
        hash[num] = index
      else
        # if not first number check store has
        return [hash[complement], index] if hash.key?(complement)

        # if not found store the num
        hash[num] = index
      end
    end
  end
end

Let us analyze the complexity of the current code:

def indices
  return 'Provide an array with length 2 or more elements' if @numbers_array.length < 2

  hash = {}

  @numbers_array.each_with_index do |num, index|
    complement = @target - num

    # store first number to hash
    if index == 0
      hash[num] = index 
    else
      # if not first number check store has 
      if hash.key?(complement)
        return [hash[complement], index]
      else
        # if not found store the num
        hash[num] = index
      end
    end
  end
end

The complexity is O(n) because:

  1. Single pass through the array: O(n)
  2. For each iteration:
  • Hash lookup (hash.key?(complement)): O(1)
  • Hash insertion (hash[num] = index): O(1)
  • Basic arithmetic (@target - num): O(1)

Total complexity = O(n) * O(1) = O(n)

The code is still efficient because:

  1. We only traverse the array once
  2. All operations inside the loop are constant time
  3. We don’t have any nested loops or array slicing
  4. Hash operations (lookup and insertion) are O(1)

♻️ Refactor Solution 3 ✅

This is still O(n):

  1. Use a hash map to store numbers and their indices
  2. Avoid array slicing
  3. Avoid using index method
  4. Make a single pass through the array
# ♻️ Refactor Solution 3 ✅  - Use Hash Map

# frozen_string_literal: true

# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}

class TwoSum
  def initialize(nums, target)
    @numbers_array = nums
    @target = target
  end

  # @return [index_1, index_2]
  def indices
    return 'Provide an array with length 2 or more elements' if @numbers_array.length < 2

    hash = {}

    @numbers_array.each_with_index do |num, index|
      complement = @target - num

      return [hash[complement], index] if hash.key?(complement)

      hash[num] = index
    end
  end
end

This refactored solution has O(n) time complexity because:

  1. Single pass through the array: O(n)
  2. Hash operations (insertion and lookup) are O(1)
  3. No array slicing or linear searches
  4. Total complexity is O(n)

The algorithm works by:

  1. For each number, calculate its complement (target – current_number)
  2. Check if the complement exists in our hash
  3. If found, return both indices
  4. If not found, store the current number and its index in the hash

The key differences:

  • Instead of searching for complements in the remaining array, we store numbers we’ve seen in a hash
  • When we see a new number, we check if its complement exists in our hash
  • If found, we return both indices
  • If not found, we store the current number and its index

Detailed explanation of refactored solution 3

I’ll explain how the hash map solution works step by step using the example:

# Input Sample
TwoSum.new([2,7,11,15], 9)
  1. Initial State:
   hash = {}  # Empty hash map
   target = 9
  1. First Iteration (num = 2, index = 0):
   complement = 9 - 2 = 7
   hash = {}  # Empty, so complement 7 not found
   hash[2] = 0  # Store 2 with its index 0
  1. Second Iteration (num = 7, index = 1):
   complement = 9 - 7 = 2
   hash = {2 => 0}  # Found complement 2 in hash!
   return [hash[2], 1]  # Returns [0, 1]

Let’s break down what happens in each iteration:

@numbers_array.each_with_index do |num, index|
  complement = @target - num  # Calculate what number we need

  if hash.key?(complement)   # Check if we've seen the number we need
    return [hash[complement], index]  # If found, return both indices
  end

  hash[num] = index  # If not found, store current number and its index
end

Key points:

  1. We only need to store each number once in the hash
  2. The hash stores numbers as keys and their indices as values
  3. We check for complements before storing the current number
  4. We only need one pass through the array

This is efficient because:

  • Hash lookups are O(1)
  • We only traverse the array once
  • We don’t need to search through the array multiple times
  • We don’t need to create array slices

Why the index order has complement index first?

The order of indices in the return statement [hash[complement], index] is important because:

  1. hash[complement] gives us the index of the first number we found (the complement)
  2. index gives us the current position (the second number)

We return them in this order because:

  • The complement was stored in the hash earlier in the array
  • The current number is found later in the array
  • This maintains the order of appearance in the original array

For example, with [2,7,11,15] and target 9:

  1. When we see 7 at index 1:
  • We look for complement 2 (9-7)
  • 2 was stored at index 0
  • So we return [0, 1] (indices of [2,7])

If we returned [index, hash[complement]], we would get [1, 0] instead, which would be the reverse order. While the problem allows returning the answer in any order, returning them in the order they appear in the array is more intuitive and matches the example outputs in the problem description.

✅ Solution 4

# Solution 4 ✅  - Use Hash Map
# @param {Integer[]} nums
# @param {Integer} target
# @return {Integer[]}
def two_sum(nums, target)
  return 'Provide an array with length 2 or more elements' if nums.length < 2

  # number index store, use hash map, store first number in store
  store = { nums[0] => 0}
  
  # check the pair from second element
  nums.each_with_index do |num, index|
    next if index == 0 # already stored first
    pair = target - num

    return [store[pair], index] if store[pair]

    store[num] = index
  end
end

Check my LeetCode progress:

The Problem: https://leetcode.com/problems/two-sum/description/

Solution: https://leetcode.com/problems/two-sum/submissions/1662877573/

🧠 Lessons learned

  1. Solution 1 ✅ – We found our first solution which is working fine. But has o(n^2)
  2. Solution 2 ✅ – We refactored and found our second solution which is working fine. But also has o(n^2)
  3. Solution 3 ✅ – We refactored to hash_map which is working fine and has time complexity o(n)! 💥

Happy Algo Coding! 🚀

Ruby Coding Bootcamp – Part 1: Basics (String & Array)

Common patterns: iteration, hashing, two-pointer, basic transformations. Try each yourself first – ask for solutions/hints per question when ready.

  1. Reverse a string without using .reverse
    reverse_string("hello") => "olleh"
  2. Palindrome check
    palindrome?("racecar") => true
    palindrome?("hello") => false
  3. Count vowels
    count_vowels("programming") => 3
  4. Find max in array without .max
    find_max([3, 7, 2, 9, 4]) => 9
  5. Remove duplicates from array, preserve order
    remove_duplicates([1,2,2,3,1,4]) => [1,2,3,4]
  6. FizzBuzz (1 to n)
    fizzbuzz(15) => ["1","2","Fizz","4","Buzz",...,"FizzBuzz"]
  7. Anagram check
    anagram?("listen", "silent") => true
  8. Sum of array, no .sum
    array_sum([1,2,3,4]) => 10
  9. Capitalize each word (title case), no .capitalize on whole string
    title_case("the ruby language") => "The Ruby Language"
  10. Find second largest number
    second_largest([4, 1, 9, 7, 9]) => 7
  11. Find Alice and Bob spending amounts
    details = [{ amount: 2500, requestor: 'Alice', id: 23 }...
  12. Pattern Check
    pattern_check("{()}") # => true

1. Reverse a string without using .reverse

Concept

1. finding the last string character index to find the last string character first

2. then decreasing the index to find the upto the first character

def reverse_string(str)
  last_str_index = str.length - 1
  result = ""
  
  while last_str_index >= 0
    result << str[last_str_index]

    last_str_index -= 1
  end

  result
end

puts reverse_string("hello")
puts reverse_string("programming")

Solution 2: Another way without using Index variables

def reverse_string(str)
  str.each_char.reduce("") { |result, char| char + result }
end

Concept

Prepend each character to an accumulator instead of appending – that flips the order without touching any index. each_char + reduce replaces the while-loop/counter entirely.

2. Palindrome check

def palindrome?(str)
  first_char_index = 0
  last_char_index = str.length - 1

  while first_char_index < last_char_index
    if str[first_char_index] != str[last_char_index]
      return false 
    end

    first_char_index += 1
    last_char_index -= 1
  end

  return true
end

p palindrome?("ala")
p palindrome?("alla")
p palindrome?("racecar")
p palindrome?("car")

Concept

  1. Two pointer approach

Two indices start at opposite ends of the string and move toward each other, comparing elements pairwise:

  • first_char_index starts at 0, last_char_index starts at length - 1
  • Each iteration compares str[first] vs str[last] – if they ever mismatch, it can’t be a palindrome, so return immediately
  • Otherwise, both pointers move inward (first += 1, last -= 1) until they meet or cross (first < last becomes false)
  • If the loop finishes without a mismatch, all mirrored pairs matched → palindrome

Why this pattern in general: it’s the go-to when you need to compare elements from both ends of a sequence without extra space – palindromes, reversing in-place, “sorted array pair sum” problems, container/water-trapping problems all reuse this exact skeleton (two indices, converge or diverge, one comparison per step).

One edge case worth saying out loud in an interview: this correctly handles even-length (abba) and odd-length (aba, middle char never gets compared to itself) without any special-casing – that’s often a follow-up question.

3. Count vowels

1. Without Using Array#each or any other Enumerable methods

def count_vowels(str)
  vowels = ['a', 'e', 'i', 'o', 'u']
  vowel_count = 0
  first = 0 

  while first <= str.length - 1
    if vowels.include?(str[first])
      vowel_count += 1
    end

    first += 1
  end

  vowel_count
end


p count_vowels("programming")
p count_vowels("ala")
p count_vowels("Grow your audience by promoting your content")

Concept

  1. Using a pointer

2. Using Enumerable#count

# using `Enumerable#count`

def count_vowels(str)
  vowels = ['a', 'e', 'i', 'o', 'u']

  str.each_char.count { |char| vowels.include?(char.downcase) }
end

p count_vowels("programming")
p count_vowels("ala")
p count_vowels("Grow your audience by promoting your content A")

Enumerable#count

The Enumerable#count method in Ruby returns the number of elements in a collection, optionally filtering them based on an item or a truthy block criterion.

[10, 20, 30].count 
# => 3

{ a: 1, b: 2 }.count 
# => 2

[1, 2, 4, 2, 1, 2].count(2) 
# => 3

["apple", "banana", "apple"].count("apple") 
# => 2

# Count numbers greater than 10
[5, 12, 8, 18, 3].count { |num| num > 10 } 
# => 2

# Count odd numbers using symbol-to-proc syntax
[1, 2, 3, 4, 5].count(&:odd?) 
# => 3

# With a Hash, it yields both the key and the value
{ candy: 5, apples: 2, cookies: 10 }.count { |key, value| value > 4 } 
# => 2

3. Using Enumerable#select

# using `String#each_char` and `Enumerable#select`

def count_vowels(str)
  vowels = ['a', 'e', 'i', 'o', 'u']
  
  str.each_char.select { |char| vowels.include?(char.downcase) }.size
end

p count_vowels("programming")
p count_vowels("ala")
p count_vowels("Grow your audience by promoting your content A")

In Ruby, Enumerable#select (Aka filter, find_all) is an inbuilt method used to filter a collection by evaluating each element against a given block and returning only the items for which the block evaluates to true.

collection.select { |element| condition }

With a block: Returns a new collection containing all elements that match the condition.
Without a block: Returns an Enumerator object.
Aliases: filter and find_all are exact aliases and perform identically

reject –> The opposite of select; returns all elements that evaluate to false.

find / detect –> Returns only the first element that matches the condition, then stops iterating.

## Array
numbers = [1, 2, 3, 4, 5, 6]

# Using block syntax to get even numbers
even_numbers = numbers.select { |num| num.even? }
# => [2, 4, 6]

# Short-hand symbol-to-proc syntax
even_numbers = numbers.select(&:even?)
# => [2, 4, 6]

## Hash
scores = { alice: 95, bob: 65, charlie: 82 }

# Filter for scores greater than 70
passing = scores.select { |name, score| score > 70 }
# => {:alice=>95, :charlie=>82}

users = [
  { name: "Alice", active: true },
  { name: "Bob", active: false },
  { name: "Charlie", active: true }
]

active_users = users.select { |user| user[:active] }
# => [{:name=>"Alice", :active=>true}, {:name=>"Charlie", :active=>true}]

4. Using Array#each

In Ruby, each is not actually a method defined by the Enumerable module itself; instead, it is a method requirement that your class must implement.

The Enumerable module acts as a mixin that provides collection-handling capabilities (like .map, .select and .reduce). However, for those methods to function, your custom collection class must define its own #each method to yield items sequentially.

# using `String#split` and `Array#each`

def count_vowels(str)
  vowels = ['a', 'e', 'i', 'o', 'u']
  vowel_count = 0
  
  str.split('').each do |char|  
    vowel_count += 1 if vowels.include?(char) 
  end

  vowel_count
end

p count_vowels("programming")
p count_vowels("ala")
p count_vowels("Grow your audience by promoting your content")

Best: Solution 2 (each_char.count)

def count_vowels(str)
vowels = ['a', 'e', 'i', 'o', 'u']
str.each_char.count { |char| vowels.include?(char.downcase) }
end

count with a block is built exactly for “how many elements satisfy this predicate” – it says what you want, not how to accumulate it. One line of actual logic, no throwaway accumulator variable. This is what a senior Ruby dev would write.

Why the others rank lower:

  • Solution 1 (your index/while version): correct, but same critique as Q1 – manual pointer + counter for something Enumerable does in one call. Fine as a “let me show I understand the mechanics” opener, but don’t lead with it if asked for idiomatic Ruby.
  • Solution 3 (select.size): works, but wasteful – select builds an intermediate array just to throw it away and count its size. count does the same job without the allocation. Small thing, but an interviewer watching for efficiency awareness will notice.
  • Solution 4 (split('').each): split('') allocates a full array up front; each_char iterates lazily without materializing one. Also reintroduces the manual counter that count eliminates. Weakest of the four.

4. Find max in array without .max

# using `Array#each`

def find_max(array)
  max = nil

  array.each do |num|
    max = num if max.nil? || num > max
  end
  
  max
end

p find_max([])
p find_max([3, 7, 2, 9, 4])
p find_max([32, 7, 29, 79, 41])

This is actually the idiomatic version – no index needed since each gives you the values directly, and seeding max with nil (instead of array[0] or 0) correctly handles edge cases: empty array returns nil instead of crashing or silently returning wrong data, and it works for negative-only arrays where seeding with 0 would be a bug.

Concept:

  1. linear scan with running accumulator

Track the best-seen-so-far value in a variable, compare each new element against it, update when you find something better. This is the base pattern behind max/min, and generalizes directly to “find the element matching some condition” problems (max by custom criteria, longest string, etc.).

One thing worth saying in an interview: this is O(n) time, O(1) space and it’s actually not worse than Array#max – that’s what .max does internally too.

Only nitpick: num > max – if you want strict correctness on the first iteration, walk through it: max is nil, max.nil? short-circuits true, so num > max never evaluates against nil (which would raise). Good – that’s intentional short-circuit ordering, not luck. Just make sure you can explain why the order of the || matters if asked.

5. Remove duplicates from array, preserve order

Using Array#uniq

def remove_duplicates(array)
  array.uniq
end

remove_duplicates([1,2,2,3,1,4]) => [1,2,3,4]

Without using uniq, select etc.

At First I tried to iterate over array using array index and deleting the duplicated value, then pass the mutated array recursively into the Method. This cause issue like: mutating array (via .delete) while iterating over it with each_with_index – the index no longer matches the shrinking array, so later lookups go out of bounds and return nil.

X - WRONG
array.each_with_index do |num, index|
other_nums = array[(index + 1)..-1]
other_nums.each do |next_num|
if num == next_num # duplicate num
uniq_nums << array.delete(num) # store duplicated
# find duplicate without duplicated num
remove_duplicates(array, uniq_nums)
end
end
end
uniq_nums + array

Nested loops (which is what pushed me to O(n²) and the mutation trap in the first place)

General rule: never mutate a collection you’re actively iterating over. This is a classic bug.

Fix – hash-based “seen” tracker, single pass, no uniq/select:

def remove_duplicates(array)
  seen = {}

  array.each do |num|
    seen[num] = true unless seen[num]
  end

  seen.keys
end

p remove_duplicates([1, 2, 2, 3, 1, 4, 3])
p remove_duplicates([7, 4, 2, 7, 2, 8, 4])
p remove_duplicates([8, 1, 0, 8, 0, 0, 1, 5, 6])

Concept:

  1. seen-set / membership tracking

Use a hash as an O(1) lookup table for “have I encountered this before?” instead of nested loops. Single pass:

  • New value → mark it seen, keep it
  • Already seen → skip it, original order preserved naturally since you only append once per unique value

This pattern is the backbone of dedup, “first unique element” – same seen/counts hash idea reused everywhere. No recursion needed;

6. FizzBuzz (1 to n)

The Fizz Buzz problem requires writing a program that prints or returns numbers from 1 to a given integer n, replacing multiples of 3 with “Fizz”, multiples of 5 with “Buzz”, and multiples of both 3 and 5 with “FizzBuzz”.

https://leetcode.com/problems/fizz-buzz/description

The Rules

For every integer i from 1 to n:

  • Print “FizzBuzz” if i is divisible by both 3 and 5 (i.e., a multiple of 15).
  • Print “Fizz” if i is divisible only by 3.
  • Print “Buzz” if i is divisible only by 5.
  • Print the number itself as a string if none of the above conditions match.

Example (n = 15)

If n = 15, the output sequence looks like this:
1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"

def fizzbuzz(limit)
  result = []
  (1..limit).each do |num|
      if num % 15 == 0
        result << "FizzBuzz"
      elsif num % 3 == 0
        result << "Fizz"
      elsif num % 5 == 0
        result << "Buzz"
      else
        result << num.to_s
      end
  end
  
  result
end

p fizzbuzz(15)
p fizzbuzz(30)

Concept:

  1. range iteration + conditional branching, no state carried between iterations (unlike Q5’s seen hash) – each number is judged independently, so a simple each with if/elsif is the correct tool, nothing fancier needed.

One idiomatic variant worth knowing for interviews, using map instead of manual array-building:

def fizzbuzz(limit)
  (1..limit).map do |num|
    if num % 15 == 0
      "FizzBuzz"
    elsif num % 3 == 0
      "Fizz"
    elsif num % 5 == 0
      "Buzz"
    else
      num.to_s
    end
  end
end

7. Anagram check

def anagram?(first, second)
  return false unless first.length == second.length

  first_count_hash = Hash.new(0)
  second_count_hash = Hash.new(0)

  first.each_char {|char| first_count_hash[char] += 1 }
  second.each_char {|char| second_count_hash[char] += 1 }

  first_count_hash == second_count_hash
end

p anagram?("listen", "silent")
p anagram?("note", "tone")
p anagram?("act", "cat")
p anagram?("earth", "heart")
p anagram?("earth", "hears")

Fix – frequency count comparison (no sort, no mutation, no uniq/tally even):

CONcept

  1. frequency-count comparison

Two strings are anagrams iff they have identical character-frequency distributions. Build a count hash for each string (Hash.new(0) gives a default of 0 so += 1 works without checking key? first), then compare the two hashes directly – Ruby’s Hash#== checks all key-value pairs match, regardless of insertion order.

This is the same “seen/counts” hash idea from Q5, reused again – frequency-count hashes are one of the most repeated tools across string/array interview problems (anagrams, first-unique-char, “group anagrams,” character-frequency questions). Worth internalizing as your default first move whenever a problem involves counting occurrences of something.

8. Sum of array, no .sum

Using Array#inject

def array_sum(array)
  array.inject(:+)
end

Without using Array#inject

# without using .sum, .inject
def array_sum(array)
  return "provide non-empty array" if array.empty?

  sum = 0
  array.each do |num|
    sum += num
  end

  sum
end

p array_sum([1,2,3,4])
p array_sum([2,72,1,0])

Correct Solution. each with an accumulator is the right idiom here – this is one of the few cases where a keeping-total variable isn’t a code smell, because you genuinely need to carry state (the sum) across iterations, unlike Q6’s FizzBuzz where each element was independent.

Concept:

  1. accumulator pattern

Since I am avoiding .sum/.inject here specifically, Good to say explicitly: “In production I’d use .sum; writing it manually to show the mechanics.”

9. Capitalize each word (title case), no .capitalize on whole string

def title_case(sentence)
  sentence.split.map { |word| word[0].upcase + word[1..] }.join(' ')
end

p title_case("the ruby language")

Concept:

  1. split → transform each element → rejoin. split (whitespace-aware, collapses multiple spaces automatically) → map for a 1-to-1 word transform (same reasoning as Q6’s FizzBuzz – independent per-element transform, no accumulator needed) → join to reassemble.

10. Find second largest number

# this has one BUG - check below

def second_largest(array)
  return nil if array.length < 2

  second_largest = array.first
  largest = array.first

  array[1..].each do |num|
    if num > largest
      second_largest = largest
      largest = num
    elsif num < largest && num > second_largest
      second_largest = num
    end
  end

  second_largest
end


p second_largest([4, 1, 9, 7, 9])
p second_largest([4, 3, 2, 3, 4])
p second_largest([50, 100, 150, 200])

Concept:

  1. single-pass dual-tracking – same accumulator idea as Q4/Q8, but tracking two running values instead of one, with an ordering dependency between them (you can only correctly update second_largest relative to where largest currently stands, which is why the elsif must re-check against both bounds, not just one).

BUG FOUND:

second_largest([4, 3, 2, 3, 4]) => 4 X WRONG - Why?

Good catch – real bug. Walk through it:

largest = second_largest = 4 (both seeded to array.first)

The real problem: initializing both trackers to the same value creates a chicken-and-egg lockout – second_largest can only be beaten by something bigger, but it started at the max, so nothing (except a new largest) can ever unseat it.

Fix – seed with -Infinity, don’t assume array.first is a valid second-place candidate:

# FIXED

def second_largest(array)
  return nil if array.length < 2

  largest = second_largest = -Float::INFINITY

  array.each do |num|
    if num > largest
      second_largest = largest
      largest = num
    elsif num > second_largest && num != largest
      second_largest = num
    end
  end

  second_largest
end

Concept refinement:

  1. dual-tracker pattern (Q10 original) + -Float::INFINITY, not real array values – this is the standard idiom for “find min/max/second-max” problems specifically because it guarantees the first comparison always succeeds and updates correctly – good to be able to answer each one individually if an interviewer pushes on “why did you add that condition?”

NOTE: This is a good one to remember: never seed a running-max/min tracker with an actual data element unless you’re certain it can’t create a lockout — -Infinity/nil-then-check (like your Q4 find_max) are the safe patterns.

11. Find Alice and Bob spending amount from orders

Qn) Find the total amount spend by Alice and Bob

details = [
  { amount: 2500, requestor: 'Alice', id: 23 },
  { amount: 2500, requestor: 'John', id: 22 },
  { amount: 1500, requestor: 'Bob', id: 21 },
  { amount: 1500, requestor: 'Alice', id: 23 },
  { amount: 1000, requestor: 'Bob', id: 21 },
  { amount: 500, requestor: 'Sera', id: 20 }
]

Answer:

# Answer 1 (filter + each + accumulator)

result = Hash.new(0)
filters = ["Alice", "Bob"]
details.filter { |order| filters.include?(order[:requestor]) }
       .each {|order| result[order[:requestor]] += order[:amount]  } 

puts result
# Answer 2 (filter + group_by + each_pair + reduce + accumulator)

filters = ["Alice", "Bob"]

result = []

details.filter {|order| filters.include?(order[:requestor]) }
       .group_by {|order| order[:requestor]}
       .each_pair { |user, orders| result << { "#{user}": orders.reduce(0) {|sum, order| sum += order[:amount] } } }

puts result

12. Pattern check

Qn) Find the expression pattern match correctly with words included in it

# my solution

def pattern_check(pattern = "")
  return true if pattern.empty?

  pairs_hash = {
    "{" => "}",
    "(" => ")",
    " " => " "
  }

  symbols = pattern.split("")
  last_index = symbols.size - 1
  first_half = last_index / 2

  # p symbols

  idx = 0
  while (idx <= first_half)
    symbol = symbols[idx]
    pair = symbols[last_index - idx]

    # p "symbol: #{symbol}"
    # p "pair index: #{last_index - idx}"
    # p "static pair symbol: #{pairs_hash[symbol]}"
    # p "pair symbol received: #{pair}"
    
    if symbol.match(/\w+/) && pair.match(/\w+/)
      idx += 1
      next
    end

    unless pairs_hash[symbol] == pair
      return false
    end
    
    idx += 1
  end

  return true
end

p pattern_check("{()}") # => true

p pattern_check("{(})") # => false

p pattern_check("{( text )}") # => true

The above solution is based on Two Pointer approach and is not correct.

Check for the correct solution (Stack Approach) here: https://railsdrop.com/what-the-question-is-actually-asking/