Golang:找到两个数的索引,使得这两个数的和等于目标数。

huangapple go评论87阅读模式
英文:

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 (
	&quot;fmt&quot;
)

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) &lt;= 1 {
		return 0, 0
	}
	hdict := make(map[int]int)
	for i := 1; i &lt; 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) &lt;= 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
}

基准测试

下面是一个小型基准测试代码,用于测试三个解决方案在提供的numstarget输入下的执行速度。请注意,为了测试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) &lt;= 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) &lt;= 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 (
	&quot;sort&quot;
	&quot;testing&quot;
)

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

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

func BenchmarkTwoSumSorted(b *testing.B) {
	sort.Ints(nums)
	b.ResetTimer()
	for i := 0; i &lt; 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) &lt;= 1:
		return -1
	case needle &gt; haystack[pivot]:
		return binary(haystack[pivot+1:], needle, startsAt+pivot+1)
	case needle &lt; haystack[pivot]:
		return binary(haystack[:pivot], needle, startsAt)
	}
	return -1 // code can never fall off here, but the compiler complains
              // if you don&#39;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&#39;s not, then nums[j] != adjusted.
            return i, j
        }
    }
    return 0, 0
}

huangapple
  • 本文由 发表于 2016年1月8日 10:29:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/34668537.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定