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/