英文:
Golang: Find two number index where the sum of these two numbers equals to target number
问题
问题是:找到两个数字的索引,使得 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} // 数字列表太长了,我将所有数字放在了一个 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))
}
数字列表太长了,我将其放在了一个 gist 中:
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
英文:
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
答案1
得分: 3
在我看来,如果没有找到加起来等于target
的元素,最好返回无效索引的值,例如-1
。尽管返回0, 0
作为有效索引对是足够的,因为两个相等的索引不能构成有效索引对,但这样更方便(因为如果你忘记检查返回值的有效性并尝试使用无效索引,你将立即得到运行时错误,提醒你不要忘记检查返回值的有效性)。因此,在我的解决方案中,我将摆脱那些没有意义的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
}
**注意:**请注意,在排序后,返回的索引将是排序后切片中值的索引。这可能与原始(未排序)切片中的索引不同(这可能是一个问题,也可能不是)。如果确实需要原始顺序(原始、未排序切片)的索引,可以存储排序和未排序的索引映射,以便获取原始索引。有关详细信息,请参阅以下问题:
https://stackoverflow.com/questions/31141202/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
}
基准测试
下面是一个小型基准测试代码,用于测试三个解决方案在提供的nums
和target
输入下的执行速度。请注意,为了测试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倍!
英文:
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:
https://stackoverflow.com/questions/31141202/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
}
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
}
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!
答案2
得分: 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 // 代码永远不会执行到这里,但是编译器会报错
// 如果你在条件语句中没有任何返回语句。
}
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
}
或者你可以使用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 返回搜索的数字在切片中的索引位置,
// 如果不存在,则 nums[j] != adjusted。
return i, j
}
}
return 0, 0
}
<kbd>playground 示例</kbd>
英文:
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
}
<kbd>playground example</kbd>
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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论