ARTS
ARTS 是陈浩(网名”左耳朵耗子“)在极客时间专栏里发起的一个活动,目的是通过分享的方式来坚持学习。
每人每周写一个 ARTS:Algorithm 是一道算法题,Review 是读一篇英文文章,Technique/Tips 是分享一个小技术,Share 是分享一个观点。
算法
LeetCode 78 Subsets & 90 Subsets II
先看一下题目要求。
78.Subsets
Given a set of **distinct** integers, _nums_, return all possible subsets (the power set).
Note: The solution set must not contain duplicate subsets.
Example:
Input: nums = [1,2,3]
Output:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
这两题其实都是非常经典的排列组合问题的变种,解题的方式也比较固定,就是是使用回溯。具体在细分的话,常见的就是使用循环+固定长度的组合数,或者是不限定每次回溯的子集长度。前者的每个分支都可以单独用来求特定长度的组合,后者代码看起来更加简洁。为了减少记忆量,上面两个题目我都是使用前者,也就是循环+固定长度组合数的方式来解题。
下面是 78 题的参考代码,原题给出的函数作为入口,其中每次循环都是用来求特定长度的子集。
func subsets(nums []int) [][]int {
sz := len(nums)
ans := &[][]int{}
// cmb := &[]int{}
cmb := make([]int, 0, sz)
for i := 0; i <= sz; i++ {
bt(i, 0, cmb, ans, nums)
}
return *ans
}
func bt(l, s int, cmb []int, ans *[][]int, nums []int) {
sz := len(nums)
if len(cmb) == l {
tmp := make([]int, len(cmb), len(cmb))
copy(tmp, cmb)
*ans = append(*ans, tmp)
return
}
for i := s; i < sz; i++ {
cmb = append(cmb, nums[i])
bt(l, i+1, cmb, ans, nums)
cmb = cmb[:len(cmb)-1]
}
}
代码本身并不复杂,但你可能注意到了 bt
函数发现 len(cmb) == l
调价满足的时候对 cmb
进行了一次拷贝的操作。目前我能想到和查到的办法来看,这个拷贝的操作是必需的,至于原因留到后面再说。
90.Subsets
Given a collection of integers that might contain duplicates, **_nums_**, return all possible subsets (the power set).
Note: The solution set must not contain duplicate subsets.
Example:
Input: [1,2,2]
Output:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]
然后是 90 题。这个可以看做是在 78 题的简单变种,加了一点难度,要求得到的结果包含不重复子集。最容易想到的办法就是直接使用 78 题的代码,然后对得到的结果去重。但是,最简单的去重可能就是使用 map 了,如果你真的想这么做的话,肯定也会遇到和我一样的问题,那就是 slice 之间无法比较,因此无法作为 map 的 key. 所以这条路看起来就走不通了。当然如果执意给二维 slice 去重的话,也是可以做到的,只是我能想到的方法非常的不性感,这个也放到后面来讲。
我们继续回到问题本身,能不能在 78 题的 bt
回溯的过程中把去重的逻辑加进去呢?
78 题现有的回溯逻辑,可以很容易的对拥有相同前缀的子集进行判重。所以对原本 bt
函数中循环的位置:
for i := s; i < sz; i++ {
cmb = append(cmb, nums[i])
bt(l, i+1, cmb, ans, nums)
cmb = cmb[:len(cmb)-1]
}
进行这样的修改就可以直接过滤掉有重复数字的子集了。
for i := s; i < sz; i++ {
if i > s && nums[i] == nums[i-1] {
continue
}
*cmb = append(*cmb, nums[i])
bt(nums, l, i+1, cmb, ans)
*cmb = (*cmb)[:len(*cmb)-1]
}
欣欣然提交代码之后会发现,只通过了10个测试例。仔细一看原来输入 nums
中重复数字并不一定相邻,这就会导致上述代码会遗漏一部分重复的子集。不难想到,先对输入数据进行预处理,把重复数字放到相邻的位置。而排序是最简单的实现方式,这里直接用了 sort.Slice()
.解法如下。
import(
"sort"
)
func subsetsWithDup(nums []int) [][]int {
ans := make([][]int, 0)
cmb := make([]int, 0)
sz := len(nums)
// 排序是为了把相同的数字放到一起,方便后面判重
sort.Slice(nums, func(i, j int) bool {
return nums[i] < nums[j]
})
for i := 0; i <= sz; i++ {
bt(nums, i, 0, &cmb, &ans)
}
return ans
}
func bt(nums []int, l, s int, cmb *[]int, ans *[][]int) {
sz := len(nums)
if l == len(*cmb) {
tmp := make([]int, len(*cmb), len(*cmb))
copy(tmp, *cmb)
*ans = append(*ans, tmp)
return
}
for i := s; i < sz; i++ {
if i > s && nums[i] == nums[i-1] {
continue
}
*cmb = append(*cmb, nums[i])
bt(nums, l, i+1, cmb, ans)
*cmb = (*cmb)[:len(*cmb)-1]
}
}
后面回顾一下上面解题过程中遇到的两个问题。
两个 Golang 中 slice 的技巧
还记的上面解题过过程中的两个 Golang 相关的问题吧,本周的 Tip 说说这两个问题。
第一个问题是,为什么 78 题的代码中必须对 cmb
进行 copy
操作。这其实是一个对二维 slice 进行追加操作时的注意点,就是大家平时喜欢说的“坑”。
if len(cmb) == l {
tmp := make([]int, len(cmb), len(cmb))
copy(tmp, cmb)
*ans = append(*ans, tmp)
return
}
第二个问题就是二维数组的去重问题,或者更准确的说,应该是 Golang 中 slice 类型的比较(判断是否相等)的问题。
二维 slcie 追加时可能会遇到的一个”坑“
先来看第一个问题。
我们都知道,Golang 中一般会把 maps, slices 和 channels 都叫做“引用类型”。原因就是这三种类型都是对各自的自层数据进行了封装,传参或者赋值时虽然传递的是上述类型对应的变量的值,但是接收方可能使用的底层数据和原值使用的底层数据是相同的。
具体到 slice 这种类型来说,如果上面的这段代码不对 cmb
进行拷贝的话,代码可能是这样写的:
if len(cmbreflect.DeepEqual(ans[i], ans[j])) == l {
*ans = append(*ans, cmb)
return
}
这看起来没有什么问题,但实际上虽然每次被 append
到 ans
中的新变量是 cmb
的拷贝,但是 cmb
和这个“拷贝”的底层数据大概率是相同的(“大概率”是因为不排除因为扩容导致 cmb
底层数据发生变化的可能)。而当后续再重新对 cmb
中的元素进行增减操作时,将会直接影响被追加到 ans
的 cmb
的那些”拷贝“们。最终会导致 ans
里最终出现大量重复的 slice.
但是尴尬的找了一圈之后我并没有找到更好的办法,只能使用 copy
函数。如果你有更好的方式的话,麻烦在下面留言告诉我!
Golang 中的 slice 是否可以比较
遇到这个问题之前,我也知道 slice 不能作为 map 的 key 来使用。具体的原因可以看这里。概括来说,这是一个 Golang 的设计哲学问题,官方在将 slice 设定为一个“引用类型之后
”, 在 slice 是否相同这个问题上不可避免的产生了歧义:
there are multiple considerations involving shallow vs. deep comparison, pointer vs. value comparison, how to deal with recursive types, and so on.
到底比较引用对象还是底层数据,指针还是值,这些确实都是问题。所以官方决定暂时不处理这个问题。
之前也确实没有真的想这么用的时候,直到这次做到 90 这道题。虽然最后也没有必要对 [][]int
这种类型进行去重的操作了,但我还是尝试找了一个 slice 执行比较的操作方式,万不得已的时候也可以试试。具体的实现如下。
import "reflect"
reflect.DeepEqual(ans[i], ans[j])
DeepEqual
对 slice 进行的“深度”比较将会判断两个 slice 的底层数组地址是否相同,或者两个 slice 的每一个元素都是否相同。当然这个方法不仅能比较 slice,具体可以看官方文档的介绍。
Go 官方文档中的“高频问答”
本周的 Review 就来说说官网的文档吧。其实官网文档有好几个部分:Document 主要是一些基本的概念相关的内容,包括入门练习 A Tour of Go,全面介绍语言特性和编程规范的 Effective Go 以及稍微深入一些的 References部分;另外还有一个 Blog主要介绍一些 Golang 相关的最新消息。
其实上面 Tip 中两个问题都是通过阅读官方文档来解决的。官方文档是初学 Golang 最好的工具,同时也是 Golang 使用中的词典。遇到问题可以用右上角搜索功能先试着找找,某些官方库里方法的使用介绍和小例子也可以轻松找到。
本周的灵光一闪
本周的观点分享是我在观察其他同事的时候突然意识到的。
不知道你是留心观察过你的同事或者同学老师的工作状态,他们中间肯定有一些大家公认的聪明人,所谓“大牛”。他们能力很强,而且总是驾轻就熟,写代码或者看论文总是“微笑浏览”。但是,我发现身边总有另外一些人,他们没有“大牛”们那么强,但他们工作非常认真踏实,都能让人满意地完成任务。我现在越来越觉得后者才是普通人应该学习和追求的榜样,如果到目前你还没有被周围人认为“聪明”,没有关系,你还有机会成为大家认为最“靠谱”的那个人。
有疑问加站长微信联系(非本文作者)