LeetCode 57.59.60,带你一块儿砍瓜切菜刷三题

本文始发于我的公众号:TechFlow,原创不易,求个关注python


今天是LeetCode专题的第34篇文章,恰好接下来的题目比较简单,不少和以前的作法相似。因此咱们今天出一个合集,一口气作完接下来的5七、59和60这三题。算法

再次申明一下,为了节约篇幅,保证文章的质量,我跳过了LeetCode当中全部的Easy以及少许没什么养分的Medium和Hard的问题。Easy的问题都不是很难,即便是新手通常来讲也不须要看题解,仔细想一想也应该能搞定。因此就不占用篇幅了,若是你们有比较感兴趣的Easy问题,能够在下方的小程序处给我留言。小程序


LeetCode 57 插入区间


第一题是57题Insert Interval,插入区间。题意会给定一组区间和一个单独的区间,要求将这个单独的区间插入区间集合,若是有两个区间存在交叉的状况,须要将它们合并,要求合并以后的最终结果。从题意上来看,基本上和咱们上一篇文章讲的区间合并问题彻底同样。惟一不一样的是,在这题当中给定的这一组区间都是自然有序的,咱们不须要对它进行排序。数组

区间已经有序了,剩下的就很简单了,咱们只须要进行插入便可。区间插入的判断条件仍是和以前同样,若是A区间的左端点在B区间左端点左侧,那么只要A区间的右侧端点在B区间左端点的右侧便可。因此这题基本上没有难度,就是一道裸题,我也不明白为何官方给定的难度是Hard。数据结构

咱们直接放出代码:app

class Solution:
    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
        ret = []
        # l, r记录待插入区间
        l, r = newInterval
        # 记录待插入区间是否完成插入
        flag = False
        
        for x, y in intervals:
            # x, y记录当前区间
            # 若是当前区间在待插入区间左侧,那么将当前区间插入答案
            if y < l:
                ret.append([x, y])
            # 若是当前区间在待插入区间右侧,那么将两个区间都插入答案
            elif r < x:
                if not flag:
                    flag = True
                    ret.append([l, r])
                ret.append([x, y])
            # 不然,说明当前区间与待插入区间能够合并
            # 更新待插入区间的范围
            else:
                l, r = min(l, x), max(r, y)
        # 若是最后尚未完成插入,说明待插入区间大于全部区间
        # 手动插入,防止遗漏
        if not flag:
            ret.append([l, r])
        return ret
复制代码

只要理解了区间合并的条件,这题真的没有难度。工具


LeetCode 59 螺旋矩阵II


前不久咱们刚出过螺旋矩阵I的题解,在螺旋矩阵I当中,咱们给定了一个矩阵让咱们螺旋形去遍历它。这题则相反,给定咱们矩阵的长和宽,让咱们生成一个这样的螺旋矩阵学习

咱们来看下样例:spa

在这题当中,咱们使用54题的思路也彻底能够解出来,可是这题更加简单了一些。因为是让咱们构造一个矩阵,那么咱们其实没有必要维护每一个方向的边界了。只要出现出界或者是遇到了已经填好的数字那么就说明应该转向了。某种程度上来讲,这题应该是I,以前的54题应该是II,由于这题更简单一些。code

若是对54题解法不熟悉的同窗,能够点击下方的传送门,学习一下方向数组的使用方法。

LeetCode54 螺旋矩阵,题目不重要,重要的是这个技巧

因为咱们不须要维护每一个方向的边界,而且移动的步数是固定的,更重要的是,转向每次最多只会发生一次,因此这个流程很是简单,咱们直接来看代码便可。

class Solution:
    def generateMatrix(self, n: int) -> List[List[int]]:
        # 初始化答案
        ret = [[0 for _ in range(n)] for _ in range(n)]
        # 初始化方向数组
        fx = [[0, 1], [1, 0], [0, -1], [-1, 0]]
        
        # 初始化方向以及起始点
        dt = 0
        x, y = 0, 0
        
        # n*n的方阵,一共须要填入n*n个数字
        for i in range(n*n):
            ret[x][y] = i+1
            # 移动的下一个位置
            x_, y_ = x+fx[dt][0], y+fx[dt][1]
            # 若是发生超界或者是遇到的数字大于0,说明须要转向
            if x_ < 0 or x_ >= n or y_ < 0 or y_ >= n or ret[x_][y_] > 0:
                dt = (dt + 1) % 4
            # 转向以后的位置
            x, y = x+fx[dt][0], y+fx[dt][1]
    
        return ret
复制代码

LeetCode 60 第K个排列


这题是一个排列组合问题,给定一个整数n,它表明[1,2,3,...,n]这n个元素的序列。而后还有一个整数K,要求这n个元素从小到大第K个排列。

这题其实蛮有意思,我以为能够上hard,但遗憾的是它有一个讨巧的办法,大概也许是这样,因此才被降级成Medium的吧。这个讨巧的办法应该蛮容易想到的,很明显,因为n个数字是肯定的,因此最小的排列必定是[1,2,3,...,n]。而咱们以前作过一道LeetCode31题,它求的是给定一个排列,而后生成字典序比它大恰好一位的下一个排列。

既然如此,咱们能够重复使用这个算法K-1次,就获得了答案了。对于作过31题的同窗而言,这题毫无难度。若是对于31题解法不熟悉的同窗能够点击下方传送门,回去复习一下。

LeetCode 31:递归、回溯、八皇后、全排列一篇文章全讲清楚

