← Back to DevBytes

Solving Two Sum Problem in Go: Step-by-Step Guide

Introduction to the Two Sum Problem

The Two Sum problem is one of the most iconic algorithmic challenges in computer science. Frequently featured as the first problem on platforms like LeetCode, it asks a deceptively simple question: given an array of integers and a target value, find two numbers in the array that add up to the target, and return their indices.

While the problem statement is short, it serves as an excellent gateway into algorithmic thinking, hash-based lookups, and time-space tradeoffs. In this tutorial, we'll explore how to solve it in Go (Golang), starting from a naive approach and progressing to an optimal solution.

Problem Statement

Given an array of integers nums and an integer target, return the indices of the two numbers such that they add up to target. You may assume that each input has exactly one solution, and you may not use the same element twice.

For example, given nums = [2, 7, 11, 15] and target = 9, the function should return [0, 1] because nums[0] + nums[1] = 2 + 7 = 9.

Why the Two Sum Problem Matters

The Two Sum problem is more than just an interview warm-up. It teaches several foundational concepts that appear repeatedly in real-world software engineering:

In production systems, similar patterns emerge whenever you need to find complementary records, deduplicate entries, or detect matching pairs in streaming data.

The Brute Force Approach

The most intuitive solution is to check every possible pair of numbers in the array. For each element, iterate through every other element and check if their sum equals the target.

Implementation

package main

import "fmt"

func twoSumBruteForce(nums []int, target int) []int {
    for i := 0; i < len(nums); i++ {
        for j := i + 1; j < len(nums); j++ {
            if nums[i]+nums[j] == target {
                return []int{i, j}
            }
        }
    }
    return nil
}

func main() {
    nums := []int{2, 7, 11, 15}
    target := 9
    result := twoSumBruteForce(nums, target)
    fmt.Println(result) // Output: [0 1]
}

Complexity Analysis

The brute force approach has a time complexity of O(n²) because of the nested loops. The space complexity is O(1) since no additional data structures are used. For small arrays, this is acceptable, but it becomes prohibitively slow as the input grows.

The Optimal Hash Map Approach

The key insight for optimization is recognizing that for each number x, we need to find a complementary number target - x. Instead of scanning the array for this complement every time, we can store numbers we've already seen in a hash map and check for the complement in constant time.

How It Works

We iterate through the array once. For each element, we calculate its complement (the value needed to reach the target). If the complement already exists in our hash map, we've found our pair. Otherwise, we store the current element's value as a key and its index as the value, then continue.

Implementation

package main

import "fmt"

func twoSum(nums []int, target int) []int {
    // Create a map to store value -> index pairs
    seen := make(map[int]int)

    for i, num := range nums {
        complement := target - num

        // Check if the complement exists in the map
        if j, found := seen[complement]; found {
            return []int{j, i}
        }

        // Store the current number with its index
        seen[num] = i
    }

    return nil
}

func main() {
    nums := []int{2, 7, 11, 15}
    target := 9
    result := twoSum(nums, target)
    fmt.Println(result) // Output: [0 1]

    // Additional test cases
    fmt.Println(twoSum([]int{3, 2, 4}, 6))       // Output: [1 2]
    fmt.Println(twoSum([]int{3, 3}, 6))           // Output: [0 1]
    fmt.Println(twoSum([]int{-1, -2, -3, -4}, -6)) // Output: [1 3]
}

Complexity Analysis

This approach reduces the time complexity to O(n) because we traverse the array only once, and each hash map lookup takes O(1) on average. The space complexity becomes O(n) in the worst case, as we may need to store nearly all elements in the map before finding a match.

Step-by-Step Walkthrough

Let's trace through the example nums = [2, 7, 11, 15] with target = 9:

The algorithm terminates after just two iterations, demonstrating the efficiency of the hash map approach.

Handling Edge Cases

Real-world inputs can be unpredictable. Here are some edge cases to consider and how the hash map solution handles them:

Best Practices

Choose the Right Data Structure

Go's built-in map type is ideal for this problem. It provides average O(1) lookup and insertion. Avoid using slices for lookups, as searching a slice is O(n).

Validate Inputs Early

In production code, always validate your inputs. Check for empty arrays, nil slices, and arrays with fewer than two elements before processing.

func twoSumSafe(nums []int, target int) ([]int, error) {
    if nums == nil || len(nums) < 2 {
        return nil, fmt.Errorf("input array must contain at least two elements")
    }

    seen := make(map[int]int)
    for i, num := range nums {
        complement := target - num
        if j, found := seen[complement]; found {
            return []int{j, i}, nil
        }
        seen[num] = i
    }

    return nil, fmt.Errorf("no two sum solution found")
}

Preallocate Maps When Possible

If you know the approximate size of the input, preallocating the map can reduce memory allocations and improve performance. Go's make(map[int]int, size) hint helps the runtime allocate an appropriately sized hash table.

Write Comprehensive Tests

Use Go's built-in testing framework to cover all edge cases:

package main

import "testing"

func TestTwoSum(t *testing.T) {
    tests := []struct {
        name     string
        nums     []int
        target   int
        expected []int
    }{
        {"basic case", []int{2, 7, 11, 15}, 9, []int{0, 1}},
        {"duplicates", []int{3, 3}, 6, []int{0, 1}},
        {"negatives", []int{-1, -2, -3, -4}, -6, []int{1, 3}},
        {"non-adjacent", []int{1, 5, 8, 3, 9}, 11, []int{2, 3}},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := twoSum(tt.nums, tt.target)
            if result[0] != tt.expected[0] || result[1] != tt.expected[1] {
                t.Errorf("got %v, want %v", result, tt.expected)
            }
        })
    }
}

Benchmark Your Solution

Use Go's benchmarking tools to measure performance and identify bottlenecks:

func BenchmarkTwoSum(b *testing.B) {
    nums := make([]int, 10000)
    for i := range nums {
        nums[i] = i
    }
    target := 19997 // Will match near the end

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        twoSum(nums, target)
    }
}

Common Mistakes to Avoid

Conclusion

The Two Sum problem is a perfect introduction to algorithmic problem solving in Go. By progressing from the brute force O(n²) approach to the optimal O(n) hash map solution, you learn how to identify opportunities for optimization through smarter data structure usage. The hash map pattern demonstrated here — storing previously seen values for constant-time lookup — is a versatile technique that applies to countless other problems, from finding duplicates to detecting cycles in data. By following the best practices of input validation, comprehensive testing, and performance benchmarking, you can write Go code that is not only correct but also production-ready. Master this foundational problem, and you'll be well-equipped to tackle more complex algorithmic challenges with confidence.

šŸ›  Tools from DevBytes

Inventory Tracker Pro — Excel inventory system, low-stock alerts Ā· $19
AI Dev Kit for Mac — local AI dev environment templates Ā· $9.99
KeyMapper for Mac — custom keyboard shortcut toolkit Ā· $7.99

← Back to all articles