Задача

Функция получает на вход целочисленный срез (nums) и результат сложения (target). Необходимо вернуть индексы двух чисел, сумма которых равна target.

Предполагается, что каждый вход будет иметь ровно одно решение и мы не может испозовать один и тот же элемент дважды. Ответ можно вернуть в любом порядке.

Пример

Input: nums = [2,7,11,15], target = 9
Output: [0,1]
Output: Because nums[0] + nums[1] == 9, we return [0, 1].
Input: nums = [3,2,4], target = 6
Output: [1,2]
Input: nums = [3,3], target = 6
Output: [0,1]

Тесты

func TestTwoSum(t *testing.T) {
	cases := []struct {
		nums   []int
		target int
		want   []int
	}{
		{[]int{2, 7, 11, 15}, 9, []int{0, 1}},
		{[]int{3, 2, 4}, 6, []int{1, 2}},
		{[]int{3, 3}, 6, []int{0, 1}},
	}
	for _, s := range cases {
		got := twoSum(s.nums, s.target)
		if !reflect.DeepEqual(got, s.want) {
			t.Errorf("twoSum = %v; want %v", got, s.want)
		}
	}
}

Approach 1: Brute Force

Brute force подход самый простой. Проходим в цикле по всем элементам x и ищем, существует ли другое значение равное target - x.

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

Временная сложность: O(n^2).

Для каждого элемента мы пытаемся найти его слагаемое, перебирая остальную часть среза, что занимает O(n) времени. Следовательно, временная сложность будет O(n^2).

Approach 2: Two-pass Hash Table

Чтобы улучшить нашу сложность во время выполнения, нам нужен более эффективный способ проверить, существует ли слагаемое в срезе. Если слагаемое существует, нам нужно найти его индекс. Какой лучший способ поддерживать отображение каждого элемента среза в его индекс? Хеш-таблица.

Мы сокращаем время поиска с O(n) до O(1). Хеш-таблица создана именно для этой цели, она поддерживает быстрый поиск почти за постоянное время. Я говорю “почти”, потому что в случае коллизии получение значения из хеш-таблицы может деградировать до O(n).

Простая реализация использует две итерации. На первой итерации мы добавляем значение каждого элемента и его индекс в хеш-таблицу. Затем, на второй итерации мы проверяем, существует ли в таблице слагаемое для target - nums[i]. Важно, что слагаемое не должно быть равно nums[i].

func twoSum(nums []int, target int) []int {
    m := make(map[int]int)
    for i := range nums {
        m[nums[i]] = i
    }
    for i := range nums {
        complement := target - nums[i]
        if v, ok := m[complement]; ok {
            if m[complement] != i {
                return []int{i, v}
            }
        }
    }
    return nil
}

Временная сложность: O(n).

Обходим срез, содержащий n элементов, ровно дважды. Поскольку хеш-таблица сокращает время поиска до O(1), то сложность становится O(n).

Approach 3: One-pass Hash Table

На самом деле, мы можем решить задачу за один проход. В то время как мы выполняем итерацию и вставляем элементы в таблицу, мы также оглядываемся назад, чтобы проверить, существует ли уже слагаемое текущего элемента в таблице. Если оно существует, мы нашли решение и немедленно возвращаем ответ.

func twoSum(nums []int, target int) []int {
    m := make(map[int]int)
    for i := range nums {
        complement := target - nums[i]
        if v, ok := m[complement]; ok {
                return []int{i, v}
        }
        m[nums[i]] = i
    }
    return nil
}

Временная сложность: O(n)