Arts 第七十五周(10/19 ~10/25)

150 阅读8分钟

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 CH5

继续 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

好久没写文章了,这次读了《全神贯注的方法》这本书。写写读书笔记吧。

全神贯注,活在当下