Codility刷题之旅 - Sorting

240 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

今天继续Codility的Lessons部分的第6个主题——Sorting。

Codility - Lessons - Sorting

还是按先总结红色部分PDF,然后再完成蓝色的Tasks部分的节奏来~

image.png

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: image.png

本题就是上一部分——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)

image.png

  • MaxProductOfThree: image.png

本题的输入,是至少包含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]

image.png

  • Triangle: image.png

这题是从输入的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

image.png

  • NumberOfDiscIntersections: image.png

本题的输入是一个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表现并不及格: image.png

而这时候只要稍加修改,对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

image.png