ARTS
ARTS 是陈浩(网名左耳朵耗子)在极客时间专栏里发起的一个活动,目的是通过分享的方式来坚持学习。
每周写一个 ARTS:Algorithm 是一道算法题,Review 是读一篇英文文章,Technique/Tips 是分享一个小技术,Share 是分享一个观点。
本周内容
我又开始更新啦!!! 四个月之前我也没想过会停更这么长时间, 转眼已经是 2021. 跟天性懒惰斗争了一百多天之后我还是取得了胜利, 这就叫 "人定胜天" !
所以本周的 ARTS 你讲看到:
- LeetCode 历史上第一道 Hard.
- 如何理解 CAP 理论中的 P.
- 什么? 你还没用过 go module?
- 可以面向工具编程但不能放弃思考.
Algorithm
本周的算法题可以算得上是二分查找类型题的天花板: LeetCode 4 寻找两个正序数组的中位数.
初看这题, 很容易想到的粗暴方法就是边排序边合并两个数组, 时间复杂度 O(N), 空间复杂度 O(1). 但题目要求是使用 O(logN) 级别的时间复杂度解题.
看到对数时间复杂度首先想到的肯定是树形递归或者二分查找, 不妨先尝试用二分的思路分析一下.
二分的通常用法就是每次排除掉一半的数据, 这样只需要进行对数级别的查找就能找到目标. 但这题最难得地方在于, 二分法的使用场景不太直接, 需要绕个弯才能想到.
首先, 求长度是 len 的有序数组的中位数, 等价于求第 len/2 小的数字(当 len 是奇数)或者求第 len/2 小和第 len/2+1 小数字的平均值. 问题就转换成了求有序数字中第 k 小额数字.
其次, 就是如何每次查找时排除一半的数字. 根据上面的等效理解方式, 我们需要求两个数组合并之后第 k 小的数字(len 为奇数是只求一次第 k 小, len 为偶数时需要求两次即第 k 小和第 k+1 小). 假设输入数组分别是 [1, 3, 4, 9] 和 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 即 len = 14, 我们需要求第 7 小和第 8 小的数字然后求平均值. 先在每个数组中各取第 7/2 = 3 个数字, 然后比较两者的大小, 即比较 4 和 3 的大小. 因为第二个数组中第 3 个数字 3 小于第一个数组中的第三个数组 4, 所以第二个数组中的前三个数字肯定不包含两数组合并后第 7 小的数字(因为全部数字中比 3 小的数字最多只有 5 个具体证明可以看这个题解). 这样就可以将 k 缩小为 k/2, 即实现了二分查找.
最后, 如果查找到两个数组分别都只能取一个数字的时候, 需要对 len 的奇偶分别做处理.
下面是代码实现.
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
m, n := len(nums1), len(nums2)
// 下面的 left 和 right 值是为了兼容两个数组长度和奇偶的情况
// m+n 是奇数时 left 和 right 相同, 代表中位数
// m+n 是偶数时 left 和 right 不同, 代表中间两个数字
// ps: left 和 right 代表的是第 k 小的数, 因此从 1 开始而非从 0 开始
left, right := (m+n+1)/2, (m+n+2)/2
if left == right {
return float64(getKthLeast(nums1, nums2, 0, m-1, 0, n-1, left))
}
return float64(getKthLeast(nums1, nums2, 0, m-1, 0, n-1, left) + getKthLeast(nums1, nums2, 0, m-1, 0, n-1, right))*0.5
}
func getKthLeast(nums1, nums2 []int, start1, end1, start2, end2, k int) int {
// 递归到目前为止, 剩下的两个数组长度
len1, len2 := end1-start1+1, end2-start2+1
// 为了简化后续判断逻辑, 调整顺序让 nums1 始终更短
if len1 > len2 {
return getKthLeast(nums2, nums1, start2, end2, start1, end1, k)
}
if len1 == 0 {
return nums2[start2+k-1]
}
if k == 1 {
return min(nums1[start1], nums2[start2])
}
// 求出中间位置, 即上述题解描述的两个数组各自的第 k/2 数字
i, j := start1+min(len1, k/2)-1, start2+min(len2, k/2)-1
if nums1[i] > nums2[j] {
return getKthLeast(nums1, nums2, start1, end1, j+1, end2, k-min(len2, k/2))
}
return getKthLeast(nums1, nums2, i+1, end1, start2, end2, k-min(len1, k/2))
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
Review 文章推荐
本周的文章是 CAP Confusion: Problems with ‘partition tolerance’.
首先这是一篇比较古老的文章, 可能你早就知道了这其中的原理.
CAP 的原理很容易理解和记忆, 无非就是只能三选二, 且 P 作为分区容忍度必须不能放弃. 所以常见的系统设计都是在 C 即一致性, 以及 A 即可用性之间权衡.
但这篇文章主要关注的点是 P 到底指什么, 显然它并不是指因为分区出现问题导致系统分裂为两个或者多个分区. 而是, 指服务底层依赖的网络服务, 以及使用网络服务的设备无法(软件或者硬件)保证绝对的可靠. 正因为这些无法避免的原因导致我们必须选择在服务层面保证 P, 而由于三选二的理论要求, 我们又只能在 A 和 C 之间选择一个. 当然这其中会有很多折中的方案, 比如提供更可靠的服务, 但是只保证最终一致性.
Tip 编程技巧
这周的 coding 完全没有技巧...
Share 灵光一闪
CICD 越来越成熟, 工具越来越多也越来越好用, 但不能仅仅停留在会用工具的程度.
长期使用某个工具会慢慢对它失去好奇心, 甚至连稍微了解一下背后的实现方式都懒得去做. 沉迷在完成每一个新功能里, 但实际上是在不停地复制自己. 这一点还是需要经常提醒自己的, 要保持好奇心, 保持饥饿的状态.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。