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

时间:2016-01-08 02:29:24

标签: performance dictionary go slice

问题是:找到nums[index1] + nums[index2] == target的两个数字的索引。这是我在golang中的尝试(索引从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))
}

nums列表太长了,我把它放到了一个要点: https://gist.github.com/nickleeh/8eedb39e008da8b47864

此代码工作正常,但我发现return 0,0部分很难看,并且运行速度比Julia慢十倍。我想知道是否有任何部分写得很糟糕并影响性能?

修改 Julia的翻译是:

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

2 个答案:

答案 0 :(得分:1)

如果始终对nums进行排序,您可以进行二分查找以查看您所在的数字的补码是否也在切片中。

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

或者您可以使用实现二进制搜索的sort.SearchInts

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
}

答案 1 :(得分:1)

在我看来,如果没有找到加起来target的元素,最好是返回无效索引的值,例如-1。虽然返回0, 0就足够了,因为有效的索引对不能是2个相等的索引,这更方便(因为如果你忘记检查返回值而你试图使用无效索引,你会立即得到运行时恐慌,警告您不要忘记检查返回值的有效性)。因此,在我的解决方案中,我将摆脱i + 1转变,因为它毫无意义。

答案的最后可以找到不同解决方案的基准。

如果允许排序:

如果切片很大且没有变化,并且您必须多次调用此twoSum()函数,那么最有效的解决方案是仅使用sort.Ints()提前对数字进行排序:

sort.Ints(nums)

然后您不必构建地图,您可以使用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
}

注意:请注意,排序后,返回的索引将是排序切片中的值索引。这可能与原始(未排序)切片中的索引不同(这可能是也可能不是问题)。如果确实需要原始顺序的索引(原始,未排序的切片),则可以存储已排序和未排序的索引映射,以便获得原始索引。有关详细信息,请参阅此问题:

Get the indices of the array after sorting in golang

如果不允许排序:

这是你摆脱i + 1转变的解决方案,因为它毫无意义。基于所有语言的切片和数组索引为零。还使用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
}

如果nums切片很大并且找不到快速解决方案(意味着i索引变大),则意味着很多元素将被添加到地图中。地图以小容量开始,如果需要额外的空间来托管许多元素(键值对),它们会在内部生长。内部增长需要利用已经添加的元素进行重新构建和重建。这“非常”昂贵。

它似乎并不重要,但确实如此。由于您知道将在地图中结束的最大元素(最差情况为len(nums)),因此您可以创建一个具有足够容量的地图来保存最坏情况下的所有元素。获得的收益将是不需要内部增长和重组。在创建make()时,您可以将初始容量作为map的第二个参数提供。如果twoSum2()很大,这会加快nums的时间:

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
}

基准

这是一个基准测试代码,用于测试您提供的输入numstarget的3个解决方案的执行速度。请注意,为了测试twoSumSorted(),您首先必须对nums切片进行排序。

将其保存到名为xx_test.go的文件中,并使用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)
    }
}

输出:

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

正如您所看到的,制作容量足够大的地图可以加快速度:它可以快速运行两次

如上所述,如果nums可以提前排序,那么〜10,000 快一倍!