但其实能够不用这么麻烦,由于Python当中有自带的排列组合的工具库,咱们能够直接调用,只用5行代码就能够搞定。

class Solution:
    # 引入包
    from itertools import permutations
    def getPermutation(self, n: int, k: int) -> str:
        # 因为最后返回的结果要是string
        # 生成['1','2','3',...,'n']的序列用来计算下一个排列
        base = [str(i) for i in range(1, n+1)]
        # 调用permutations会获得一个按顺序计算排列的生成器
        perms = permutations(base, n)
        for i in range(k):
            # 咱们调用k-1次next就能得到答案了
            ret = next(perms)
        return ''.join(ret)
复制代码

这个方法虽然写起来短平快,可是也有一个重大的问题,就是耗时很长。这个其实很容易算,根据当前排列生成下一个排列的复杂度是O(n)。咱们一共须要计算k-1次,因此总体的复杂度就是O(nk),极端状况下k=n!,因此最差复杂度是n \cdot n!。要知道n个物体的排列一共有n!种,若是k很大,这个复杂度是爆表的。不过这题没有过多为难人,这样的复杂度也能AC。我我的以为这是一个很遗憾的事情,由于简单的算法也能够AC会致使不少人没有斗志再去研究复杂的算法了。

最后,咱们来看正解。

正解其实不涉及任何新的算法和数据结构,甚至说穿了一文不值,可是不少人就是很难想到。仍是老话题,它须要咱们对问题进行深刻的思考。

既然咱们一个一个地求下一个排列很是慢,那么咱们能不能有快速一点的办法呢?加快速度大概有两种办法,第一种增长步幅,好比以前的方法是每次获取字典序+1的排列, 咱们能不能生成字典序+k的排列?第二种想法是咱们能不能直接求解答案,直接生成出这个排列?

简单分析一下会发现第一种是不可行的,或者说是伪命题。由于若是说咱们能够想出一个求解字典序+k的算法,那么咱们令这个k等于题目中要求的k不就是直接求解答案了?因此说,若是可能存在更好的办法,必定只能是第二种,也就是直接构造答案的方法。那么问题就剩下了,咱们怎么直接构造这个答案呢?这就须要咱们对排列组合的理解了。

若是你亲手写过某个集合的全排列,你会发现一些规律。好比说123的全排列好了。咱们都知道,它的全排列一共是123,132,213,231,31和321。这个很简单,咱们观察一下会发现,123一共三个数字,6种排列。每一个数字打头的都是2种,若是换成1234的排列呢?列一下就会知道是6种。若是你找规律你会发现,每一个数字开头的种数是(n-1)!。

若是要推导也很简单,由于每一个数字都是公平的,因此每一个数字开头的种数都是同样的。而全排列一共有n!种,因此分摊到每一个数字头上剩下(n-1)!种。那若是咱们已经知道了第一个数字是1,请问第二个数字是2的种类数有多少种?一样的方法能够算出是(n-2)!种。到这里有没有火花闪过的感受?

咱们来举个例子吧,假设咱们如今n=5,咱们算一下会知道,一共有120种排列。假设咱们要求第100个排列,因为从0开始,因此也就是第99大的排列。那么根据咱们刚才说的,咱们已经知道每一个数字开头的状况都是4!也就是24种,那么咱们用99/24获得4,因此开头的数字是第4大的数(第0大的是1),也就是5。答案一会儿缩小了不少,那接下来呢?接下来咱们用99减去5以前的全部状况,也就是96种,获得3。也就说答案是5开头的第3个排列,那么我再问,第二个数字是多少?

一样的办法,除去5以后还剩下4个数字,每一个数字排第二都有3!也就是6种,咱们用3/6=0,应该是第0大的数,也就是1。咱们继续,除去5和1以后,还剩3个数字。每一个数字排第三的状况有2种,咱们用3/2=1,咱们应该选择第1大的数,这里剩下的数是2,3,4,因此第三位应该是3。以此类推,咱们能够获得每一位的数字。总体的复杂度是O(n),和上面的方法相比,有了质的突破。

我把整个计算过程作成了图,有看不懂的小伙伴能够结合一下下图理解:

最后,咱们把这个方法实现便可:

class Solution:
    def getPermutation(self, n: int, k: int) -> str:
        frac = [1 for _ in range(n+1)]
        # 生成一个序列存放全部的元素
        nums = [i for i in range(1, n+1)]
        # 计算每一位的种类数
        for i in range(1, n):
            frac[i] = i * frac[i-1]
            
        ret = ''
        k -= 1
        for i in range(n-1, -1, -1):
            # 计算第i位的数字是当前第几大的数
            cur = k // frac[i]
            # 放完以后取模,表示去除掉以前的全部状况数
            k %= frac[i]
            # 求出当前元素以后,须要从序列当中移除
            ret += str(nums[cur])
            nums.remove(nums[cur])
            
        return ret
复制代码

结尾


到这里三题就算是讲完了,今天的这三道题目或多或少的都和以前的问题有关,这也是我把这三题放在一篇文章当中阐述的缘由。

这三题当中我我的最喜欢第三题,尤为是完美解法。它的整个思路和代码都不复杂,也没有什么特殊的技巧或者是方法,可是若是没有对题目有足够深刻的了解是很难想到这个算法的。这其实也是算法题的精髓所在,比赛当中多的是知道解法也作不出来的题目。因此咱们要提高算法水平,光学算法是不够的,也须要对题目有深刻理解才行。

今天的文章就到这里,原创不易,须要你的一个关注,你的举手之劳对我来讲很重要。

相关文章
相关标签/搜索