开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情
今天继续Codility的Lessons部分的第6个主题——Sorting。
还是按先总结红色部分PDF,然后再完成蓝色的Tasks部分的节奏来~
PDF Reading Material
- 前言: 介绍Sorting的概念
- 6.1-Selection Sort: 总体复杂度O(n^2)
- 6.2-Counting Sort: 总体复杂度复杂度O(n+k)
- 6.3-Merge Sort: 总体复杂度O(nlogn)
- 6.4-Sorting Functions: Python中的list.sort()方法的复杂度是O(nlogn)
- 6.5-Exercise: 输入是一个包含n个整数的数组A,返回的是A中unique整数的数量。PDF中给的解题思路,是对A先做排序,然后再从左到右顺序比较相邻元素是否一致,最终就可以得到unique整数的数量。使用的是Python默认的sort()函数,时间复杂度为O(nlogn)。
排序部分是算法里很常见的问题了,所以上述的选择排序、计数排序、归并排序三部分的具体排序方法我就没有总结了。
Tasks
- Distinct:
本题就是上一部分——PDF部分的Exercise题。PDF中给的解法,是用Python的sort函数排序后,再从左到右顺序扫一遍已经排序的列表,得到distinct value的数量。因为题目要求Efficiency,我直接按照计数排序的思路做的,专门创建了一个dict来做计数,当然这样会比较耗内容:
def solution(A):
# 计数排序,但是不排序,直接返回计数项的数量
count_dict = {}
for a in A:
count_dict[a] = count_dict.get(a,0)+1
return len(count_dict)
- MaxProductOfThree:
本题的输入,是至少包含3个元素的数组A,然后需要的输出,是在A中任意选择三个元素后可以得到的最大乘积。并且数组内的元素可能为正,也可能为负。
这里我想的解题思路,是先对数组A中的元素按负数、非负数分组,然后再进行排序和判断。因为根据非负数的数量,最终三数相乘最大乘积的确定方式也会有所不同:
- 如果非负数数量=0,则最大乘积只能是负数,那一定是选最大的三个负数。
- 如果非负数数量>0,则最大乘积一定是非负数且包含最大的非负数,剩下两个是(第二、三大的非负数乘积)vs(最小的两个负数乘积)里较小的(要考虑数量不满足的情况)
这里的排序就使用了Python自带的sort(),也就是归并排序,由于对正数和负数进行了分组,所以O(nlogn)中的n拆分在两组,同时最开始进行分组是n的复杂度。
def solution(A):
# 3个元素则直接返回
if len(A)==3:
return A[0] * A[1] * A[2]
# 循环一遍,进行元素分组(负数、非负数)
negs = []
poss = []
for a in A:
if a<0:
negs.append(a)
else:
poss.append(a)
# 非负数1个以上
if len(poss)>0:
poss.sort()
negs.sort()
max_pos = poss[-1]
max2_pos_prod = poss[-3] * poss[-2] if len(poss)>=3 else 0
max2_neg_prod = negs[0] * negs[1] if len(negs)>=2 else 0
return max(max2_pos_prod, max2_neg_prod) * max_pos
# 没有非负数
else:
negs.sort()
return negs[-1] * negs[-2] * negs[-3]
- Triangle:
这题是从输入的Array A里,判断是否可以找到3个index下的自然数作为三角形的三条边,组成一个三角形。本身用到的定理是三角形的较小的两边之和大于第三边。如果存在则返回1,不存在则返回0。另外这题说A中元素的范围还是【-2147.., 2147...】,本身应该是说错了,应该都是正整数才对。
本身还是先对A中元素进行排序,然后从最小的三个元素开始向上不断平移一位,直到三个元素满足了【较小的两边之和>第三边】,则跳出循环并返回1,或者就是完全循环结束,返回0。
这里的原因是,若整个数组中存在着两边之和大于第三边的a+b>c,则对于排序后的c,取正好小于他的连续两个自然数(假设是d,e),则一定是有d+e>c的(因为e>=b, d>=a)。
def solution(A):
# 排序并循环一次完成判断
A.sort()
for i in range(len(A)-2):
a, b, c = A[i], A[i+1], A[i+2]
if a+b>c:
return 1
return 0
- NumberOfDiscIntersections:
本题的输入是一个Array A,A的每个index代表了一个圆盘的中心点坐标,而每个index对应的值,代表了圆盘的半径。需要返回的,是其中任意两个圆盘的组合里,会有交点的圆盘组合个数。
这个部分我解了半天也通过不了测试数据,后来找网上的解法看了半天,才发现圆盘相交即可,而不一定要圆圈相交。也就是如果一个圆盘完全包含于另一个圆盘内,也是算相交的。
解题思路的话,在搞清楚圆盘相交概念后就比较简单了,也能明白排序在这道题里的意义了。我们从A里第一个元素(圆心在最左侧)开始思考,判断后边的每个圆盘是否跟他相交,其实就是看右侧圆盘的最左边,是否有在第一个圆盘的最右边的左侧。
我们建立一个lefts,一个rights的数组来记录每个圆盘的左边和右边,然后组合判断的逻辑就是任意两个圆盘判断一次即可,最简单的循环判断逻辑是O(N^2)的复杂度,逻辑如下:
def solution(A):
# 生成每个圆盘左右两边的垂直线坐标
lefts = [i-a for i,a in enumerate(A)]
rights = [i+a for i,a in enumerate(A)]
# 组合判断
cnt = 0
for idx, r in enumerate(rights[:-1]):
for l in lefts[idx+1:]:
if l<=r:
cnt += 1
if cnt>10000000:
return -1
return cnt
但这个复杂度无疑是无法通过评估的,在很多case上的Performance表现并不及格:
而这时候只要稍加修改,对lowers数组先做一个排序,就可以在循环rights的同时,在sorted(lefts)里使用复杂度更低的二分查找,找到所有符合l<=u的盘子个数。
def binary_search(target, A):
N = len(A)
start, end = 0, N-1
if target<=A[0]: #事实上不可能,因为盘子的左边一定也在A里
return 0
elif target>=A[-1]:
return N
else:
while start<=end:
mid = (start+end)//2
if target < A[mid]:
end = mid-1
elif target > A[mid+1]:
start = mid+1
else:
return mid+1
def solution(A):
# 生成每个圆盘左右两边的垂直线坐标
lefts = [i-a for i,a in enumerate(A)]
rights = [i+a for i,a in enumerate(A)]
# 组合判断
cnt = 0
lefts.sort()
for idx, r in enumerate(rights[:-1]):
q_lcnt = binary_search(r+0.1, lefts) # 二分查找<=r(当前右边)的左边的数量
q_lcnt -= (idx+1) # 减掉包括自己在内的,往左移宫idx+1个盘子的左边(一定在q_lcnt里,且已经被统计过)
cnt += q_lcnt
if cnt>10000000:
return -1
return cnt