When writing tests in RSpec, especially in modern Rails 7+ apps with Ruby 3+, understanding test doubles, stubs, and mocks is essential for writing clean, fast, and maintainable tests.
In this guide, we’ll break down:
What are doubles, stubs, and mocks
When to use each
Common RSpec methods (let, let!, subject, allow, expect)
Test‑Driven Development (TDD) and Behavior‑Driven Development (BDD) are complementary testing approaches that help teams build robust, maintainable software by defining expected behaviour before writing production code. In TDD, developers write small, focused unit tests that fail initially, then implement just enough code to make them pass, ensuring each component meets its specification. BDD extends this idea by framing tests in a global language that all stakeholders—developers, QA, and product owners—can understand, using human-readable scenarios to describe system behaviour. While TDD emphasizes the correctness of individual units, BDD elevates collaboration and shared understanding by specifying the “why” and “how” of features in a narrative style, driving development through concrete examples of desired outcomes.
Mindset: “Does this behave as expected from user’s perspective?”
Style: More natural language, business-focused
🛠️ Frameworks Support Both Approaches
📋 RSpec (Primarily BDD-oriented)
# BDD Style - describing behavior
describe "TwoSum" do
context "when given an empty array" do
it "should inform user about insufficient data" do
expect(two_sum([], 9)).to eq('Provide an array with length 2 or more')
end
end
end
⚙️ Minitest (Supports Both TDD and BDD)
🔧 TDD Style with Minitest
class TestTwoSum < Minitest::Test
# Testing implementation correctness
def test_empty_array_returns_error
assert_equal 'Provide an array with length 2 or more', two_sum([], 9)
end
def test_valid_input_returns_indices
assert_equal [0, 1], two_sum([2, 7], 9)
end
end
🎭 BDD Style with Minitest
describe "TwoSum behavior" do
describe "when user provides empty array" do
it "guides user to provide sufficient data" do
_(two_sum([], 9)).must_equal 'Provide an array with length 2 or more'
end
end
describe "when user provides valid input" do
it "finds the correct pair indices" do
_(two_sum([2, 7], 9)).must_equal [0, 1]
end
end
end
🎯 Key Differences in Practice
🔄 TDD Approach
# 1. Write failing test
def test_two_sum_with_valid_input
assert_equal [0, 1], two_sum([2, 7], 9) # This will fail initially
end
# 2. Write minimal code to pass
def two_sum(nums, target)
[0, 1] # Hardcoded to pass
end
# 3. Refactor and improve
def two_sum(nums, target)
# Actual implementation
end
🎭 BDD Approach
# 1. Describe the behavior first
describe "Finding two numbers that sum to target" do
context "when valid numbers exist" do
it "returns their indices" do
# This describes WHAT should happen, not HOW
expect(two_sum([2, 7, 11, 15], 9)).to eq([0, 1])
end
end
end
📊 Summary Table
Aspect
TDD
BDD
Focus
Implementation correctness
User behavior
Language
Technical
Business/Natural
Frameworks
Any (Minitest, RSpec, etc.)
Any (RSpec, Minitest spec, etc.)
Test Names
test_method_returns_value
"it should behave like..."
Audience
Developers
Stakeholders + Developers
🎪 The Reality
RSpec encourages BDD but can be used for TDD
Minitest is framework-agnostic – supports both approaches equally
Your choice of methodology (TDD vs BDD) is independent of your framework choice
Many teams use hybrid approaches – BDD for acceptance tests, TDD for unit tests
The syntax doesn’t determine the methodology – it’s about how you think and approach the problem!
System Tests 💻⚙️
System tests in Rails (located in test/system/*) are full-stack integration tests that simulate real user interactions with your web application. They’re the highest level of testing in the Rails testing hierarchy and provide the most realistic testing environment.
System tests actually launch a real web browser (or headless browser) and interact with your application just like a real user would. Looking at our Rails app’s configuration: design_studio/test/application_system_test_case.rb
# frozen_string_literal: true
# :markup: markdown
gem "capybara", ">= 3.26"
require "capybara/dsl"
require "capybara/minitest"
require "action_controller"
require "action_dispatch/system_testing/driver"
require "action_dispatch/system_testing/browser"
require "action_dispatch/system_testing/server"
require "action_dispatch/system_testing/test_helpers/screenshot_helper"
require "action_dispatch/system_testing/test_helpers/setup_and_teardown"
module ActionDispatch
# # System Testing
#
# System tests let you test applications in the browser. Because system tests
# use a real browser experience, you can test all of your JavaScript easily from
# your test suite.
#
# To create a system test in your application, extend your test class from
# `ApplicationSystemTestCase`. System tests use Capybara as a base and allow you
# to configure the settings through your `application_system_test_case.rb` file
# that is generated with a new application or scaffold.
#
# Here is an example system test:
#
# require "application_system_test_case"
#
# class Users::CreateTest < ApplicationSystemTestCase
# test "adding a new user" do
# visit users_path
# click_on 'New User'
#
# fill_in 'Name', with: 'Arya'
# click_on 'Create User'
#
# assert_text 'Arya'
# end
# end
#
# When generating an application or scaffold, an
# `application_system_test_case.rb` file will also be generated containing the
# base class for system testing. This is where you can change the driver, add
# Capybara settings, and other configuration for your system tests.
#
# require "test_helper"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
# end
#
# By default, `ActionDispatch::SystemTestCase` is driven by the Selenium driver,
# with the Chrome browser, and a browser size of 1400x1400.
#
# Changing the driver configuration options is easy. Let's say you want to use
# the Firefox browser instead of Chrome. In your
# `application_system_test_case.rb` file add the following:
#
# require "test_helper"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :firefox
# end
#
# `driven_by` has a required argument for the driver name. The keyword arguments
# are `:using` for the browser and `:screen_size` to change the size of the
# browser screen. These two options are not applicable for headless drivers and
# will be silently ignored if passed.
#
# Headless browsers such as headless Chrome and headless Firefox are also
# supported. You can use these browsers by setting the `:using` argument to
# `:headless_chrome` or `:headless_firefox`.
#
# To use a headless driver, like Cuprite, update your Gemfile to use Cuprite
# instead of Selenium and then declare the driver name in the
# `application_system_test_case.rb` file. In this case, you would leave out the
# `:using` option because the driver is headless, but you can still use
# `:screen_size` to change the size of the browser screen, also you can use
# `:options` to pass options supported by the driver. Please refer to your
# driver documentation to learn about supported options.
#
# require "test_helper"
# require "capybara/cuprite"
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :cuprite, screen_size: [1400, 1400], options:
# { js_errors: true }
# end
#
# Some drivers require browser capabilities to be passed as a block instead of
# through the `options` hash.
#
# As an example, if you want to add mobile emulation on chrome, you'll have to
# create an instance of selenium's `Chrome::Options` object and add capabilities
# with a block.
#
# The block will be passed an instance of `<Driver>::Options` where you can
# define the capabilities you want. Please refer to your driver documentation to
# learn about supported options.
#
# class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
# driven_by :selenium, using: :chrome, screen_size: [1024, 768] do |driver_option|
# driver_option.add_emulation(device_name: 'iPhone 6')
# driver_option.add_extension('path/to/chrome_extension.crx')
# end
# end
#
# Because `ActionDispatch::SystemTestCase` is a shim between Capybara and Rails,
# any driver that is supported by Capybara is supported by system tests as long
# as you include the required gems and files.
class SystemTestCase < ActiveSupport::TestCase
include Capybara::DSL
include Capybara::Minitest::Assertions
include SystemTesting::TestHelpers::SetupAndTeardown
include SystemTesting::TestHelpers::ScreenshotHelper
..........
How They Work
System tests can:
Navigate pages: visit products_url
Click elements: click_on "New product"
Fill forms: fill_in "Title", with: @product.title
Verify content: assert_text "Product was successfully created"
test "visiting the index" do
visit products_url
assert_selector "h1", text: "Products"
end
Complex user workflow (from profile_test.rb):
def sign_in_user(user)
visit new_session_path
fill_in "Email", with: user.email
fill_in "Password", with: "password"
click_button "Log In"
# Wait for redirect and verify we're not on the login page anymore
# Also wait for the success notice to appear
assert_text "Logged in successfully", wait: 10
assert_no_text "Log in to your account", wait: 5
end
Key Benefits
End-to-end testing: Tests the complete user journey
JavaScript testing: Can test dynamic frontend behavior
Real browser environment: Tests CSS, responsive design, and browser compatibility
User perspective: Validates the actual user experience
When to Use System Tests
Critical user workflows (login, checkout, registration)
Complex page interactions (forms, modals, AJAX)
Cross-browser compatibility
Responsive design validation
Our profile_test.rb is a great example – it tests the entire user authentication flow, profile page navigation, and various UI interactions that a real user would perform.
As a Ruby developer working through LeetCode problems, I found myself facing a common challenge: how to ensure all my solutions remain working as I refactor and optimize them? With multiple algorithms per problem and dozens of solution files, manual testing was becoming a bottleneck.
Today, I’ll share how I set up a comprehensive GitHub Actions CI/CD pipeline that automatically tests all my LeetCode solutions, providing instant feedback and maintaining code quality.
🤔 The Problem: Testing Chaos
My LeetCode repository structure looked like this:
Complete Validation: Ensures all solutions work together
Cleaner CI History: Single status check per push/PR
Auto-Discovery: Automatically finds new test folders
❌ Rejected Alternative (Separate Actions):
More complex maintenance
Higher resource usage
Fragmented test results
More configuration overhead
🛠️ The Solution: Intelligent Test Discovery
Here’s the GitHub Actions workflow that changed everything:
name: Run All LeetCode Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Install dependencies
run: |
gem install minitest
# Add any other gems your tests need
- name: Run all tests
run: |
echo "🧪 Running LeetCode Solution Tests..."
# Colors for output
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Track results
total_folders=0
passed_folders=0
failed_folders=()
# Find all folders with test files
for folder in */; do
folder_name=${folder%/}
# Skip if no test files in folder
if ! ls "$folder"test_*.rb 1> /dev/null 2>&1; then
continue
fi
total_folders=$((total_folders + 1))
echo -e "\n${YELLOW}📁 Testing folder: $folder_name${NC}"
# Run tests for this folder
cd "$folder"
test_failed=false
for test_file in test_*.rb; do
if [ -f "$test_file" ]; then
echo " 🔍 Running $test_file..."
if ruby "$test_file"; then
echo -e " ${GREEN}✅ $test_file passed${NC}"
else
echo -e " ${RED}❌ $test_file failed${NC}"
test_failed=true
fi
fi
done
if [ "$test_failed" = false ]; then
echo -e "${GREEN}✅ All tests passed in $folder_name${NC}"
passed_folders=$((passed_folders + 1))
else
echo -e "${RED}❌ Some tests failed in $folder_name${NC}"
failed_folders+=("$folder_name")
fi
cd ..
done
# Summary
echo -e "\n🎯 ${YELLOW}TEST SUMMARY${NC}"
echo "📊 Total folders tested: $total_folders"
echo -e "✅ ${GREEN}Passed: $passed_folders${NC}"
echo -e "❌ ${RED}Failed: $((total_folders - passed_folders))${NC}"
if [ ${#failed_folders[@]} -gt 0 ]; then
echo -e "\n${RED}Failed folders:${NC}"
for folder in "${failed_folders[@]}"; do
echo " - $folder"
done
exit 1
else
echo -e "\n${GREEN}🎉 All tests passed successfully!${NC}"
fi
🔍 What Makes This Special?
🎯 Intelligent Auto-Discovery
The script automatically finds folders containing test_*.rb files:
# Skip if no test files in folder
if ! ls "$folder"test_*.rb 1> /dev/null 2>&1; then
continue
fi
This means new problems automatically get tested without workflow modifications!
The status badge is a visual indicator that shows the current status of your GitHub Actions workflow. It’s a small image that displays whether your latest tests are passing or failing.
🎨 What It Looks Like:
✅ When tests pass: ❌ When tests fail: 🔄 When tests are running:
📋 What Information It Shows:
Workflow Name: “Run All LeetCode Tests” (or whatever you named it)
Current Status:
Green ✅: All tests passed
Red ❌: Some tests failed
Yellow 🔄: Tests are currently running
Real-time Updates: Automatically updates when you push code
# Compare solution_v1.rb vs solution_v2.rb performance
💡 Conclusion: Why This Matters
This GitHub Actions setup transformed my LeetCode practice from a manual, error-prone process into a professional, automated workflow. The key benefits:
🎯 For Individual Practice
Confidence: Refactor without fear
Speed: Instant validation of changes
Quality: Consistent test coverage
🎯 For Team Collaboration
Standards: Enforced testing practices
Reviews: Clear CI status on pull requests
Documentation: Professional presentation
🎯 For Career Development
Portfolio: Demonstrates DevOps knowledge
Best Practices: Shows understanding of CI/CD
Professionalism: Industry-standard development workflow
🚀 Take Action
Ready to implement this in your own LeetCode repository? Here’s what to do next:
Copy the workflow file into .github/workflows/test.yml
Ensure consistent naming with test_*.rb pattern
Push to GitHub and watch the magic happen
Add the status badge to your README
Start coding fearlessly with automated testing backup!
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.
# 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.
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:
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:
Creating a new array slice in each iteration
Using index method to find the second number’s position
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:
Single pass through the array: O(n)
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:
We only traverse the array once
All operations inside the loop are constant time
We don’t have any nested loops or array slicing
Hash operations (lookup and insertion) are O(1)
♻️ Refactor Solution 3 ✅
This is still O(n):
Use a hash map to store numbers and their indices
Avoid array slicing
Avoid using index method
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:
Single pass through the array: O(n)
Hash operations (insertion and lookup) are O(1)
No array slicing or linear searches
Total complexity is O(n)
The algorithm works by:
For each number, calculate its complement (target – current_number)
Check if the complement exists in our hash
If found, return both indices
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)
Initial State:
hash = {} # Empty hash map
target = 9
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
@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:
We only need to store each number once in the hash
The hash stores numbers as keys and their indices as values
We check for complements before storing the current number
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:
hash[complement] gives us the index of the first number we found (the complement)
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:
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