ARTS是什么?
Algorithm:每周至少做一个leetcode的算法题;
Review:阅读并点评至少一篇英文技术文章;
Tip:学习至少一个技术技巧;
Share:分享一篇有观点和思考的技术文章。
Algorithm
LC 215. Kth Largest Element in an Array
题目解析:
找出数组中的第 K 大的元素。这是一道非常经典的题目,如果是第一次看到这道题,估计都会试着去想着先排序后遍历。但如果是这样子的话,时间复杂度就会是 O(nlgn)。问题是能不能够更快呢?这里我们需要用到快速排序里的一个思想,就是通过一个基准元素将数组分成两部分,一部分中的元素不大于基准元素,另一部分中的元素不小于基准元素。 然后我们需要判断我们要找的元素在哪一个部分,去到其中的一部分继续递归寻找即可,举个例子:
[3,2,1,5,6,4] and k = 2
这里我们要找数组中第二大的元素,我们选择数组中间的元素来作为基准元素,第一次寻找我们可以把数组划分成下面这样
[2,3,5,6,4],[1]
这样第二大的元素肯定在第二个部分,于是我们需要去第二个部分继续寻找,在第二个部分中我们还是选取中间元素 5 来作为基准元素,这样就继续吧数组拆分成两部分
[5,6],[2,3,4]
然后我们继续在 [5,6] 中寻找我们想要的答案。这道题目有两个比较难理解的地方,第一个地方是时间复杂度的分析,也就是为什么这样做会比 O(nlgn) 还要快? 另外就是双指针实现的部分,这里我们先讲解时间复杂度的分析,至于实现上的细节我会在代码注释中详细说明。
我们先来分析一下快速排序的时间复杂度,快速排序在把数组基于基准元素拆分成两个部分之后。会分别对这两部分递归再去做相同的操作。也就是说每次拆分前都需要进行遍历,遍历的时间复杂度是 O(n),平均拆分了 lgn 次,所以最后的时间复杂度是 O(nlgn)。注意这里我说的是平均复杂度,而不是最差时间复杂度,有可能选取的基准元素没法把数组拆分成元素数量相同的两部分,而导致时间复杂度上升,极端情况下,会造成时间复杂度变为 O(n^2),当然这种概率还是很低的,这里我们暂且不讨论。
我们再来看看和快速排序相比,我们上面的思路有何不同,虽然说和快速排序一样是把数组拆分成两个部分,但是不同的是我们仅仅是选取其中的一个部分再往下递归遍历,这样每次拆分都会让遍历的元素数量减半,于是我们就可以通过下面的式子计算得出最终的时间复杂度:
n + n/2 + n/4 + n/8 + ... + 1 = 2n -> O(n)
这里我们得到的时间复杂度还是平均时间复杂度,我们假设每次都能够让数组均分。不管怎么说,按照这种思路下来,时间上面都是比排序后遍历要来的快。而且这种基于快速选择的方式方法在很多场景下都有应用。
参考代码:
public int findKthLargest(int[] nums, int k) {
if (nums.length < k) {
return -1;
}
return quickSort(nums, k - 1, 0, nums.length - 1);
}
private int quickSort(int[] nums, int k, int start, int end) {
if (start >= end) {
return nums[start];
}
// 选取中间的元素作为基准值
int pivot = nums[(start + end) / 2];
int l = start, r = end;
while (l <= r) {
// 当前元素如果大于基准元素的话,需要去到左边
// 这里保持不变,继续遍历
// 当前元素如果不大于基准元素的话,需要去到右边
// 这里退出循环,我们找到一个需要交换的元素
while (l <= r && nums[l] > pivot) {
l++;
}
// 当前元素如果小于基准元素的话,需要去到右边
// 这里保持不变,继续遍历
// 当前元素如果不小于基准元素的话,需要去到左边
// 这里退出循环,我们找到一个需要交换的元素
while (l <= r && nums[r] < pivot) {
r--;
}
// 交换左右两边找到的符合条件的元素
if (l <= r) {
swap(nums, l++, r--);
}
}
// 到这里,需要判断我们该选择哪一个部分继续遍历
// 我们通过 k 来和 l, r 对比进行判断
if (r >= k) {
return quickSort(nums, k, start, r);
}
if (l <= k) {
return quickSort(nums, k, l, end);
}
return nums[k];
}
private void swap(int[] nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
Review
继续 Pro Git,这一章主要来看 Git 的一些分布式的开发模式。
首先,我们从整体上来看看 Git 的分布式开发具体有几种形式
工作流程
-
集中式工作流
这是最简单的一个流程,有一个公共的仓库,然后每个开发人员将代码 push 到这个公共的仓库。这也是一些小型项目经常用到的一个流程。需要注意的是在 push 代码到公共仓库之前,记得先 merge/rebase 公共仓库更新的 commit。
-
集成管理员工作流
这个开发流程大致是每个开发人员都有一个自己的公共仓库,平时自己在本地开发,然后把 commit push 到自己的公共仓库上去。等一个模块或者项目开发完成后,会通知整个项目的管理员,让其把自己开发的模块从自己的公共仓库 clone 下来,然后 push 到一个整合的代码仓库(最终的代码仓库)。
这个有点类似做 GitHub 开源项目的感觉,fork 别人的项目(最终的代码仓库)到自己的代码仓库,然后自己 clone 自己的代码仓库,在自己的代码仓库开发完成后,提交 pull request 让最终代码仓库的维护者审核你的 commit,最终整合到最终的代码仓库。
-
司令官与副官工作流
这个有点类似 master-slave (主从模式)的感觉。这种工作流主要用在比较复杂的系统中,比如说 Linux 的内核。每个副官(lieutenant)负责一个部分,司令官(dictator)负责整合每个副官的改变,然后开发者根据所开发的部分,将代码上传到对应的副官仓库上。
这种工作流并不常见,但是对于一些特别复杂的项目还是挺有用的。
代码提交
说完了工作流程,再来看看如何更好地提交代码。别小看这个,做好了是方便别人和自己日后查看,这跟写好代码是一个道理。
-
提交指南
-
提交不能有不必要的空格,可以使用
git diff --check查看不必要的空格 -
让每个 commit 在逻辑上都是区分开的,尽量不要让一个 commit 过于庞大。这一点非常重要,有助于后面发现错误快速定位问题,快速回滚。
-
写好 commit message,关于这一点可以看我之前写的 ARTS 的 Review 部分。概括起来就是 “用命令的口吻写简洁的描述,并尊从相应的惯例”。
-
-
一些常用的指令
git log --no-merges mybranch...origin/master上面这个指令用于查看
origin/master上有,但是mybranch上没有的 commits 信息,并且过滤掉 merge commit。这个指令可以用于查看当前分支和主分支之间的差别,或者说是主分支比当前分支多出了哪些 commit。git diff master...mybranch这个指令用于比较当前分支上最近的 commit 和
mybranch分支与master分支的最近的公共节点的差别。和前面一个指令类似,我们用了...符号,这个符号的意思其实是 “找出后者(这里的 mybranch)有的,但是前者(这里的 master)没有的东西”。
Tip
在 shell 里面,我们经常能够看到如下类似的指令:
>$ echo sth > /dev/null 2>&1
第一次看到这东西,肯定会感觉一头雾水。但是熟话说的好,一回生二回熟。这次就来拆解一些这里面每个符号的意义。
首先,echo 就不用多说了,表示往指定文件输入字符,后面接字符或者文件。> 后表示的是接收输入字符的文件,如果不指定,比如 echo sth,那就表示直接在命令行输出 sth。
这里我们用 /dev/null 来作为接收文件,但是其表示的是 空设备文件,也就是不输出任何的信息到命令行终端,也就是不显示任何信息。有些时候,把信息打在终端上其实会消耗程序运行的效率,这种做法有些时候可以提高程序的运行效率。
接着就是 2>&1 了,这里面 1 表示的是 stdout 标准输出,同时也是系统的默认,比如说 > /dev/null 其实就等同于 1 > /dev/null。2 表示的是 stderr 错误输出,& 表示的是等同于的意思。因此整个式子表示的就是,错误输出和标准输出都输出到相同的位置,也就是前面定义的 /dev/null。
Share
好久没写文章了,这次读了《全神贯注的方法》这本书。写写读书笔记吧。