【PythonCode】力扣Leetcode31~35题Python版

【PythonCode】力扣Leetcode31~35题Python版

前言

力扣Leetcode是一个集学习、刷题、竞赛等功能于一体的编程学习平台,很多计算机相关专业的学生、编程自学者、IT从业者在上面学习和刷题。
在Leetcode上刷题,可以选择各种主流的编程语言,如C++、JAVA、Python、Go等。还可以在线编程,实时执行代码,如果代码通过了平台准备的测试用例,就可以通过题目。
本系列中的文章从Leetcode的第1题开始,记录我用Python语言提交的代码和思路,供Python学习参考。

31. 下一个排列

整数数组的一个排列就是将其所有成员以序列或线性顺序排列。

  • 例如,arr = [1,2,3] ,以下这些都可以视作 arr 的排列:[1,2,3]、[1,3,2]、[3,1,2]、[2,3,1] 。

整数数组的下一个排列是指其整数的下一个字典序更大的排列。更正式地,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的下一个排列就是在这个有序容器中排在它后面的那个排列。如果不存在下一个更大的排列,那么这个数组必须重排为字典序最小的排列(即,其元素按升序排列)。

  • 例如,arr = [1,2,3] 的下一个排列是 [1,3,2] 。
  • 类似地,arr = [2,3,1] 的下一个排列是 [3,1,2] 。
  • 而 arr = [3,2,1] 的下一个排列是 [1,2,3] ,因为 [3,2,1] 不存在一个字典序更大的排列。

给你一个整数数组 nums ,找出 nums 的下一个排列。

必须原地修改,只允许使用额外常数空间。

示例 1:
输入:nums = [1,2,3]
输出:[1,3,2]
示例 2:
输入:nums = [3,2,1]
输出:[1,2,3]
示例 3:
输入:nums = [1,1,5]
输出:[1,5,1]
提示:
1 <= nums.length <= 100
0 <= nums[i] <= 100

代码实现:

class Solution:
    def nextPermutation(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        i = len(nums) - 2
        while i >= 0 and nums[i] >= nums[i + 1]:
            i -= 1
        if i >= 0:
            j = len(nums) - 1
            while j >= 0 and nums[i] >= nums[j]:
                j -= 1
            nums[i], nums[j] = nums[j], nums[i]
        
        left, right = i+1, len(nums)-1
        while left < right:
            nums[left], nums[right] = nums[right], nums[left]
            left += 1
            right -= 1

解题思路:先根据题意,搞清楚下一个排列的含义。首先,整数数组的排列是将数组中的元素按任意顺序组合成的另一个数组,这跟初中高中数学中的排列基本一样。也可以参考我之前对Python中Counter的介绍,只要数组中各元素的数量与整数数组相等,不管数组中的元素是什么顺序,该数组都是整数数组的排列。参考:详解Python中非常好用的计数器Counter

下一个排列是指比当前排列的字典序更大的下一个排列,按题目原话,如果数组的所有排列根据其字典顺序从小到大排列在一个容器中,那么数组的下一个排列就是在这个有序容器中排在它后面的那个排列。先看字典序是怎么排序的,字典序是一个数学概念,也称为字典顺序、词汇顺序,是基于字符顺序排列的方法。例如一个数组由数字 1,2,3 组成,[1,2,3]是第一个排列,它的下一个排列是 [1,3,2],后面的排列依次是[2,1,3],[2,3,1],[3,1,2],[3,2,1]。根据题目定义,字典序最大的排列[3,2,1]的下一个排列是字典序最小的排列[1,2,3]。

根据前面的分析可以发现,下一个排列刚好比前一个排列大一点点,中间没有其他字典序排列。假如将排列中某一个数字与比它靠前且比它小的数字交换位置,就可以得到字典序更靠后的排列,如果要保证交换位置后,得到的是下一个排列,而不是中间隔着多个排列,在交换位置时,应该尽量找靠近数组末尾的数字来交换,且要找到尽量接近的数字进行交换。另外,在交换数字位置后,后面的子数组要重新排列,将更大的数字排到后面。

根据这个思路,并结合案例分析,先从数组的末尾(倒数第二个)开始往前找,找到最大的索引 i 满足 nums[i] < nums[i+1],如果不存在,说明排列是降序的,例如前面的[3,2,1],此时将排列翻转,即可得到下一个排列[1,2,3]。找到 i 后,再从数组最后一个数开始往前找,找到最大的索引 j 满足 nums[j] > nums[i],将nums[j]和nums[i]交换位置。交换后,i 后面的子数组nums[i+1:]是降序的,将nums[i+1:]翻转成升序,这样就可以得到下一个排列。

因为最大的索引 i 满足 nums[i] < nums[i+1],所以nums[i]后面的子数组nums[i+1:]一定是递减的,nums[i]是需要变大一点点的那个数(nums[i]后的每个数都比其后面大,不具备变大的条件)。因为最大的索引 j 满足 nums[j] > nums[i],所以nums[j]一定是nums[i]后面所有大于nums[i]的数字中最小的(大于nums[i]的数中最接近nums[i]的),将nums[j]和nums[i]交换位置可以让这个数组在 i 的位置上刚好大一点点。然后,因为nums[i+1:]是递减的,并且nums[j] > nums[i],同时nums[j+1] <= nums[i](如果nums[j+1] > nums[i],那么 j 就不是满足nums[j] > nums[i]的最大索引),根据这几个条件可以推出,交换nums[j]和nums[i]的位置后,nums[i+1:]仍然是递减的,此时,将nums[i+1:]翻转成升序,即可保证新排列是下一个排列。

根据上面的分析,可以按照思路写代码,题目要求原地修改数组,所以代码中要直接交换不同位置的数字,不能创建新数组。先根据规则找到索引 i 和 j,交换他们的值,然后翻转nums[i+1:],翻转时使用双指针,不断交换数组中元素的位置。

32. 最长有效括号

给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。

示例 1:
输入:s = “(()”
输出:2
解释:
最长有效括号子串是 “()”
示例 2: 输入:s = “)()())”
输出:4
解释:
最长有效括号子串是 “()()”
示例 3:
输入:s = “”
输出:0
提示:
0 <= s.length <= 3 * 104
s[i] 为 ‘(’ 或 ‘)’

