Golang:找到两个数字的索引,其中两个数字之和等于目标数字

The problem is: find the index of two numbers that nums[index1] + nums[index2] == target. Here is my attempt in golang (index starts from 1):

package main

import (
    "fmt"
)

var nums = []int{0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 25182, 25184, 25186, 25188, 25190, 25192, 25194, 25196} // The number list is too long, I put the whole numbers in a gist: https://gist.github.com/nickleeh/8eedb39e008da8b47864
var target int = 16021

func twoSum(nums []int, target int) (int, int) {
    if len(nums) <= 1 {
        return 0, 0
    }
    hdict := make(map[int]int)
    for i := 1; i < len(nums); i++ {
        if val, ok := hdict[nums[i+1]]; ok {
            return val, i + 1
        } else {
            hdict[target-nums[i+1]] = i + 1
        }
    }
    return 0, 0
}

func main() {
    fmt.Println(twoSum(nums, target))
}

The nums list is too long, I put it into a gist: https://gist.github.com/nickleeh/8eedb39e008da8b47864

This code works fine, but I find the return 0,0 part is ugly, and it runs ten times slower than the Julia translation. I would like to know is there any part that is written terrible and affect the performance?

Edit: Julia's translation:

function two_sum(nums, target)
    if length(nums) <= 1
        return false
    end
    hdict = Dict()
    for i in 1:length(nums)
        if haskey(hdict, nums[i])
            return [hdict[nums[i]], i]
        else
            hdict[target - nums[i]] = i
        end
    end
end

If nums is always sorted, you can do a binary search to see if the complement to whichever number you're on is also in the slice.

func binary(haystack []int, needle, startsAt int) int {
    pivot := len(haystack) / 2
    switch {
    case haystack[pivot] == needle:
        return pivot + startsAt
    case len(haystack) <= 1:
        return -1
    case needle > haystack[pivot]:
        return binary(haystack[pivot+1:], needle, startsAt+pivot+1)
    case needle < haystack[pivot]:
        return binary(haystack[:pivot], needle, startsAt)
    }
    return -1 // code can never fall off here, but the compiler complains
              // if you don't have any returns out of conditionals.
}

func twoSum(nums []int, target int) (int, int) {
    for i, num := range nums {
        adjusted := target - num
        if j := binary(nums, adjusted, 0); j != -1 {
            return i, j
        }
    }
    return 0, 0
}

playground example

Or you can use sort.SearchInts which implements binary searching.

func twoSum(nums []int, target int) (int, int) {
    for i, num := range nums {
        adjusted := target - num
        if j := sort.SearchInts(nums, adjusted); nums[j] == adjusted {
            // sort.SearchInts returns the index where the searched number
            // would be if it was there. If it's not, then nums[j] != adjusted.
            return i, j
        }
    }
    return 0, 0
}

In my opinion if no elements found adding up to target, best would be to return values which are invalid indices, e.g. -1. Although returning 0, 0 would be enough as a valid index pair can't be 2 equal indices, this is more convenient (because if you forget to check the return values and you attempt to use the invalid indices, you will immediately get a run-time panic, alerting you not to forget checking the validity of the return values). As so, in my solutions I will get rid of that i + 1 shifts as it makes no sense.

Benchmarking of different solutions can be found at the end of the answer.

If sorting allowed:

If the slice is big and not changing, and you have to call this twoSum() function many times, the most efficient solution would be to sort the numbers simply using sort.Ints() in advance:

sort.Ints(nums)

And then you don't have to build a map, you can use binary search implemented in sort.SearchInts():

func twoSumSorted(nums []int, target int) (int, int) {
    for i, v := range nums {
        v2 := target - v
        if j := sort.SearchInts(nums, v2); v2 == nums[j] {
            return i, j
        }
    }
    return -1, -1
}

Note: Note that after sorting, the indices returned will be indices of values in the sorted slice. This may differ from indices in the original (unsorted) slice (which may or may not be a problem). If you do need indices from the original order (original, unsorted slice), you may store sorted and unsorted index mapping so you can get what the original index is. For details see this question:

Get the indices of the array after sorting in golang

If sorting is not allowed:

Here is your solution getting rid of that i + 1 shifts as it makes no sense. Slice and array indices are zero based in all languages. Also utilizing for ... range:

func twoSum(nums []int, target int) (int, int) {
    if len(nums) <= 1 {
        return -1, -1
    }
    m := make(map[int]int)
    for i, v := range nums {
        if j, ok := m[v]; ok {
            return j, i
        } else {
            m[target-v] = i
        }
    }
    return -1, -1
}

If the nums slice is big and the solution is not found fast (meaning the i index grows big) that means a lot of elements will be added to the map. Maps start with small capacity, and they are internally grown if additional space is required to host many elements (key-value pairs). An internal growing requires rehashing and rebuilding with the already added elements. This is "very" expensive.

It does not seem significant but it really is. Since you know the max elements that will end up in the map (worst case is len(nums)), you can create a map with a big-enough capacity to hold all elements for the worst case. The gain will be that no internal growing and rehashing will be required. You can provide the initial capacity as the second argument to make() when creating the map. This speeds up twoSum2() big time if nums is big:

func twoSum2(nums []int, target int) (int, int) {
    if len(nums) <= 1 {
        return -1, -1
    }
    m := make(map[int]int, len(nums))
    for i, v := range nums {
        if j, ok := m[v]; ok {
            return j, i
        } else {
            m[target-v] = i
        }
    }
    return -1, -1
}

Benchmarking

Here's a little benchmarking code to test execution speed of the 3 solutions with the input nums and target you provided. Note that in order to test twoSumSorted(), you first have to sort the nums slice.

Save this into a file named xx_test.go and run it with go test -bench .:

package main

import (
    "sort"
    "testing"
)

func BenchmarkTwoSum(b *testing.B) {
    for i := 0; i < b.N; i++ {
        twoSum(nums, target)
    }
}

func BenchmarkTwoSum2(b *testing.B) {
    for i := 0; i < b.N; i++ {
        twoSum2(nums, target)
    }
}

func BenchmarkTwoSumSorted(b *testing.B) {
    sort.Ints(nums)
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        twoSumSorted(nums, target)
    }
}

Output:

BenchmarkTwoSum-4              1000       1405542 ns/op
BenchmarkTwoSum2-4             2000        722661 ns/op
BenchmarkTwoSumSorted-4    10000000           133 ns/op

As you can see, making a map with big enough capacity speeds up: it runs twice as fast.

And as mentioned, if nums can be sorted in advance, that is ~10,000 times faster!