【问题标题】:Golang: Find two number index where the sum of these two numbers equals to target numberGolang:找到两个数字索引,这两个数字的总和等于目标数字
【发布时间】:2016-04-12 15:12:27
【问题描述】:

问题是:找到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 翻译慢十倍。我想知道有没有写得很糟糕,影响性能的部分?

编辑: 朱莉娅的翻译:

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

【问题讨论】:

  • 你的列表总是排序的吗?如果是的话,有更好的算法而不需要使用地图。 i+1i &lt; len(nums) 看起来也不对。您将获得越界访问权限。
  • 请出示您要比较的 Julia 翻译。
  • 另外你如何测量这两个程序的运行时间?
  • @kostya 谢谢!不,列表未排序。我会再考虑i+1 部分。
  • @CodingPickle Julia 的翻译已添加。 (nums太长了,这里贴不上去。我放到gist链接里了。)

标签: performance dictionary go slice


【解决方案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
}

【讨论】:

    【解决方案2】:

    在我看来,如果没有找到添加到 target 的元素,最好返回无效索引的值,例如-1。尽管返回 0, 0 就足够了,因为有效的索引对不能是 2 个相等的索引,但这更方便(因为如果您忘记检查返回值并尝试使用无效的索引,您将立即运行-time panic,提醒你不要忘记检查返回值的有效性)。因此,在我的解决方案中,我将摆脱 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
            } 
            m[target-v] = i
        }
        return -1, -1
    }
    

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

    这似乎并不重要,但确实如此。由于您知道最终将出现在地图中的最大元素(最坏情况是 len(nums)),因此您可以创建一个容量足够大的地图,以容纳最坏情况下的所有元素。收益将是不需要内部增长和重新散列。您可以在创建map 时提供初始容量作为make() 的第二个参数。如果nums 很大,这会大大加快twoSum2() 的速度:

    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
            }
            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 倍!

    【讨论】:

    • @EmilVikström 函数返回索引-1 是一个有效的整数,但它是一个无效的 index
    • 哇,令人印象深刻的答案! ;) 我怀疑使用 2 * len(nums) 作为初始地图容量可能会使 TwoSum2 稍微快一些。
    • @kostya 不,对它进行了测试,结果变得更糟(与使用 len(nums) 初始容量相比慢了 20%)。
    • @icza,感谢您的检查。这只是证实了关于性能的推理非常困难;)
    • @kostya 不一定。 len(nums) 将避免重新散列。但是使用更大的初始容量会导致更多的内存使用,并且可能更难维护(分配的区域越大,可能更难找到足够的连续空间)。由于即使使用len(nums)(仅在最坏的情况下),地图也没有“完全”填充,因此您不会体验到更大容量的收益。
    猜你喜欢
    • 1970-01-01
    • 2022-12-18
    • 2018-07-31
    • 1970-01-01
    • 2015-07-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多