Skip to content

wxtt-github/LeetCode-implemented-by-Python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

19 Commits
 
 
 
 

Repository files navigation

LeetCode-implemented-by-Python

根据灵神整理的基础算法题单,自己做了一遍,加上了个人理解的思路,复杂度说明(一般为最低复杂度)。

❤感谢灵神辛苦整理题单,讲授课程。

📑灵茶山艾府题单

  • 相向双指针1
  • 相向双指针2
  • 滑动窗口
  • 二分查找
  • [二分查找 - 变形](#二分查找 - 变形)
  • [链表 - 反转系列](#链表 - 反转系列)
  • [链表 - 快慢指针](#链表 - 快慢指针)
  • [链表 - 删除系列](#链表 - 删除系列)
  • [二叉树与递归 - 深入理解](#二叉树与递归 - 深入理解)
  • [二叉树与递归 - 灵活运用](#二叉树与递归 - 灵活运用)
  • [二叉树与递归 - 前序/中序/后序](#二叉树与递归 - 前序/中序/后序)
  • [二叉树与递归 - 最近公共祖先](#二叉树与递归 - 最近公共祖先)
  • [二叉树 - BFS](#二叉树 - BFS)
  • [回溯 - 子集型](#回溯 - 子集型)
  • [回溯 - 组合型与剪枝](#回溯 - 组合型与剪枝)
  • [回溯 - 排列型](#回溯 - 排列型)
  • [动态规划 - 从记忆化搜索到递推](#动态规划 - 从记忆化搜索到递推)
  • [0-1 背包 完全背包 至多/恰好/至少](#0-1 背包 完全背包 至多/恰好/至少)
  • [最长公共子序列 LCS](#最长公共子序列 LCS)
  • [最长递增子序列 LIS](#最长递增子序列 LIS)
  • [状态机 DP - 买卖股票系列](#状态机 DP - 买卖股票系列)
  • [区间 DP](#区间 DP)
  • [树形 DP - 直径系列](#树形 DP - 直径系列)
  • [树形 DP - 最大独立集](#树形 DP - 最大独立集)
  • [树形 DP - 最小支配集](#树形 DP - 最小支配集)
  • 单调栈
  • 单调队列

相向双指针1

  1. 167. 两数之和 II - 输入有序数组
"""
思路:
1.最简单的暴力法是写两层for循环,枚举组合数Cn2,这样时间复杂度是O(n^2),
因为你没有利用到数组有序这个条件。
2.进阶做法是相向双指针,left=0,right=len(numbers)-1,计算sum1值,
这里有3种情况,等于target时说明找到答案直接返回(题目说明存在且唯一答案),
小于target时需要把左指针右移(因为假设左指针不动,右指针已经是可给的最大值了,
中间的值会更小,均不符合,因此要控制左指针的值偏大一些),同理大于target时,
要把右指针左移。

时间复杂度:O(n)。最多用双指针遍历整个数组。
空间复杂度:O(1)。只创建了常量空间。
"""
class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left = 0
        right = len(numbers) - 1
        while left < right:
            sum1 = numbers[left] + numbers[right]
            if sum1 == target:
                return [left+1, right+1]
            elif sum1 < target:
                left += 1
            elif sum1 > target:
                right -= 1
  1. 15. 三数之和
"""
思路:三数之和是两数之和的进阶变式,我们需要将其转换为两数之和问题
1.两数之和是在有序条件下,所以我们直接nums.sort()进行排序。排序好后,
固定一个数x,遍历数组从0到length-3的位置(因为要留两个位置给剩余两个数),
原题为x+y+z=0,因此问题转换成y+z=-x,即两数之和的问题。需要注意的是,
两数之和求的是数组下标,而三数之和求的是数组中的元素,考虑到排序后有
相同元素并列在一起的情况,为了不使ans出现同样的元素列表,我们需要跳过
这些相同的元素(注意这里不能去重,否则无法处理[0,0,0]这样的情况),
只需要做如下处理即可
①if i > 0 and nums[i] == nums[i-1],
②left += 1
    while left < right and nums[left] == nums[left-1]:
        left += 1
2.两个优化。I.当前遍历的列表的前三个数相加大于0时,说明已经找不到等于0的
三元组了,直接break。II.当前遍历的列表的第一个元素x和最后两个元素小于0时,
说明只有当第一个元素x变大时,才有机会出现等于0的三元组,因此直接continue。
注意这两个的区别,一个是全局无三元组,一个是双指针已经没必要进行下去。

时间复杂度:O(n^2)。快排的时间复杂度为O(nlogn),固定x遍历+双指针两层嵌套循环,
          时间复杂度为O(n^2)
空间复杂度:O(1),如果不算快排的空间复杂度的话。
          O(logn),如果算快排的空间复杂度的话。
"""
class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        ans = []
        length = len(nums)
        for i in range(0, length-2):
            x = nums[i]
            if i > 0 and nums[i] == nums[i-1]:
                continue
            # 优化处理1
            if x + nums[i+1] + nums[i+2] > 0:
                break
            # 优化处理2
            if x + nums[length-1] + nums[length-2] < 0:
                continue
            left = i + 1
            right = length - 1
            while left < right:
                s = nums[left] + nums[right]
                if s < (-1)*x:
                    left += 1
                elif s > (-1)*x:
                    right -= 1
                else:
                    ans.append([x, nums[left], nums[right]])
                    left += 1
                    while left < right and nums[left] == nums[left-1]:
                        left += 1
                    right -= 1
                    while left < right and nums[right] == nums[right+1]:
                        right -= 1
        return ans
  1. 2824. 统计和小于目标的下标对数目
"""
思路:很明显的两数之和变式,只是将等于改为了小于,把有序变成了无序,
首先nums.sort()对数组排序,然后用双指针处理,值得注意的是,当满足
nums[left]+nums[right]<target时,说明这是一个结果,此时不能将
cnt自增再让left右移,因为left和right直接包围的数和left组合起来也
符合条件,如果left右移的话,就永远丢失这些解了。因此cnt要加上right-left,
再将left右移。

时间复杂度:O(nlogn)。
空间复杂度:O(1),如果不算快排的空间复杂度的话。O(logn),如果算的话。
"""
class Solution:
    def countPairs(self, nums: List[int], target: int) -> int:
        nums.sort()
        cnt = 0
        left = 0
        right = len(nums) - 1
        while left < right:
            s = nums[left] + nums[right]
            if s < target:
                cnt += right - left
                left += 1
            elif s >= target:
                right -= 1
        return cnt
  1. 16. 最接近的三数之和
"""
思路:三数之和的变式,在三数之和的基础上存储与target值的差值的绝对值,
在双指针内,每次尝试更新绝对值,并尽量往着target值靠即可。需要注意的是
优化,一方面双指针遍历时若相等,直接返回target,另一方面,依然可以根据
与target值进行大小比较进行两个优化,一个break一个continue,对于continue
的优化,还要记得更新记录的最小绝对值temp。

时间复杂度:O(n^2),固定第一个数和双指针共两层嵌套循环。
空间复杂度:空间复杂度:O(1),不算快排的话。O(logn),算快排的话。
"""
class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        nums.sort()
        temp = abs(nums[0] + nums[1] + nums[2] - target)
        ans = nums[0] + nums[1] + nums[2]
        length = len(nums)
        for i in range(length-2):
            x = nums[i]
            if i > 0 and nums[i] == nums[i-1]:
                continue

            # 优化1
            s = x + nums[i+1] + nums[i+2]
            if s > target:
                if s - target < temp:
                    ans = s
                break
            # 优化2
            s = x + nums[length-2] + nums[length-1]
            if s < target:
                if target - s < temp:
                    ans = s
                    temp = target - s
                continue

            left = i + 1
            right = length - 1
            while left < right:
                s1 = nums[left] + nums[right] + x
                s2 = abs(nums[left] + nums[right] + x - target)
                if s2 < temp:
                    temp = s2
                    ans = nums[left] + nums[right] + x
                if s1 < target:
                    left += 1
                elif s1 > target:
                    right -= 1
                else:
                    return target
        return ans
  1. 18. 四数之和
"""
思路:
1.实际上与三数之和相比没有质的进阶,只需要先固定第一个数,再固定第二个数,
再处理双指针即可,只需要注意各个循环的边界条件
2.对于优化,在三数之和时有两个优化,一个是计算前三个之和,若大于target则
直接break(因为双指针怎么选都会更大,从固定数开始后续的子列表已经没有意义了),
然后是固定数加最后两个数,若小于target,双指针怎么变都是更小,我们需要考虑
的是让固定数变大,因此我们直接continue(相当于这个固定数已经没有意义了,我们
需要去找别的固定数,注意与break的区别是break直接舍弃全部,因为确实没解了,
而固定数变大还是可能有解的)

时间复杂度:O(n^3),固定第一、二个数字,双指针遍历共嵌套3层循环。
空间复杂度:O(1),不算快排的话。O(logn),算快排的话。
"""
class Solution:
    def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
        nums.sort()
        length = len(nums)
        ans = []
        for i in range(0, length-3):
            x = nums[i]
            if i > 0 and nums[i] == nums[i-1]:
                continue
            # 固定第一个数优化1
            if x + nums[i+1] + nums[i+2] + nums[i+3] > target:
                break
            # 固定第一个数优化2
            if x + nums[length-3] + nums[length-2] + nums[length-1] < target:
                continue
            for j in range(i+1, length-2):
                if j > i+1 and nums[j] == nums[j-1]:
                    continue
                y = nums[j]
                # 固定第二个数优化1
                if x + y + nums[j+1] + nums[j+2] > target:
                    break
                # 固定第二个数优化2
                if x + y + nums[length-2] + nums[length-1] < target:
                    continue
                left = j + 1
                right = length - 1
                while left < right:
                    s = nums[left] + nums[right] + y + x
                    if s == target:
                        ans.append([x, y, nums[left], nums[right]])
                        left += 1
                        while left < right and nums[left] == nums[left-1]:
                            left += 1
                        right -= 1
                        while left < right and nums[right] == nums[right+1]:
                            right -=1
                    elif s < target:
                        left += 1
                    elif s > target:
                        right -= 1
        return ans
  1. 611. 有效三角形的个数
"""
思路:三角形三条边的关系,很容易看出是三数之和的变式。
1.框架。由于有效三角形的定义是任意两边之和大于第三边,但是任意是比较难处理的,
如果你任选,需要组合数为3种情况,但是如果是有序状态a<=b<=c,那么只需要
满足a+b>c一种情况即可(因为无论是a或b加上了c,都会大于另外一边,因为c是
最大的嘛,c>b,c>a)。因此我们先nums.sort(reverse=True)进行排序,reverse
代表反转,不带参数是升序排列,反转后就是降序排列。为什么需要降序排列呢,
因为假如我们升序排列,a<=b<=c,要满足a+b>c,变式为a+b-c>0,而我们一般会
固定第一个数a,双指针控制的b,c一正一负,就不好调整双指针了,因为left
右移满足>0,right左移也继续满足>0,不是我们想要的效果,我们想要的效果应该
是,控制较小的两个数,来和最大的数进行比较,因此我们采用降序排列(其实你继续
升序排列也可以,只需要从i从2开始遍历到length-1,固定最大数nums[i]就行,但
我不太喜欢)。当你采用降序排列时,a>=b>=c,我们固定a,i从0到length-1遍历,
双指针控制b,c,当b+c>a时,也说明了b,c中间的数和b一起配对也满足,因此
ans+=right-left,防止漏解,然后left+=1,当b+c<=a时,我们希望让两数之和变大,
因此我们控制right-=1,至此,完成了主体逻辑。
2.两个优化。三数之和的两个优化在这里也适用,本质上都是拿这三个数构造一个式子,
与target进行比较。
优化I.比如这里我们降序排列后,固定a,后续的式子我们取出两个最小的数min[-1],
min[-2],若这两个数的和大于a,说明从a到min[-1]的这个序列,a..min[-1],min[-2]
任取3个数均满足有效三角形(因为你最小的两个边大于最大边了,我把最大边a变成中间
数也满足,我把min[-1][-2]改成其他两个数也大于a,也大于其他数)。因此我们计算这
个序列的组合数C(length-i)(3),即从length-i个数中取出3个数的组合数,然后break。
优化II.固定a,后续的式子取出两个最大数,若这两个最大数的和小于等于a,说明对于a,
在后面的序列中已经找不到两个数b,c,使得b+c>a了,因此我们continue,使a变为下一个
数,即让a变小,让他还有可能匹配。

时间复杂度:O(n^2)。固定数+双指针共两层嵌套循环。
空间复杂度:O(1),不算快排的话。O(logn),算快排的话。
"""
class Solution:
    def triangleNumber(self, nums: List[int]) -> int:
        nums.sort(reverse=True)
        ans = 0
        length = len(nums)
        if length < 3:
            return 0
        for i in range(length-2):
            x = nums[i]
            # 优化1
            if nums[length-2] + nums[length-1] > x:
                ans += (length-i) * (length-i-1) * (length-i-2) // 6
                break
            # 优化2
            if nums[i+1] + nums[i+2] <= x:
                continue
            left = i + 1
            right = length - 1
            while left < right:
                s = nums[left] + nums[right]
                if s > x:
                    ans += right - left
                    left += 1
                elif s <= x:
                    right -= 1
        return ans

相向双指针2

  1. 11. 盛最多水的容器
"""
思路:双指针。根据题目的示例1,又根据木桶效应,我们首先固定左边最长的
红边,若右边较短的红边往内靠近(替换成中间的黑边),则木桶的长度一定是
变小的,若这时高度再变小,则说明容积肯定会变小,我们的目的是在长度变小
时,尽量找到高度更高的木板,与其对冲,说不定就能找到更大的容积。所以我们
控制双指针在0,length-1的位置,先把长度置为最大,然后每次选择较矮的木板,
往内靠近。
优化。当较短木板向里靠近时,我们只需要关注高度比该木板高的中间木板,没有
必要每次靠近一块木板就计算一次area,因此我们可以先控制指针向里一位,接着
用循环找到较高的木板(注意要先移动再用while循环,不要直接while循环,否则
一开始条件不满足的话,会造成left永远不右移,死循环)。如下是左指针的优化:
left += 1
while height[left] < height[left-1] and left < right:
    left += 1

时间复杂度:O(n)
空间复杂度:O(1)
"""
class Solution:
    def maxArea(self, height: List[int]) -> int:
        ans = 0
        left = 0
        right = len(height) - 1
        while left < right:
            area = (right - left) * min(height[left], height[right])
            if area > ans:
                ans = area
            if height[left] < height[right]:
                left += 1
                # 优化1
                while height[left] < height[left-1] and left < right:
                    left += 1
            else:
                right -= 1
                # 优化2
                while height[right] < height[right+1] and left < right:
                    right -= 1
        return ans
  1. 42. 接雨水

法一(时间复杂度:O(n),空间复杂度:O(n))

"""
思路:
1.根据木桶效应,容纳的水量取决于短板。对于第i格,可以用两块板围成一个
木桶,前面的板为前缀最大值,后面的板为后缀最大值,取这两个值的最小值,
代表这个木桶容纳的水量,但是,这个木桶还可能包括了黑色的柱子,因此我们
还要用这个容积减去黑柱的高度,才能得到还能接的雨水量。
2.前缀最大值的计算为,pre_max[0]=height[0],第i个的pre_max为取第i-1
个的pre_max和第i个的height做比较,取最大值。后缀最大值的计算反向进行,
suf_max[-1]=height[-1],第i-1个suf_max为取第i个的suf_max和第i-1个的
height做比较,取最大值。
3.计算能接的雨水数。桶能装的水取决于前缀最大值和后缀最大值的最小值,
桶能装的水减去柱子的高度即height,则为能接的雨水数,可以理解为桶的
底部被柱子所占,桶还剩余的空间即为接水量。

时间复杂度:O(n)。只是对长度为n的数组遍历了常数次而已。
空间复杂度:O(n)。创建了前缀最大值数组和后缀最大值数组。
"""
class Solution:
    def trap(self, height: List[int]) -> int:
        ans = 0
        length = len(height)
        pre_max = [0] * length
        suf_max = [0] * length
        # 计算前缀最大值
        pre_max[0] = height[0]
        for i in range(1, length):
            pre_max[i] = max(pre_max[i-1], height[i])
        # 计算后缀最大值
        suf_max[-1] = height[-1]
        for i in range(length-2, -1, -1):
            suf_max[i] = max(suf_max[i+1], height[i])
        # 计算i从0到length-1的每个位置能容纳的雨水数
        for pre, suf, h in zip(pre_max, suf_max, height):
            ans += min(pre, suf) - h
        return ans

法二(时间复杂度:O(n),空间复杂度:O(1))

"""
思路:为了将空间复杂度从O(n)优化成O(1),必须利用一些其他的性质。计算接水量的
核心步骤依然是,桶的容积减去柱子高度,而桶容积取决于前缀最大值和后缀最大值中的
最小值。之前我们是用两个数组来保存前缀最大值和后缀最大值,最后再遍历一次计算。
因此我们要优化这一部分,用两个变量来储存,达到一边储存,一边计算的效果,而不是
先储存完,最后计算。
因为前缀最大值由前往后是非递减的,后缀最大值由后往前也是非递减的,之前的计算是
我们遍历i,计算第i个的min(pre_max[i], suf_max[i])-height[i],但是事实上
我们不需要等到pre_max[i], suf_max[i]都知道后才能计算出min,比如当我们计算
第5个的min时(假设长度很长,比如20),如果我们已经知道pre_max[5]=2,
而suf_max[-2]=7,根据非递减性质,那么suf_max[5]的值肯定是大于pre_max[5]的,
那么我们可以直接知道min值为pre_max[5]。利用这个性质,我们可以初始化left,
right在左右两边,每次先计算当前的前后缀最大值,直接拿这两个前后缀最大值进行比较,
再移动left或right,我们每次必定可以在左或右其中一个方向计算出一个min,从而计算
出该位置能接的雨水数,最后把这些雨水数累加即可。

时间复杂度:O(n)
空间复杂度:O(1)
"""
class Solution:
    def trap(self, height: List[int]) -> int:
        ans = 0
        left = 0
        right = len(height) - 1
        pre_max = 0
        suf_max = 0
        while left <= right:
            pre_max = max(pre_max, height[left])
            suf_max = max(suf_max, height[right])
            if pre_max < suf_max:
                ans += pre_max - height[left]
                left += 1
            else:
                ans += suf_max - height[right]
                right -= 1
        return ans

滑动窗口

与相向双指针对应,其实滑动窗口可以理解为同向双指针

  1. 209. 长度最小的子数组
"""
思路:滑动窗口思想,核心思想是当序列和满足>=target时,要将left右移,直到不满足>=target,
当不满足时,要将right右移直到满足>=target

时间复杂度:O(n)
空间复杂度:O(1)
"""
class Solution:
    def minSubArrayLen(self, target: int, nums: List[int]) -> int:
        ans = len(nums) + 1
        left = 0
        s = 0
        # 写法一
        for right, x in enumerate(nums):
            s += x
            while s >= target:
                ans = min(ans, right - left + 1)
                s -= nums[left]
                left += 1

        # # 写法二
        # for right, x in enumerate(nums):
        #     s += x
        #     while s - nums[left] >= target:
        #         s -= nums[left]
        #         left += 1
        #     if s >= target:
        #         ans = min(ans, right - left + 1)

        # # 写法三
        # right = 0
        # while left <= right and right <= len(nums):
        #     if s < target and right < len(nums):
        #         s += nums[right]
        #         right += 1
        #     elif s < target and right == len(nums):
        #         break
        #     else:
        #         ans = min(ans, right - left)
        #         s -= nums[left]
        #         left += 1

        if ans == len(nums) + 1:
            return 0
        else:
            return ans
  1. 3. 无重复字符的最长子串
"""
思路:
考虑一般化的情况,当已有一个无重复子串时,在其后面添加一个字符ch,他可能会导致不满足无重复了,
而且一定是由ch这个字符的重复导致的问题,那么当遇到这种情况时,我们需要一直右移left,直到
ch这个字符的出现次数仅为1,而当我们正常添加了一个无重复的字符时,则计算其长度并与ans比较,
right右移,这里用遍历,就相当于right一直在右移了。

时间复杂度:O(n)。左右指针最多各移动n次。
空间复杂度:O(1)。尽管使用了哈希表,但是这题的字符个数是有限的。
"""
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        ans = 0
        left = 0
        cnt = dict()
        for right, ch in enumerate(s):
            if ch not in cnt:
                cnt[ch] = 1
            else:
                cnt[ch] += 1
            while cnt[ch] > 1:
                cnt[s[left]] -= 1
                left += 1
            ans = max(ans, right - left + 1)
        return ans
  1. 713. 乘积小于 K 的子数组
"""
思路:
滑动窗口思想,需要注意计算ans时不是加1,而是加上right-left+1。为什么是r-l+1呢?
假如此时序列为l...temp...r,此时满足这些数的乘积小于k,注意此时右端点r是固定的,
题目要求连续子数组,则[l,r],[l+1,r],[l+2,r],[r,r](即[r]本身)都是满足的,
共有r-l+1个。

时间复杂度:O(n)
空间复杂度:O(1)
"""
class Solution:
    def numSubarrayProductLessThanK(self, nums: List[int], k: int) -> int:
        if k <= 1:
            return 0
        ans = 0
        left = 0
        # 乘积的英文为product,商的英文为quotient
        product = 1
        for right, x in enumerate(nums):
            product *= x
            while product >= k:
                product /= nums[left]
                left += 1
            if product < k:
                # 最容易错的一步,不是加1,因为乘数越少肯定越“小”
                ans += right - left + 1
        return ans
  1. 2958. 最多 K 个重复元素的最长子数组
"""
思路:
考虑一般化的情况,假设给一个序列后面新增一个数字x,若引起大于k的条件,则一定是这个
数字x的数量大于k了,因此我们首先新增这个数x,方法是在哈希表中将这个key的value加1,
然后做一个循环,不断右移left直到保证这个序列是符合条件的,然后取最大值即可。

时间复杂度:O(n)。左右指针最多各移动n次。
空间复杂度:O(n)。与之前滑动窗口第2题不同,这里的哈希表可能的数量为n,而不是有限。
"""
class Solution:
    def maxSubarrayLength(self, nums: List[int], k: int) -> int:
        ans = 0
        left = 0
        cnt = dict()
        for right, x in enumerate(nums):
            if x not in cnt:
                cnt[x] = 1
            else:
                cnt[x] += 1
            while cnt[x] > k:
                cnt[nums[left]] -= 1
                left += 1
            ans = max(ans, right - left + 1)
        return ans
  1. 2730. 找到最长的半重复子字符串
"""
思路:
考虑破坏掉半重复性质的条件,一定是新加入的字符ch,与他前一个的字符s[right-1]相等。导致有两对相邻
字符是相等的,因此当遇到这种情况时,我们不断右移left,直到s[left]==s[left-1],即减少了一对相等的
相邻字符,让相等的相邻字符数量为1,然后计算max即可。

时间复杂度:O(n)。左右指针最多各移动n次。
空间复杂度:O(1)。只用了常量的辅助空间。
"""
class Solution:
    def longestSemiRepetitiveSubstring(self, s: str) -> int:
        ans = 0
        cnt = 0
        left = 0
        for right, ch in enumerate(s):
            if right == 0:
                ans = max(ans, right - left + 1)
            else:
                if ch == s[right-1]:
                    cnt += 1
            while cnt > 1:
                left += 1
                if s[left] == s[left-1]:
                    cnt -= 1
                    break
            ans = max(ans, right - left + 1)

        return ans
  1. 2779. 数组的最大美丽值
"""
思路:
问题的关键在于首先要理解,对于数组的每个数x,可以变为[x-k,x+k]的任意一个数。
并且问题的长度的判定,仅仅是相同数的个数,没有连续的要求。因此我们首先对数组排序,
对于每个数x,实际上相当于[x-k,x+k],那么这个美丽值,其实就是这些区间的最大相交数。
对于两个区间(用数x,y来代替)是否相交,只需要x+k>=y-k即可,而在他们之间的这些区间,
肯定也是与这两个区间相交的。上述式子可以改写成y-x<=2k,也就是满足这个条件时,美丽值就是
right-left+1。因此我们写个循环,控制每次都满足该式子,然后计算最大ans即可。
注意left越向右,肯定是越满足这个式子的,而right越向右,肯定是越不满足这个式子的。

时间复杂度:O(nlogn)。用了快排。
空间复杂度:O(logn)。算快排的空间复杂度的话。O(1),不算快排的空间复杂度的话。
"""
class Solution:
    def maximumBeauty(self, nums: List[int], k: int) -> int:
        ans = 0
        left = 0
        nums.sort()

        for right, x in enumerate(nums):
            while x - nums[left] > 2 * k:
                left += 1
            ans = max(ans, right - left + 1)
        return ans
  1. 1004. 最大连续 1 的个数 III
"""
思路:乍一看可能和滑动窗口无关,这是因为顺着题目的思路导致的。换一个角度想,既然
我们有能将k个0变成1的能力,统计的是最长连续1的序列的长度,那么我们可以想成在一个
滑动窗口中,最多出现k个0,这个滑动窗口的最大长度就是我们要求的答案。因为这个滑动
窗口中的0我们都能变成1,那么整个滑动窗口就都是1了。

时间复杂度:O(n)
空间复杂度:O(1)
"""
class Solution:
    def longestOnes(self, nums: List[int], k: int) -> int:
        ans = 0
        cnt0 = 0
        left = 0
        for right, x in enumerate(nums):
            if x == 0:
                cnt0 += 1
            if cnt0 > k:
                if nums[left] == 0:
                    cnt0 -= 1
                left += 1
            ans = max(ans, right - left + 1)
        return ans
  1. 2962. 统计最大元素出现至少 K 次的子数组
"""
思路:首先明确,题目中的子数组指的是连续的元素序列。比较能容易想到这题要用滑动窗口
的思想,但是对于计算子数组的数量,比较难以确定,这里思路是,新增right后,判断是否
等于k个,若等于,则ans+=len-right,然后右移left直到小于k,即等于k-1个。这样
处理的原因是,若子序列[a...b...c...d]中[a...b...c]满足条件,则d-c+1个子数组
均满足条件,包括[a...b...c,c+1],[a...b...c,c+1,c+2],...[a...b...c...d]。

时间复杂度:O(n)
空间复杂度:O(1)
"""
class Solution:
    def countSubarrays(self, nums: List[int], k: int) -> int:
        ans = 0
        left = 0
        max_element = max(nums)
        cnt = 0
        for right, x in enumerate(nums):
            if x == max_element:
                cnt += 1
            while cnt == k:
                ans += len(nums) - right
                if nums[left] == max_element:
                    cnt -= 1
                left += 1
        return ans
  1. 2302. 统计得分小于 K 的子数组数目
"""
思路:只能说观察出是滑动窗口方法,然后猜测数学原理

时间复杂度:O(n)
空间复杂度:O(1)
"""
class Solution:
    def countSubarrays(self, nums: List[int], k: int) -> int:
        ans = 0
        left = 0
        s = 0
        for right, x in enumerate(nums):
            s += x
            while s * (right - left + 1) >= k:
                s -= nums[left]
                left += 1
            ans += right - left + 1
        return ans
  1. 1658. 将 x 减到 0 的最小操作数
"""
思路:题目想问的就是取一个前缀和一段后缀,他们两个的和加起来等于x。
因此我们可以转换思路,因为中间的序列是连续的,我们可以把中间的序列
看成一个滑动窗口,当其和等于sum(nums)-x时,满足题意。需要注意的是
当数组和小于x时,要处理这个特殊情况,直接返回-1,否则后面会死循环。

时间复杂度:O(n)
空间复杂度:O(1)
"""
class Solution:
    def minOperations(self, nums: List[int], x: int) -> int:
        ans = len(nums) + 1
        left = 0
        if sum(nums) < x:
            return -1
        s1 = 0
        s2 = sum(nums) - x
        for right, x in enumerate(nums):
            s1 += x
            while s1 > s2:
                s1 -= nums[left]
                left += 1
            if s1 == s2:
                ans = min(ans, len(nums) - (right - left + 1))
        if ans == len(nums) + 1:
            return -1
        else:
            return ans
  1. 1234. 替换子串得到平衡字符串
"""
思路:首先明确这个字符串只含有四种字符,其次他的长度一定是4的倍数。对于这个字符串,
我们分成两部分,一部分取其中的连续子串s1,剩下一部分为s2(注意这部分不一定连续)。
若s1以外的部分,他其中的一种字符数量已经超过了n/4,那么无论我们怎么替换子串s1,
这个字符的数量都是大于n/4的,因此不满足题意,若以外的部分中的每一种字符的数量都
小于等于n/4,并且联系已知这个字符串长度一定是4的倍数,那么我们可以通过替换子串s1,
来达到满足题意的效果。使用哈希表记录s1以外部分,每种字符的出现次数,先判断初始特殊情况,
然后遍历右指针right,其中再不断右移left,进行求解。


时间复杂度:O(n)。
空间复杂度:O(1),因为只有4种字符,使用的哈希表空间为常量。
"""
import numpy as np
class Solution:
    def balancedString(self, s: str) -> int:
        cnt = dict()
        for ch in s:
            if ch not in cnt:
                cnt[ch] = 1
            else:
                cnt[ch] += 1
        m = len(s) // 4
        if len(cnt) == 4 and min(cnt.values()) == m:
            return 0
        ans = np.inf
        left = 0
        for right, ch in enumerate(s):
            cnt[ch] -= 1
            while max(cnt.values()) <= m:
                ans = min(ans, right - left + 1)
                cnt[s[left]] += 1
                left += 1
        return ans
  1. 76. 最小覆盖子串

二分查找

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

"""
思路:
首先要实现一个函数lower_bound,他能够返回使得nums[i]>=k的最小的i,
如果都不满足,则返回数组长度。
无论是求开始位置还是结束位置,都可以用lower_bound解决,当求开始位置时,只需要
设置k为target本身,当求结束位置时,设置k为target+1,求得的值再减1即为结束位置。

时间复杂度:O(logn)
空间复杂度:O(1)
"""
def lower_bound(nums: List[int], target: int) -> int:
    left = 0
    right = len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return left

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        start = lower_bound(nums, target)
        if start == len(nums) or nums[start] != target:
            return [-1, -1]
        end = lower_bound(nums, target + 1) - 1
        return [start, end]

2.2529. 正整数和负整数的最大计数

"""
思路:求负整数的个数,就将lower_bound中的target设为0,即为答案
求正整数的个数,就将target设为1,再用len(nums)减去这个数即可。

时间复杂度:O(logn)
空间复杂度:O(1)
"""
def lower_bound(nums: List[int], target) -> int:
    left = 0
    right = len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return left

class Solution:
    def maximumCount(self, nums: List[int]) -> int:
        positive = lower_bound(nums, 0)
        negative = len(nums) - lower_bound(nums, 1)
        return max(positive, negative)

3.2300. 咒语和药水的成功对数

"""
思路:最普通的方法是遍历这两个数组,这样时间复杂度是O(mn)。对于spells[i],
若能与potions[j]进行配对,则他们两个相乘要大于等于success,也就是相当于,
对于potions里的每个元素,只要满足大于等于success//spells[i]的向上取整的值,
就可以与spells[i]进行配对,因此我们可以先将potions进行排序,然后再用二分查找。

时间复杂度:O(nlogn+mlogn)即O((n+m)logn)。其中m为spells长度,n为potions长度。快排为O(nlogn),
之后对spells遍历,外层为m,内层对potions进行二分,为logn。
空间复杂度:O(logn)。快排空间复杂度为O(logn)。
"""
def lower_bound(nums: List[int], target: int) -> int:
    left = 0
    right = len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return left

class Solution:
    def successfulPairs(self, spells: List[int], potions: List[int], success: int) -> List[int]:
        ans = []
        potions.sort()
        for i in spells:
            x = (success - 1) // i + 1
            start = lower_bound(potions, x)
            if start == len(potions):
                ans.append(0)
            else:
                ans.append(len(potions) - start)
        return ans

4.2563. 统计公平数对的数目

"""
思路:这题无非就是先对数组排序,然后遍历这个数组,固定一个数,对他之后的数组进行
二分查找,但是对于二分查找的实现方式,与前面几题不同,需要做一下重构。因为如果新建
一个数组nums2=nums[index+1:len(nums)],需要注意这是一个O(n)的复制,外层遍历
叠加起来,时间复杂度就成了O(n^2)。因此不妨在二分查找算法上,传入一个start和end,
就省去了复制数组的时间,使得时间复杂度为O(nlogn)

时间复杂度:O(nlogn)
空间复杂度:O(logn)
"""
def lower_bound(nums: List[int], target: int, start, end):
    left = start
    right = end
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return left

class Solution:
    def countFairPairs(self, nums: List[int], lower: int, upper: int) -> int:
        nums.sort()
        ans = 0
        for index, value in enumerate(nums):
            if index == len(nums) - 1:
                break
            start1 = lower_bound(nums, lower - value, index+1, len(nums) - 1)
            if start1 == len(nums):
                continue
            start2 = lower_bound(nums, upper - value + 1, index+1, len(nums) - 1)
            ans += start2 - start1
        return ans

5.2080. 区间内查询数字的频率

"""
思路:最简单的做法是暴力,求频率就需要O(n)的时间复杂度,为了优化这个复杂度,
我们需要使用二分查找。用一个哈希表,将每个值的下标列表存起来,此时这个列表
满足非递减的性质,此时再对这个列表进行二分查找,即可得到答案。

时间复杂度:初始化需要O(n),求频率需要O(logn)
空间复杂度:哈希表需要O(n)
"""
from typing import List
from collections import defaultdict
def lower_bound(nums: List[int], target) -> int:
    left = 0
    right = len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return left

class RangeFreqQuery:
    def __init__(self, arr: List[int]):
        # 和dict的区别是,defaultdict需要从collections导入,当访问不存在
        # 的键时,会自动创建一个默认的类型,如果用dict,需要用not in做几次判断处理
        index_map = defaultdict(list)
        for index, value in enumerate(arr):
            index_map[value].append(index)
        self.index_map = index_map

    def query(self, left: int, right: int, value: int) -> int:
        start = lower_bound(self.index_map[value], left)
        if start == len(self.index_map[value]):
            return 0
        end = lower_bound(self.index_map[value], right+1)
        return end - start

6.275. H 指数 II

"""
思路:这题为了要实现O(logn)的时间复杂度,首先不能陷入题目的思路,如果按照
题目的思路,先判断h指数可否为1,可否为2...可否为n,那时间复杂度一定是O(n)。
要转换思路,二分查找,直接判断中间(left+right)/2可否成立,因为若a<b,当h
指数可为b时,一定也可为a。然后就是如何判断h指数可否成立的问题,比如对于一个列表
[1,2,3,4,5,6],我们想判断h指数可否为3,也就是有没有3个数大于等于3,也就是说
我们可以直接取倒数第3个数,若这个数大于等于3,又根据单调性,肯定能有至少3个数
大于等于3,那我们进行抽象,h指数可否为mid,就直接判断citations[-mid]是否>=
mid即可,还要注意这里不是返回left,而是返回right,因为当left==right时进行最后
一次二分判断时,若不满足,是将right=mid-1,也就是这个mid不满足,而这个right会
满足,这时候也同时跳出了循环。
[注]:对于取倒数第mid个数citations[-mid],如果改写成citations[len(citations)-mid],
会有错误,因为对于特殊列表[0],mid=0时,会出现错误,其实循环也可以直接从1到len(citations)
开始,因为h指数必可为0,这样无论用哪种写法也不会产生这个错误,因为问题关键主要是,取倒数第0个
数这种说法,本身就不太合理。

时间复杂度:O(logn)
空间复杂度:O(1)
"""
class Solution:
    def hIndex(self, citations: List[int]) -> int:
        left = 0
        right = len(citations)
        while left <= right:
            mid = (left + right) // 2
            # if citations[len(citations) - mid] >= mid:
            #     left = mid + 1
            if citations[-mid] >= mid:
                left = mid + 1
            else:
                right = mid - 1
        return right

7.875. 爱吃香蕉的珂珂

8.2187. 完成旅途的最少时间

9.2861. 最大合金数

10.2439. 最小化数组中的最大值

11.2517. 礼盒的最大甜蜜度

二分查找 - 变形

1.162. 寻找峰值

2.153. 寻找旋转排序数组中的最小值

3.33. 搜索旋转排序数组

4.1901. 寻找峰值 II

5.154. 寻找旋转排序数组中的最小值 II

链表 - 反转系列

1.206. 反转链表

2.92. 反转链表 II

3.25. K 个一组翻转链表

4.24. 两两交换链表中的节点

5.445. 两数相加 II

6.2816. 翻倍以链表形式表示的数字

链表 - 快慢指针

1.876. 链表的中间结点

2.141. 环形链表

3.142. 环形链表 II

4.143. 重排链表

链表 - 删除系列

1.237. 删除链表中的节点

2.19. 删除链表的倒数第 N 个结点

3.83. 删除排序链表中的重复元素

4.82. 删除排序链表中的重复元素 II

5.203. 移除链表元素

6.3217. 从链表中移除在数组中存在的节点

7.2487. 从链表中移除节点

二叉树与递归 - 深入理解

1.104. 二叉树的最大深度

2.111. 二叉树的最小深度

3.112. 路径总和

4.129. 求根节点到叶节点数字之和

5.1448. 统计二叉树中好节点的数目

6.987. 二叉树的垂序遍历

二叉树与递归 - 灵活运用

1.100. 相同的树

2.101. 对称二叉树

3.110. 平衡二叉树

4.199. 二叉树的右视图

5.226. 翻转二叉树

6.617. 合并二叉树

7.1026. 节点与其祖先之间的最大差值

8.1080. 根到叶路径上的不足节点

二叉树与递归 - 前序/中序/后序

1.98. 验证二叉搜索树

2.938. 二叉搜索树的范围和

3.2476. 二叉搜索树最近节点查询

4.1373. 二叉搜索子树的最大键值和

5.105. 从前序与中序遍历序列构造二叉树

6.106. 从中序与后序遍历序列构造二叉树

7.889. 根据前序和后序遍历构造二叉树

8.1110. 删点成林

二叉树与递归 - 最近公共祖先

1.236. 二叉树的最近公共祖先

2.235. 二叉搜索树的最近公共祖先

3.1123. 最深叶节点的最近公共祖先

二叉树 - BFS

1.102. 二叉树的层序遍历

2.103. 二叉树的锯齿形层序遍历

3.513. 找树左下角的值

4.107. 二叉树的层序遍历 II

5.116. 填充每个节点的下一个右侧节点指针

6.117. 填充每个节点的下一个右侧节点指针 II

7.2415. 反转二叉树的奇数层

8.2641. 二叉树的堂兄弟节点 II

回溯 - 子集型

1.17. 电话号码的字母组合

2.78. 子集

3.131. 分割回文串

回溯 - 组合型与剪枝

1.77. 组合

2.216. 组合总和 III

3.22. 括号生成

4.39. 组合总和

回溯 - 排列型

1.46. 全排列

2.51. N 皇后

3.52. N 皇后 II

4.2850. 将石头分散到网格图的最少移动次数

动态规划 - 从记忆化搜索到递推

1.198. 打家劫舍

2.70. 爬楼梯

3.746. 使用最小花费爬楼梯

4.377. 组合总和 Ⅳ

5.2466. 统计构造好字符串的方案数

6.2266. 统计打字方案数

7.213. 打家劫舍 II

8.LCR 166. 珠宝的最高价值

0-1 背包 完全背包 至多/恰好/至少

1.494. 目标和

2.322. 零钱兑换

3.2915. 和为目标值的最长子序列的长度

4.416. 分割等和子集

5.518. 零钱兑换 II

6.279. 完全平方数

最长公共子序列 LCS

1.1143. 最长公共子序列

2.72. 编辑距离

最长递增子序列 LIS

1.300. 最长递增子序列

2.1671. 得到山形数组的最少删除次数

3.1626. 无矛盾的最佳球队

状态机 DP - 买卖股票系列

1.122. 买卖股票的最佳时机 II

2.309. 买卖股票的最佳时机含冷冻期

3.188. 买卖股票的最佳时机 IV

4.714. 买卖股票的最佳时机含手续费

5.2826. 将三个组排序

6.2786. 访问数组中的位置使分数最大

区间 DP

1.516. 最长回文子序列

2.1039. 多边形三角剖分的最低得分

3.3040. 相同分数的最大操作数目 II

4.1771. 由子序列构造的最长回文串的长度

5.1000. 合并石头的最低成本

树形 DP - 直径系列

1.543. 二叉树的直径

2.124. 二叉树中的最大路径和

3.2246. 相邻字符不同的最长路径

4.687. 最长同值路径

5.3203. 合并两棵树后的最小直径

6.1617. 统计子树中城市之间最大距离

7.2538. 最大价值和与最小价值和的差值

树形 DP - 最大独立集

1.337. 打家劫舍 III

2.1377. T 秒后青蛙的位置

3.2646. 最小化旅行的价格总和

树形 DP - 最小支配集

1.968. 监控二叉树

单调栈

1.739. 每日温度

2.42. 接雨水

3.496. 下一个更大元素 I

4.503. 下一个更大元素 II

5.901. 股票价格跨度

6.1019. 链表中的下一个更大节点

7.1944. 队列中可以看到的人数

8.84. 柱状图中最大的矩形

9.1793. 好子数组的最大分数

单调队列

1.239. 滑动窗口最大值

2.2398. 预算内的最多机器人数目

3.862. 和至少为 K 的最短子数组

4.1499. 满足不等式的最大值

5.1696. 跳跃游戏 VI

6.2944. 购买水果需要的最少金币数

About

根据灵神整理的基础算法题单,代码加上了个人理解的思路,复杂度,用于速查

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy