ARTS 第2周 | LeetCode 31 | Go 会支持泛型吗 | Go 中的值方法和指针方法

ARTS

ARTS 是陈浩(网名左耳朵耗子)在极客时间专栏里发起的一个活动,目的是通过分享的方式来坚持学习。

每人每周写一个 ARTS:Algorithm 是一道算法题,Review 是读一篇英文文章,Technique/Tips 是分享一个小技术,Share 是分享一个观点。

本周内容

这一周的 ARTS 你将看到

  1. 一道看似是考排列(permutation)实际上是智力题的 LeetCode 31 题 Next Permutation.
  2. Go 官方如何在保持原来内味儿的基础上尝试提供新的泛型特性。
  3. 当我们在谈论 Go 中方法的 receiver 是值类型还是指针类型时,实际上是在说什么。
  4. 英语可能是大部分人此生唯一一项学习了十几年却从来没有真正使用过的技能。

Algorithm

废话不多说,直接来看题目。

31. Next Permutation

Implement next permutation, which rearranges numbers into the lexicographically next greater permutation of numbers.

If such arrangement is not possible, it must rearrange it as the lowest possible order (ie, sorted in ascending order).

The replacement must be in-place and use only constant extra memory.

Here are some examples. Inputs are in the left-hand column and its corresponding outputs are in the right-hand column.

1,2,3 → 1,3,2
3,2,1 → 1,2,3
1,1,5 → 1,5,1

这题看似题目里提到了排列,很容易就像从排列通常的实现入手,比如回溯的方式。但是题目中要求必须是原地对原数组进行调整,并且要求调整到按照字典序来说的“下一个”值。

做出这道题目,需要两个前提条件。

  1. 对于一个数字来说,其中全部元素的“字典序”其实也就是数组中的数字排列在一起的时候拼成的数字大小。这个结论你可以用几个数字打乱顺序试一下,就会发现结论。
  2. 找到“下一个”符合字典序的排列形式,在有了 1 的结论之后,就可以等效成找到比当前排列的数字大的数字里,“最小”的那一个。

基于上面两个结论之后才能慢慢推导出下面的求解方法。

1. 从后向前查找第一个相邻升序的元素对 (i,j),满足 A[i] < A[j]。此时 [j,end) 必然是降序
2. 在 [j,end) 从后向前查找第一个满足 A[i] < A[k] 的 k。A[i]、A[k] 分别就是上文所说的「小数」、「大数」
3. 将 A[i] 与 A[k] 交换
4. 可以断定这时 [j,end) 必然是降序,逆置 [j,end),使其升序
5. 如果在步骤 1 找不到符合的相邻元素对,说明当前 [begin,end) 为一个降序顺序,则直接跳到步骤 4

这道题目没有用到什么同样的解法,而且题目里面也对解法做了一些限制,这就是我会觉得这道题更像智力题的原因之一。原因之二就是我完全没有想到好的解法,上面的解法来自力扣中国的这个题解

代码我就不贴了。

Review

本周的文章回顾来自 Go 官方成员 Ian Lance Taylor 在 Gophercon 2019 上关 Go 将支持泛型(generic)的演讲稿:Why Generics?

这篇文章主要介绍了 Go 官方在是否为 Go 添加泛型这个特性的思考,已经初步的成果,其中包括一些简单的代码样例。

总的来说,Go 官方已经开始准备提供泛型的特性了,但是比起提供新特性来说演讲者更想表达的是 Go 官方对于泛型的态度:

  1. 最小化新概念,官方不希望因为泛型引入太多的新关键字。
  2. 泛型代码的复杂性是由书写者承担的,不是用户。即希望用户在调用使用了泛型的 package 时尽量少的为泛型花费精力,这部分工作更多的应该是功能提供这来完成。
  3. 生成耗时短,运行速度快。
  4. 保留 Go 的清晰和简单。

千言万语汇成一句话:

generics can bring a significant benefit to the language, but they are only worth doing if Go still feels like Go.