代码实现:

class Solution:
    def longestValidParentheses(self, s: str) -> int:
        n = len(s)
        if n == 0: 
            return 0
        dp = [0] * n
        for i in range(n):
            if i > 0 and s[i] == ")":
                if  s[i-1] == "(":
                    dp[i] = dp[i-2] + 2
                elif s[i-1] == ")" and i-dp[i-1]-1 >= 0 and s[i-dp[i-1]-1] == "(":
                    dp[i] = dp[i-1] + 2 + dp[i-dp[i-1]-2]
        return max(dp)

解题思路:根据题意,字符串中只包含 ‘(’ 和 ‘)’ 两种字符,目标是要找到最长的有效括号子串,返回子串的长度。

先初步分析,有效的括号一定是以 ‘)’ 结尾的,假设字符串的长度为 n ,如果最后一个字符是 ‘(’ ,那么它的最长有效括号长度与前 n-1 个字符的最长有效括号长度一样。如果最后一个字符是 ‘)’ ,并且倒数第二个字符是 ‘(’ ,那么它的最长有效括号长度为前 n-2 个字符的最长有效括号+2,如果… 。从右往左分析,要找到字符串的最长有效括号长度,可以先找到子问题的最长有效括号长度,这可以用动态规划来求解。参考:循序渐进,搞懂什么是动态规划,前面力扣第10题也用了动态规划求解,可以结合一起看加深理解。

按照动态规划的解题步骤,第一步先定义问题的状态,在字符串 s 中,有效的括号可能有多段,中间被无法按规则组合连接的字符隔开了,最长的是哪一段不可预知。所以为了方便推导,用dp[i]表示由字符 s[i] 及其前面的字符组成的最长有效括号(必须包含字符s[i]),这样就先不用判断哪段更长。dp是一个一维数组,从0开始求出每个dp[i]的值,dp中的最大值max(dp)就是本题的答案。

第二步列出状态转移方程,当第n个字符为 ‘(’ 时,dp[n]=0。当第n个字符为 ‘)’ 时,如果第n-1个字符是 ‘(’ ,则dp[n]=dp[n-2]+2。如果第n-1个字符也为 ‘)’ ,此时,假设第n-1个 ‘)’ 是一段有效括号的一部分,对于第n个 ‘)’ ,如果它是一段更长有效括号的一部分,那么它一定有一个对应的 ‘(’ ,且对应的 ‘(’ 位置在第n-1个 ‘)’ 的最长有效括号的前面(dp[n-1]的前面)。因此,如果第n-1个 ‘)’ 构成的最长有效括号的前面恰好是 ‘(’ ,那么我们就用 dp[n-1] 加上 2,并且,在找到的 ‘(’ 前面,还可能有一段有效括号相邻,还要再加上这一段,这一段是第n−dp[n−1]−2个字符的最长有效括号即dp[n-dp[n-1]-2],所以dp[n] = dp[n-1] + 2 + dp[n-dp[n-1]-2]。要找出这个转移方程,最好画图分析下,下图是一个示例,供参考。注意事项,dp[n-dp[n-1]-2]可能为0,并且,如果找到的 ‘(’ 是字符串 s 的第一个字符,n-dp[n-1]-2=-1,这种情况dp[n]应该加上0,dp[n-dp[n-1]-2]=dp[-1]取到的是数组的最后一个值,初始化数组时要考虑。如果第n个 ‘)’ 找不到它对应的 ‘(’ ,则dp[n]=0。

在这里插入图片描述

第三步为状态初始化,字符串s的长度为n,根据前面的分析,有一部分情况dp[i]为0,所以将dp数组的值全部初始化为0,这样在代码中,这些情况就可以不做处理。根据上面的分析,可以按照思路写代码。

33. 搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标从0开始计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你旋转后的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

你必须设计一个时间复杂度为 O(logn) 的算法解决此问题。

示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
示例 2:
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
示例 3:
输入:nums = [1], target = 0
输出:-1
提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-104 <= target <= 104

代码实现:

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)-1
        while left < right:
            mid = (left+right) // 2
            if nums[mid] >= nums[left]:
                if nums[left] <= target <= nums[mid]:
                    right = mid
                else:
                    left = mid + 1
            else:
                if nums[mid] <= target <= nums[right]:
                    left = mid
                else:
                    right = mid - 1
        if nums[left] == target:
            return left
        else:
            return -1

解题思路:要判断nums中是否存在target,如果数组升序且无时间复杂度要求,直接遍历即可,非常简单。本题的要求相对最简单的情况有两个变化,一是要求时间复杂度为O(logn),这就要用二分搜索,不能遍历。二是对升序的数组做了一次旋转,使数组的结构发生了变化。

分析数组nums的构成,nums最开始升序排列且值都不重复,在索引k处对nums旋转是将nums中k索引到结尾的所有数整体平移到数组的前面,旋转后的nums分为两段升序的部分,并且nums中的第一个数大于最后一个数。

k是随机的,不能判断两段升序数据哪一段更长,因此在用二分法时,数组的中间值可能大于第一个值,也可能小于第一个值,所以在二分法时,要分两种情况,判断两种情况下target在二分搜索的哪个范围,不断进行二分缩小范围,找到target的位置。

具体到代码实现,使用两个指针(左指针和右指针)指向数组的头和尾,根据头和尾找到它们中间的那个数,与目标值比较。如果中间值大于等于第一个值,说明旋转后的数组第一段更长,此时,如果target大于等于数组的第一个值且小于等于中间值,说明target可能在数组左半部分,将右指针更新成中间索引,继续在左半部分二分搜索,如果target小于第一个值或大于中间值,说明target可能在数组右半部分,将左指针更新成中间索引的下一个索引,继续二分。如果中间值小于第一个值,说明旋转后的数组第二段更长,此时,如果target大于等于中间值且小于等于最后一个值,说明target可能在数组右半部分,将左指针更新成中间索引,继续在右半部分二分搜索,如果target小于中间值或大于最后一个值,说明target可能在数组左半部分,将右指针更新成中间索引的前一个索引,继续二分。

直到最后左指针与右指针相遇,可以找到target的位置或确认数组中不存在target,返回结果。

34. 在排序数组中查找元素的第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

你必须设计并实现时间复杂度为 O(logn) 的算法解决此问题。

示例 1:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
示例 2:
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
示例 3:
输入:nums = [], target = 0
输出:[-1,-1]
提示:
0 <= nums.length <= 105
-109 <= nums[i] <= 109
nums 是一个非递减数组
-109 <= target <= 109

代码实现:

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        if len(nums) == 0 or nums[0] > target or nums[-1] < target:
            return [-1, -1]

        def search_start(nums,target):
            left, right = 0, len(nums)-1
            while left < right:
                mid = (right+left) // 2
                if nums[mid] >= target:
                    right = mid
                else:
                    left = mid + 1
            return left if nums[left] == target else -1
        
        def search_end(nums,target):
            left, right = 0, len(nums)
            while left < right:
                mid = (right+left) // 2
                if nums[mid] <= target:
                    left = mid + 1
                else:
                    right = mid
            return right-1 if nums[right-1] == target else -1
        
        start = search_start(nums, target)
        return [-1, -1] if start == -1 else [start, search_end(nums,target)]

解题思路:根据题目描述,数组nums是按非递减排序的,数组中靠前的数都小于或等于靠后的数,并且,数组中相等的数的位置一定是连续的。

给定一个目标值target,题目要求找到target在数组中的开始位置和结束位置,不存在target时返回[-1, -1]。当数组中存在目标值时,可能存在一个,也可能存在多个,所以开始位置可能等于结束位置,也可能不相等。因此,要将问题拆分成两个部分,一个部分是找到目标值出现的开始位置,另一个部分是找到目标值出现的结束位置。

题目要求时间复杂度为O(logn),需要使用二分搜索,二分法在前面的题目中已经多次用到了,思路都是相通的。

具体到代码实现,使用两个指针(左指针和右指针)指向数组的头和尾,根据头和尾找到它们中间的那个数,与目标值比较。如果中间值大于等于目标值,说明目标值的开始位置可能在数组的左半部分,此时将右指针更新成中间索引,继续二分搜索。如果中间值小于目标值,说明目标值的开始位置可能在数组的右半部分,此时将左指针更新成中间索引的后一个索引,继续二分搜索。不断二分,直到左指针与右指针相遇,可以找到开始位置或确认数组中不存在目标值。

找结束位置与找开始位置基本一样,唯一区别是中间的数与目标值相等时,下一步搜索的方向相反,找开始位置时尽量往左边找,更新右指针,找结束位置时尽量往右边找,更新左指针。

特别说明下,在上面的代码实现中,计算mid是取整除(使用二分搜索一般都会这样,本题比较特殊,所以特别说一下)。在找开始位置时,严格来说,中间值大于目标值和中间值等于目标值并不一样,因为mid是取整除,在中间值大于目标值时将右指针更新成mid,而不是mid-1。在找结束位置时,也是因为mid是取整除,右指针的起始值为数组长度(比最大索引大1),这是为了避免取整除的二分法永远也搜索不到数组的最后一个数,右指针变大后,返回结果时返回右指针的前一个索引。同理,严格来说,中间值小于目标值与中间值等于目标值并不一样,此时都将左指针更新成mid+1,而不是mid,是为了避免左右指针永远也不相遇,返回值是右指针的前一个索引,所以这样刚好可以返回正确的结束位置。

最后再补充一点,如果找不到开始位置,说明数组中不存在目标值,就不用找结束位置了,所以先执行找开始位置的代码,能找到开始位置,再执行找结束位置的代码。

35. 搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(logn) 的算法。

示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
示例 2:
输入: nums = [1,3,5,6], target = 2
输出: 1
示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4
提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 为 无重复元素 的 升序 排列数组
-104 <= target <= 104

代码实现:

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        left, right = 0, len(nums)-1
        while left < right:
            mid = (right+left) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                left = mid + 1
            elif nums[mid] > target:
                right = mid - 1
        return left if nums[left] >= target else left+1

解题思路:本题比较简单,思路与前面两题非常相似,在一个已经升序排列的数组(无重复值)中查找目标值,如果存在则返回目标值的索引,如果不存在则返回目标值按顺序插入升序数组的预期索引。

本题也要求时间复杂度为O(logn),那就与前面两题一样,使用二分法搜索。

具体到代码实现,使用两个指针(左指针和右指针)指向数组的头和尾,根据头和尾找到它们中间的那个数,与目标值比较。如果中间值等于目标值,已经找到结果,直接返回。如果中间值小于目标值,说明目标值可能在数组的右半部分,将左指针更新成中间索引的后一个索引,继续二分搜索。反之,如果中间值大于目标值,则更新右指针,在左半部分继续二分搜索。不断二分下去,最终左指针与右指针相遇,此时如果还没有找到目标值,说明数组中不存在目标值,则根据最终指针指向的索引判断将目标值插入数组中的预期索引。

指针相遇时,如果指针指向的值与目标值相等,则返回指针指向的索引。如果指针指向的值大于目标值,则目标值应该插入指针指向的位置,“抢占”这个位置,将更大的数往后“挤”。如果指针指向的值小于目标值,则目标值应该插入指针指向的下一个索引。


相关阅读

【PythonCode】力扣Leetcode26~30题Python版

📢欢迎 点赞👍 收藏⭐ 评论📝 关注 如有错误敬请指正!

☟ 学Python,点击下方名片关注我。☟

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小斌哥ge

非常感谢,祝你一切顺利。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值