ARTS 第1周 LeetCode 78 Subsets | Golang 二维 slice 注意事项 | Go 官网文档

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
    }

这看起来没有什么问题,但实际上虽然每次被 appendans 中的新变量是 cmb 的拷贝,但是 cmb 和这个“拷贝”的底层数据大概率是相同的(“大概率”是因为不排除因为扩容导致 cmb底层数据发生变化的可能)。而当后续再重新对 cmb 中的元素进行增减操作时,将会直接影响被追加到 anscmb 的那些”拷贝“们。最终会导致 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 使用中的词典。遇到问题可以用右上角搜索功能先试着找找,某些官方库里方法的使用介绍和小例子也可以轻松找到。

本周的灵光一闪

本周的观点分享是我在观察其他同事的时候突然意识到的。

不知道你是留心观察过你的同事或者同学老师的工作状态,他们中间肯定有一些大家公认的聪明人,所谓“大牛”。他们能力很强,而且总是驾轻就熟,写代码或者看论文总是“微笑浏览”。但是,我发现身边总有另外一些人,他们没有“大牛”们那么强,但他们工作非常认真踏实,都能让人满意地完成任务。我现在越来越觉得后者才是普通人应该学习和追求的榜样,如果到目前你还没有被周围人认为“聪明”,没有关系,你还有机会成为大家认为最“靠谱”的那个人。


发表评论

您的电子邮箱地址不会被公开。