翻译过来就是:“你要的泛型可以有,但是 Go 语言那味儿不能丢”。

不过不得不说,目前还处在草稿阶段的泛型确实还算简单清晰。最后附上原文地址,感兴趣的话可以去看看。

Tip

本周的技术细节是关于 Golang 的 value/pointer receiver 的探讨。

首先看一下这个问题

这是一个很经典的问题,题主的疑惑概括来说就是

  1. Golang 在 receiver 不同(这里指指针和值)但方法名相同时如何判断对象是否实现了接口?
  2. pointer receiver 调用没有显示实现的 value method 时实际上发生了什么?

先贴一些资料:

官网 Effective Go 对于 pointer 还是 value reveiver 的解释 Poniters vs Values.

Why do T and *T have different method sets? 同样来自官网。

官网 FAQ 关于“应该定义值方法还是指针方法”问题的回答 Should I define methods on values or pointers?

其实 receiver 就是 Go 的一种语法糖,功能上和普通的形参非常相似,不同的只是 Go 会帮你对实参取地址或者解引用而已。

至于问题中题主提到的“生成” method 问题,我的理解是 Go 不会为你“自动生成”新的 method.而是在判断是否能作为某种 interface 的 实现 或者 能否作为某个方法的 caller 时,进行一些判定。Go 中的类型针对 pointer receiver 和 value receiver 有不同的方法集(method set), 具体来说就是一般情况下 value receiver 只能调用 velue mathod 而 pointer receiver 可以调用全部的 method(包括 velue mathod 和 value method)。当你通过不同的 receiver 调用指定的方法时候,Go 会为你做一些“适配”。比如使用 可以取地址的 value 类型的 caller 调用 pointer method 时它会帮你取地址,又或者使用 pointer 类型的 caller 调用 value method 时会帮你解引用。

回到题主上面的问题,当运行下面的代码时。

var a Integer = 1
var b LessAdder = &a
b.Add(100)
fmt.Println(a)

b.Add(100) 实际上是帮你解引用 (*b).Add(100),但这样写其实还是会让人产生误解。根据官网的解释,receiver 等价于 Add 的参数,所以干脆这样理解:对于原来的 Add

func (a Integer) Add(b Integer) {
    a += b
}

可以等价成下面的函数。

void Add(a, b Integer) {
    a += b
}

上面参数表里的 a 就是等价的 receiver. 我们继续之前的猜想:b.Add(100) 实际上是帮你解引用 (*b).Add(100).

而真正的调用方式应该是这样的 Add(*b,100),所以对 *b 的副本加 100 并不会改变 a 的值。

另外和这个问题关系非常密切的还有另外一个问题,就是 Go 中对象是否可以寻址的问题。这个问题可以看看这个回答

遗留问题

最后针对这个问题还是有一些不完全明白的地方,比如 Golang 什么情况会判断 value 类型的 caller 是否使用了 pointer method,又是什么情况会判断 value 类型的 caller 能否取地址呢?或者,是否可能同时判断?

Share

本周的灵光一闪。

最近两年发现我对于阅读英文的文档是非常抵触的,只要有翻译就一定回去看翻译,如果实在找不到翻译甚至可能回去找中文世界类似的替代品。长此以往英语阅读能力下降的很厉害,反过来我就更加不想看英文文档。

这段时间在看 GO 官网文档的时候突然再也不想忍受这种状态了,决定硬着头皮不看翻译。几天下来感觉收货还是不少的,毕竟这些都是第一手的资料。阅读英文技术文档的时候,丝毫不会有对内容正确性的质疑(这里指 GO 官网),也不需要边读边判断文档内容的正确性,在度过了最开始的艰难阶段之后反而觉得更加轻松。当然英文世界肯定也有无脑搬运和误导性的结论,但涉及到官方文档或者国外开源项目的时候,英文文档应该是你首选的资料。

阅读 492

推荐阅读
又饿又傻
用户专栏

0 人关注
7 篇文章
专栏主页
目录