ARTS是什么?
Algorithm:每周至少做一个leetcode的算法题;
Review:阅读并点评至少一篇英文技术文章;
Tip:学习至少一个技术技巧;
Share:分享一篇有观点和思考的技术文章。
Algorithm
题目解析:
有一堆人,他们中间会相互讨厌彼此,把这堆人分成两组,相互讨厌的人不能被分在一组,问你是否可分。这道题目的输入条件是人数和一个数组表示的相互讨厌的人。
一开始的时候没想太多,就直接弄了两个集合表示两个组,然后遍历讨厌关系数组依次往两个集合里面放人,如果有冲突就 return false,如果能放完,就 return true。这么简单粗暴的想法肯定是有问题的,问题在于这么做缺乏全局的考虑,比如下面这个例子:
[[1,2],[3,4],[5,6],[1,6],[3,6]]
上面的例子是可以将人分成 1,3,5 和 2,4,6 这两组,但是当你遍历完 [1,2] 后,你遍历到 [3,4] 的时候,你并不知道到底 3 是要放在 1 那一组还是 2 那一组。
我们需要进行更为全面的遍历,你可以把这个想象成图的问题,图上的每个节点代表的是每个人,相邻的两个人相互讨厌。然后,我们这时有两种颜色,我们需要把所有的节点都标上颜色,相邻的两个节点不能标成同样的颜色,否则,算作失败,当把所有节点都标好颜色后,表示可以分成两组。我们可以用深度优先搜索来实现,当然广度优先搜索也是可以的。
这道题目也可以用并查集来做,思考也非常直接,当我们发现两个人相互讨厌对方,那就去和除自己外,对方讨厌的人做集合合并。比如例子:
[[1,2],[1,4]]
我们还是遍历上面这个数组,遍历到 [1,2] 时,1,2 自成一个集合,并且标记 1 讨厌的人是 2,2 讨厌的人是 1。然后,遍历到 [1,4] 时,4 会去和 1 讨厌的人,也就是 2 所在的集合合并。因为只有两个集合,不在这个集合就会在那个集合,不能和讨厌的人在同一个集合。
时间复杂度方面,如果使用搜索,那么要遍历到图上的节点和边,时间复杂度就是 O(N + M) 这里的 N 表示的是人数,M 表示的是 dislikes 数组的长度。使用并查集的话,因为我们用到了路径压缩,每次的 find 还有 union 操作可以看作是 O(1) 的时间复杂度,整体时间复杂度就会是 O(N)。
空间复杂度和时间一样。说到这里,你可能会说并查集更优,但其实这么简单的认为是不准确的,这里你并不知道图上边的个数,也就是输入参数是不确定的,并查集中的 O(1) 只能算是平均时间复杂度,而且我们忽略了整体时间中的常数项。因此,哪种方法更优还要结合实际情况来考虑。
参考代码(DFS):
func possibleBipartition(N int, dislikes [][]int) bool {
group := map[int][]int{}
for i := 1; i <= N; i++ {
group[i] = []int{}
}
// group 中存放每个人所讨厌的人
for i := 0; i < len(dislikes); i++ {
group[dislikes[i][0]] = append(group[dislikes[i][0]], dislikes[i][1])
group[dislikes[i][1]] = append(group[dislikes[i][1]], dislikes[i][0])
}
// colors 数组存放每个人(节点)被标记的颜色
// 0 表示还未被标记
// -1,1 表示两种颜色
colors := make([]int, N + 1)
// 遍历所有人(节点)
for i := 1; i <= N; i++ {
// 如果该节点还未被标记,那么就去标记
// 如果标记不成功,返回 false
if colors[i] == 0 && !dfs(colors, group, i, 1) {
return false
}
}
return true
}
func dfs(colors []int, group map[int][]int, index int, color int) bool {
// 给当前节点做标记
colors[index] = color
// 遍历相邻节点(讨厌的人)
for _, v := range group[index] {
// 如果发现讨厌的人被标记同样的颜色
// 表示无法标记,return false
if colors[v] == color {
return false
}
// 相邻节点未被标记
// 递归进行标记,颜色和当前节点必须不一样
if colors[v] == 0 && !dfs(colors, group, v, -color) {
return false
}
}
return true
}
参考代码(Union-Find):
func possibleBipartition(N int, dislikes [][]int) bool {
// 集合数组
root := make([]int, N + 1)
// 用于记录讨厌的人
hater := make([]int, N + 1)
// 初始化,所有人一开始自成集合
for i := 0; i <= N; i++ {
root[i] = i
}
for i := 0; i < len(dislikes); i++ {
e0, e1 := dislikes[i][0], dislikes[i][1]
// 分别找到这两个相互讨厌的人所在的集合
r0, r1 := find(root, e0), find(root, e1)
// 如果在同一个集合,直接返回 false
if r0 == r1 {
return false
}
// 记录讨厌的人
if hater[e0] == 0 {
hater[e0] = e1
}
if hater[e1] == 0 {
hater[e1] = e0
}
// 和彼此相互讨厌的人所在的集合合并
union(root, hater[e0], e1)
union(root, hater[e1], e0)
}
return true
}
func find(root []int, ele int) int {
if root[ele] == ele {
return ele
}
root[ele] = find(root, root[ele])
return root[ele]
}
func union(root []int, ele1 int, ele2 int) {
root1 := find(root, ele1)
root2 := find(root, ele2)
if root1 != root2 {
root[root1] = root2
}
}
Review
这篇文章摘录自美国一家叫做 Palantir 的公司的技术博客。主要是讲述了代码评审的一些注意事项,以及我们为什么要做代码评审,其背后有那些意义。
-
我们为什么要做代码评审,其背后的价值是什么?
- 首先提交代码的人可以从代码评审中获知一些有价值的信息,比如代码的风格要求,自己的代码是否存在 bugs,通过交流让自己对自己写的东西更自信,同时也可以快速知道自己的问题所在
- 代码评审提供了一个很好的交流技术认知与观点的渠道,通过这样的交流,代码提交者和评审员都可以相互了解彼此的不足,相互鼓励。如果代码评审做得好可以有效凝结团队的凝聚力
- 代码评审可以让代码更加的一致,清晰易懂,让代码更易维护
-
代码评审具体评审什么?
这个主要看团队,没有具体的规定。有的团队只需要保证基本的功能 work,有些团队则需要审查代码的方方面面。可以看到的是,这里有一个效率和代码质量的 trade-off,也就是说你代码评审做的越仔细,肯定能发现更多的问题,但是需要花更多的时间,开发效率会受影响。
另外就是,代码评审没有所谓的 “阶级性”,不管是普通的软件工程师写出来的代码,还是经验非常丰富的资深软件工程师写出来的代码,都需要进行代码评审。就算是你的代码中不存在任何的错误,代码评审也可以给别人提供很好的参考和学习的机会。代码评审的目的并不一定是要找出程序中的 bugs,其背后还有很多潜在的价值,知道这一点非常重要。
-
何时做代码评审?
代码评审一般是在各种测试以及代码风格检测之后,在代码提交到 main branch 之前。这样做可以确保评审只需要针对特定的业务逻辑,可以节省评审员的时间。另外就是,如果你的一次提交的修改量过大,比如说修改超过五个文件,那么就需要将一次提交拆解成多个小的提交,这样可以保证每个提交的主题比较清晰,评审会更有针对性,效果也更好。
每一个提交最好只有一个目的,比如说你这个提交是为了解决之前的一个 bug,那么这个提交之中就不要出现比如说代码重构等内容,否则的话,很可能会造成混乱,因为这两者从结果上看就存在本质的区别,如果是解决了一个 bug,那么需要通过具体的测试,或者有具体的结果证伪;而如果是重构代码,那么就不应该期待有功能上或者是运行结果上的改变。
-
谁来做评审员?
一般来说,其中一个评审员会是较为资深的软件工程师或是团队的领导。评审员数量尽量不要超过三个,因为每个人都或多或少有自己的观点和偏好,有些时候这么做可以,那么做也行,过多的观点有时并不是一个好事,很可能会影响代码评审的效率。
-
代码评审中的一些注意事项
应该从下面的几个方面来思考:
-
目的:这次代码提交的目的是什么,这次提交达到目的了吗?对于代码的变动,不懂的地方可以提出对应的问题,比如说为什么要改这个类,要改那个函数,当解释不是特别清楚的时候,可能意味着要思考是否需要重写,或是加上对应的测试和注释
-
具体实现:可以思考自己之前是否也做过类似的改变?当时是怎么做的,遇到了哪些问题,这些问题对这次提交有没有借鉴意义?另外,思考代码的实现中是否存在冗余,还有就是有没有新引入外部依赖。最后再大致看看代码有没有准守基本的代码标准
-
代码可读性:作为评审员,可以问问自己读过代码后的感受,找出代码中让自己模糊不清的地方,比如变量命名不清,函数没有拆分等等。另外就是,如果代码中有 TODOs 时,需要提醒提交员记录,比如将这个 TODO 放到任务清单中,或是提交 GitHub 的 issue,不然的话,很容易遗忘。
-
可维护性:代码必须要有测试,并查看对应的测试覆盖率等基本信息。如果项目中有 README 文件,必须考虑到该文件是否要做更新。评审员需要留下整体的回复,让代码提交者清楚地知道自己的建议或者认可。
-
-
评论和回复
代码评审中的评论要尽量的简洁、友好并且态度积极,避免带有强制性的口吻,我们需要知道建议和命令的区别,比如下面的语句就是建议
Is this really the correct behavior? If so, please add a comment explaining the logic.
而下面这句话就是命令,要尽量避免:
Add @Override
在一次的评审过后,记得加上自己的期待,比如
Feel free to merge after responding to the few minor suggestions
Please consider my suggestions and let me know when I can take another look.
对于代码提交者来说,一定要重视别人的评论,每个评论都要写上自己的最后的决定和做这个决定的理由,即使是简单的 “ACK” 或者是 “done” 也不例外,这是对别人的一个基本的尊重,不管怎样,别人花了时间来帮你,要让别人看出你积极诚恳的态度。另外,如果不能够和评审员保持一样的观点,记得一定要通过线下或者其他的渠道进行交流。
总之,就像文中说的那样,代码评审这个东西,更像是一门艺术而不是科学,去学习它的唯一途径就是去做,去实践,去踩坑,才会发现自己变得越来越有经验,但是前提是我们需要有看重代码评审的意识,以及认真学习的态度。
Tip
上周在学习 Andrew Ng 公开课的时候,了解了 Octave 这个图形数据化的工具。之前用过 Matlab,这个是 Matlab 的开源版本,安装好后直接在命令行就可以运行,感觉非常的方便,这里做下简单的记录
如果是在 Mac 上安装 Octave,非常的简单,只需要借助 Homebrew,确认 Homebrew 更新后,一行命令就可以搞定:
>$ brew install octave
相关命令:
-
调节命令行前面的引导字符
>$ PS1('>>') >> -
定义向量和矩阵
% 用分号表示行结束 % 向量一般默认是 m x 1 的大小,区别于数组的 1 x m v = [1;2;3] % vector arr = [1 2 3] % array matrix = [1 2; 3 4; 5 6] % 3 x 2 matrix a = 1:0.1:2 % 生成一个等间隔数组 -
一些内置的矩阵相关的函数
>> one(2, 3) ans = 1 1 1 1 1 1 >> zeros(2, 3) ans = 0 0 0 0 0 0 >> eye(3) ans = Diagonal Matrix 1 0 0 0 1 0 0 0 1 -
加载并保存数据数据文件
>> load {filename} % 加载 filename 文件中的数据 >> save {filename} {variableName} % 把一个变量表示数据导入到对应的文件中 -
矩阵的常见操作
>> A(2,:) % : 表示取整行或者是整列,这个例子中取的是第二行 >> A([1 3], :) % 取第一行和第三行 >> A(A[1;2;3;4]) % 按列拼接 >> A = A(:) % 把矩阵转换成向量 -
矩阵的常见计算
>> A' % 矩阵的转置 >> pinv(A) % 求逆矩阵,inv(A) 也是求逆矩阵,不同之处在于 pinv 会一直有解,即使矩阵不可逆 >> A .* B % . 后加运算符表示的是矩阵的算术计算,这里仅仅矩阵对应位置的元素相乘,区别于矩阵计算 -
一些常用的内置函数
- sum
- prod
- find
- floor & ceil
- max
-
一些常见的图形命令,这里就不一一解释,对应查文档即可
- plot
- hold on
- xlabel & ylabel
- legend
- title
- close
- figure
- subplot
- clf
- imagesc
- colorbar
- colormap
Share
分享皓叔的一篇关于 makefile 的 文章
makefile 可以说是相当常见且基础的东西了,但由于自己的专业不是计算机相关,对这个名词还是相当地陌生。可能 makefile 在当下并没有这么经常被提及,我想原因可能是现代的 IDE 帮你做了很多事情,比如 VS Code,要做什么直接安装一个插件就好了,没必要费事费时地去写 makefile,写不好还容易出错。
那之所以想学学如何写 makefile 的原因在于,makefile 实在是太常见了,在项目中经常看到,似乎成为了行业中的一种常见的规范。另外就是 makefile 可以让你离计算机底层更近一点,让你更加了解一些语言的编译流程和环境配置,总之,能够让你思考得更深。
这篇文章是这个系列的第一篇,主要就是两点,一是程序的编译和链接,二是 makefile 的基本规则,做个学习记录:
-
程序的编译和链接:
比如 Java,C,C++ 这样的语言,在运行之前都需要有一个编译的步骤,这个步骤主要是编译器帮助检测程序的语法是否正确,编译过后会生成一个中间文件,比如 C 文件经过编译后在 UNIX 系统下生成 .o 文件,表示的是对象文件(object file)。
但是编译过后,还需要链接函数和全局变量,我们需要使用 .o 文件来链接程序。如果源文件过多,会生成很多的 .o 文件,在链接时需要明显指出中间目标文件名,为了方便起见,我们需要 makefile 来定义这一层的依赖关系。链接这个过程不会考虑源文件,只会在 .o 文件中寻找。
-
makefile 的基本规则:
大致规则如下:
target ... : prerequisites ... command ...target 表示的是目标文件,prerequisites 表示的是要生成 target 所需要的文件,command 是 make 需要执行的命令。
makefile 说白了就是表示的是文件的依赖关系,换句话说就是 target 依赖于 prerequisites,其生成的规则在 command 中,如果 prerequisites 中有一个以上的文件有更新的话,command 就会被执行。