hot100刷题记录

一、两数之和

题目:https://leetcode.cn/problems/two-sum/description/?envType=study-plan-v2&envId=top-100-liked
方法1:枚举

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        for id, num in enumerate(nums):
            for idx, num_1 in enumerate(nums[id+1:]):
                if num + num_1 == target:
                    return [id, id + idx + 1]

两个指针,外部循环遍历全部列表,内部循环从原始列表的第二个数开始遍历,枚举所有可能的情况,判断是否等于目标数。

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        for i in range(len(nums)):
            for j in range(i + 1, len(nums)):
                if i != j and nums[i] + nums[j] == target:
                    return [i, j]
        return []

方法2:哈希表
新建一个字典,如果目标值减去当前值的差在字典里,那就说明存在这两个数之和等于目标值。如果没有,那就将当前值添加进字典。
遍历数组,对于每一个数 nums[i]nums[i]nums[i]:
先查找字典中是否存在 target−nums[i]target - nums[i]target−nums[i],存在则输出 target−nums[i]target - nums[i]target−nums[i] 对应的下标和当前数组的下标 iii。
不存在则在字典中存入 target−nums[i]target - nums[i]target−nums[i] 的下标 i。

def twoSum(self, nums: List[int], target: int) -> List[int]:
    numDict = dict()
    for i in range(len(nums)):
        if target-nums[i] in numDict:
            return numDict[target-nums[i]], i
        numDict[nums[i]] = i
    return [0]
  1. 首先,创建一个空字典 _dict 用来存储数组中每个元素的值和它们对应的索引。这样做的目的是为了快速查找数组中是否存在某个特定的值。
  2. 然后,使用一个循环遍历数组 nums 中的每一个元素。对于当前元素 nums[i],算法尝试找到一个数 x,使得 x + nums[i] = target。为了找到这样的数 x,我们可以将等式重写为 x = target - nums[i],然后查找 x 是否存在于之前遍历过的元素中。
  3. if target - nums[i] in _dict 这行代码就是检查 target - nums[i] 是否作为一个键(key)存在于字典 _dict 中。如果存在,这意味着我们找到了两个数 nums[i]x = target - nums[i],它们的和为 target
  4. 如果找到这样的一个数,return [_dict[target - nums[i]], i] 会返回这两个数的索引。其中 _dict[target - nums[i]] 是之前存储在字典中的与 x 相对应的索引,而 i 是当前 nums[i] 的索引。
    为什么要将减的值(即 x 对应的索引)放在前面,而将 i 放在后面呢?这是因为在遍历数组时,当前元素 nums[i] 的索引 i 总是大于或等于 x 对应的索引(因为 x 是在 nums[i] 之前出现的)。所以,当我们找到满足条件的一对数时,返回它们的索引时,x 对应的索引是较小的,而 i 是较大的,按照常规逻辑,我们习惯将较小的索引放在前面。
    这种方法的关键在于利用哈希表(即 Python 中的字典)来实现快速查找,这使得整个算法的时间复杂度降低到 O(n),其中 n 是数组 nums 的长度。

二、49. 字母异位词分组

题目链接地址
建立哈希表(字典),将相同的字母异位词有序化,作为键,变体作为值存储在列表里,最后取出字典的值放在列表里。

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        _dict = {}

        for i in strs:
            n_i = ''.join(sorted(i)) # # 对字符串排序,作为字典的键
            if n_i in _dict:
                _dict[n_i].append(i) # 如果键已存在,添加到对应的列表中
            else:
                _dict[n_i] = [i] # 如果键不存在,创建一个新的键并初始化列表

        return [i for i in _dict.values()] # 返回字典中所有值的列表
        # return list(_dict.values())  # 返回字典中所有值的列表

三、128. 最长连续序列

地址
思路:随机选择一个数,如果该数-1的值不存在于列表中,说明该数字不是有序数列中的中间和后面,是开头;
找到开头,再往后找,也就是加1,如果存在,那就给当前序列长度加一,然后继续往后找,直到不存在为止;
可能有很多连续序列,不可能每个序列长度都存储到一个序列中,所以要找到全局最长,每次和局部最长对比,取最长,这样最后就是全局最长的了。

def longestConsecutive(nums):
    # 创建一个集合,用于存储数组中的所有不重复的数字
    num_set = set(nums)
    longest = 0  # 初始化最长连续序列的长度为0

    # 遍历数组中的每个数字
    for num in nums:
        # 检查当前数字的前一个数字是否不在集合中
        # 如果不在,说明当前数字可能是一个连续序列的起点
        if num - 1 not in num_set:
            current_num = num  # 当前正在检查的数字
            current_streak = 1  # 当前连续序列的长度

            # 继续向后查找当前数字之后的连续数字
            while current_num + 1 in num_set:
                current_num += 1  # 移动到下一个数字
                current_streak += 1  # 增加连续序列的长度

            # 更新最长连续序列的长度
            longest = max(longest, current_streak)

    # 返回最长连续序列的长度
    return longest

四、283. 移动零

地址

快慢指针,快指针每次走一步,慢指针只在某种条件成立时才走一步。慢指针指向当前应该被替换的位置
条件:快指针遇到了非0值,将其往前替换,也就是跟慢指针的值交换,使得非0往左移动,0往右移动

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        # 定义慢指针
        slow = 0
        # 快指针fast遍历列表
        for fast in range(len(nums)):
            # 当快指针位置不是0
            if nums[fast] != 0:
                # 替换,将当前快指针位置交换慢指针的值
                nums[fast], nums[slow] = nums[slow], nums[fast]
                # 交换过了,当前慢指针是非0值,所以往前走一步
                slow += 1

        return nums

循环过程:
遍历数组时,fast指针负责寻找非零元素。
当fast指针指向的元素不为0时,我们需要将其移到slow指针的位置。这是因为slow指针左侧的所有元素都已经处理过,即它们非零且保持了原有顺序。
交换nums[fast]和nums[slow]之后,我们增加slow指针的值,以指向下一个可能的替换位置。

五、11. 盛最多水的容器

地址
思路:对撞指针。一个从前往后,一个从后往前,计算两者间的面积。判断两个指针位置的高度,哪个低就把哪个指针移动。更新面积。退出条件:指针相遇。
**双指针必要条件:**要有终止条件(一般是两个指针相遇),未达到终止条件时要不断循环,在某个条件下移动这两个指针

本题本质上是 数组长度-1 * 最短高度 的最值问题,需要找到连续最优子数组和子最短高度
在这里插入图片描述

class Solution:
    def maxArea(self, height: List[int]) -> int:
        # 双指针 对撞指针
        pre = 0 # 前指针
        end = len(height) - 1 # 后指针
        marea = 0 # 最大面积

        # 当指针没遇到时,循环执行
        while pre < end:
            # 计算宽度,短板效应,选择两者间最短的
            y = (height[pre] if height[pre] < height[end] else height[end])
            # 计算长,指针位置的差
            x = end - pre # 长
            # 计算当前面积
            area = x * y
            
            # 更新最大面积
            if marea < area:
                marea = area
            
            # 如果左指针的值小于右边,就往右走一步
            if height[pre] < height[end]:
                pre += 1
            # 否则,右指针左移
            elif height[pre] >= height[end]:
                end -= 1
        # 从两个端点出发,考虑到所有可能的最优解,并记录过程中的最大值
        return marea

六、15. 三数之和

地址
最先想是直接三层遍历,但是一想就不可能是这样,太慢了。
依旧是对撞指针。
三数之和,如果把一个数固定,那就是两数之和。本题是双指针,可以用两个指针来指向另外两个数,成功实现降维。
先将数组进行排序,以保证按顺序查找时,元素值为升序,从而保证所找到的三个元素是不重复的。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        res = []

        # 排序
        # 如果有重复元素,就可以避过
        a = sorted(nums)


        for i in range(len(a)):
            # 将i的情况放在大于0的情况,防止溢出
            # 如果遇到重复元素,就跳过,因为题目不让重复
            if i >0 and a[i] == a[i-1]:
                continue
            
            # 定义左右指针
            l, r = i+1, len(a)-1
            # 当指针未相撞时,执行循环
            while l < r:
                # 如果当前遍历的位置和左右指针的值和为1,表明找到解了,添加到答案,然后移动左右指针
                if a[i] + a[l] + a[r] == 0:
                    res.append([a[i], a[l], a[r]])
                    l += 1
                    r -= 1
                # 如果说小于0,因为我们是排序了的,左边肯定比右边小,i又不能变,所以肯定是l太靠左了,加一右移
                elif a[i] + a[l] + a[r] < 0:
                    l += 1
                # 反之就是右边太大,左移
                else:
                    r -= 1
        # 虽然说上面是跳过了重复元素,但是也存在两个重复元素和其他的重复的都存在,导致有多个解是相同的
        # 将答案的每个元素排序后转为元组,因为列表不可哈希,用不了集合。集合去重
        unique_tuples = set(tuple(sorted(sublist)) for sublist in res)  # 去重并转换为集合
        nr = [list(tup) for tup in unique_tuples]  # 将元组转换回列表   

        return(nr)

七、42. 接雨水

地址
双指针解法。
最先的想法是找到每个子池子,先从左往右,直到找到池子,就计算池子水,完成后将右指针移动到当前左指针的位置,然后把左指针重置为0。还弄了个公式,但是还有近40个用例没通过。原因是height = [5,5,4,7,8,2,6,9,4,5] 中会直接右指针指向8,这样导致再也找不到池子。
代码如下:

class Solution:
    def trap(self, height: List[int]) -> int:
        if len(height) < 3:
            return 0
        # 初始化左右指针
        left, right = 0, len(height)-1
        area = 0
        # 设置终止条件,如果差为1,代表只有俩柱子,不行
        while right - left != 1 and left < right:
            # 如果形成了一个池
            if height[left] >= max(height[left+1:right]) and height[right] >= max(height[left+1:right]):
                # 找到短板
                y = height[left] if height[left]<=height[right] else height[right]
                temp_area = (right-left-1) * y - sum([i if i <= y else y for i in height[left+1:right]])
                area += temp_area # 累加
                # 最开始就是一个池子
                if left == 0 and right == len(height)-1:
                    break
                else: # 将right移动至left,重置left为0
                    right, left = left, 0
            elif height[right] <= height[right-1]:
                right -= 1
            else:
                left += 1
        
        return area

不应该这样找子池子,而是应该找每个柱子上的积水,就不存在这些问题。
关键点在于怎么移动左右指针和计算每个柱子的积水量。
每个柱子积水量,取决于两边的柱子和自己的高度,如果两边的柱子都比自己高,那自己肯定是接水的。而且接水量是两端最短的柱子减去自己的高度。
扩展到一系列柱子,计算得到左右两边柱子的最高值,如果左边柱子比右边柱子低,那就以左边柱子为边界计算积水量;否则以右边。
如果说左边的初始高度低于右边,则先移动左指针,如果不低于,那就移动右指针。这样就能成功遍历所有位置。

class Solution:
    def trap(self, height):
        # 如果没有柱子或者小于3个,那就无法存贮水
        if not height or len(height) < 3:
            return 0
        # 初始化左右指针
        left, right = 0, len(height) - 1
        # 初始化左右边遇到的最大值
        left_max, right_max = height[left], height[right]
        # 接水量
        water_trapped = 0

        # 当双指针不相遇时
        while left < right:
            # 当左边柱子比右边低
            if height[left] < height[right]:
                # 如果当前柱子比最高还高,更新左边的最高值
                if height[left] >= left_max:
                    left_max = height[left]
                # 否则,认定为有积水,积水量等于最高值减去当前的柱子高度
                else:
                    water_trapped += left_max - height[left]
                # 左边柱子低,往右移动
                left += 1
            # 当右边柱子比左边低
            else:
                # 如果当前柱子比最高还高,更新右边的最高值
                if height[right] >= right_max:
                    right_max = height[right]
                # 否则,认定为有积水,积水量等于最高值减去当前的柱子高度
                else:
                    water_trapped += right_max - height[right]
                # 右边柱子低,往左移动
                right -= 1

        return water_trapped

八、3. 无重复字符的最长子串

地址

双指针,先用快指针遍历所有,使用哈希表记录该字符个数,如果大于1,则慢指针右移,相应记录在哈希表值减1.维护出一个字符数量均为1的队列。

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        # 初始化快慢指针,最大长度,记录字符数的哈希表
        slow, fast, res = 0,0,0
        win = {}

        # 当快指针还没跑完
        while fast < len(s):
            # 见一个字符就加进哈希表
            if s[fast] not in win:
                win[s[fast]] = 1
            else:
                win[s[fast]] += 1

            # 如果遇到的快指针位置的字符数量大于1了,就把慢指针往右移动,同理把最左边的,也就是慢指针位置的字符数量减1
            while win[s[fast]] > 1:
                win[s[slow]] -= 1
                slow += 1

            # 更新最大长度,是快慢指针的差+1,求的是当前窗口长度
            res = max(res, fast - slow + 1)

            fast += 1

        return res

九、438. 找到字符串中所有字母异位词

地址
字母异位词的判断:最简单的方法是排序比较字符串,但是存在问题是时间复杂度太高;采用哈希表方式,分别记录目标字符串的字符、数量和滑动窗口的字符、数量,相等即是异位。
思路:定义两个哈希表,分别记录目标字符串和窗口内字符串的字符和数量;定义快慢指针,初始化为0.
使用快指针遍历完所有字符。在这个过程中,维护滑动窗口大小和目标子串长度一致。如果一致,那就判断哈希表是否相等,相等就把慢指针的索引添加到结果;如果不一致,就滑动窗口,首先是哈希表中的慢指针的键的值-1,如果值变成了0,就删除该键,然后移动慢指针。 再每次循环都移动快指针。

class Solution:
    def findAnagrams(self, s: str, p: str):
        
        # 答案
        res = []
        # 目标子串的哈希表
        dict_p = {}
        for i in p:
            if i not in dict_p:
                dict_p[i] = 1
            else:
                dict_p[i] += 1

        # 滑动窗口的哈希表
        dict_win = {}
        # 快慢指针
        fast, slow = 0, 0

        # 当快指针没有遍历完字符串
        while fast < len(s):
            # 把快指针位置的值加入窗口
            if s[fast] not in dict_win:
                dict_win[s[fast]] = 1
            else:
                dict_win[s[fast]] += 1
            
            # 如果当前窗口大小大于等于目标子串大小
            if fast - slow + 1 >= len(p):
                # 如果说这两个哈希表相等,说明已经OK了,记录开始索引
                if dict_win == dict_p:
                    res.append(slow)
                
                # 将最早的索引元素移除,先将计数减1
                dict_win[s[slow]] -= 1
                # 如果值为0,就直接删除键
                if dict_win[s[slow]] == 0:
                    del dict_win[s[slow]]
                # 将慢指针右移一位
                slow += 1
            # 每轮都移动快指针
            fast += 1
        return res

十、560. 和为 K 的子数组

地址
思路:前缀和
最开始还在想用滑动窗口,但是没做出来
正确思路:关键在于怎么判断连续子数组的和是k,使用前缀和法,维护一个哈希表记录前缀和的值和出现次数。当某一个地方的前缀和-k存在于这个哈希表的y位置(键为y),就说明从y这个位置开始到当前为止的总和是等于k的,所以把这个位置的值(出现的次数)累加即可。
举个例子,对于数组[1,2,3,4,5],
前缀和数组是[0,1,3,6,10,15],这里的0是提前加的为了防止开头元素到某一地方就是k的情况。
3-3=0,0在哈希表对应的值为1,结果加1;6-3=3,3在哈希表的值为1;因此总和为2.覆盖了所有可能的情况。

class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        # 初始化哈希表,增加第一个前缀和
        _dict = dict()
        _dict[0] = 1
        # 前缀和变量
        pre = 0
        # 结果
        res = 0

        for i in nums:
            # 前缀和
            pre += i
            # 如果当前前缀和-k存在于哈希表,就在结果累加对应的次数
            if pre - k in _dict:
                res += _dict[pre-k]
            # 否则,将前缀和放在哈希表中
            if pre in _dict:
                _dict[pre] += 1
            else:
                _dict[pre] = 1

        return res

十一、239. 滑动窗口最大值

地址
双端队列(Deque)
双端队列是队列的一种扩展形式,允许你从队列的两端进行入队和出队操作。这就像是一个可以从两头进出的管道,使得双端队列比普通队列更灵活。在Python中,collections.deque是实现双端队列的一个类。

双端队列解决滑动窗口最大值
在滑动窗口最大值问题中,双端队列能够帮助我们维持一个当前窗口最大值的索引,同时保证队列中的元素是按照从大到小的顺序排列的。这样,队列的队首始终是当前窗口的最大值。以下是关键步骤的详细解释:
初始化:创建一个空的双端队列(deq)用于存储元素的索引,以及一个结果列表(res)。
维护双端队列:
清理过期的元素索引:如果队列中队首的元素索引已经从窗口滑出(即索引小于当前索引i减去窗口大小k),我们就从队首移除它。
保持队列降序:如果当前元素大于队列中的某些元素,那么这些元素将不可能是任何将来窗口的最大值(因为当前元素更大且还在窗口中),所以我们需要移除那些比当前元素小的所有元素的索引。
滑动窗口:
对于每次滑动,我们添加当前元素的索引到双端队列的末尾(在进行了上述清理后)。
一旦窗口形成(即i >= k - 1),队列的队首就是当前窗口的最大值的索引。我们将这个最大值添加到结果列表中。

from collections import deque

class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        if not nums:
            return []
        if k == 1:
            return nums
        
        def clean_deque(i):
            # 删除所有小于当前元素的索引
            while deq and nums[i] > nums[deq[-1]]:
                deq.pop()
            # 删除已经滑出窗口的元素索引
            if deq and deq[0] <= i - k:
                deq.popleft()
                
        deq = deque()
        max_index = 0
        # 初始化双端队列
        for i in range(k):
            clean_deque(i)
            deq.append(i)
            # 更新最大值索引
            if nums[i] > nums[max_index]:
                max_index = i
        res = [nums[max_index]]
        
        # 遍历数组
        for i in range(k, len(nums)):
            clean_deque(i)
            deq.append(i)
            res.append(nums[deq[0]])
        return res

十二、76. 最小覆盖子串

地址
思路:滑动窗口。难点在于如何判断当前窗口内子串符合条件,这个关系到窗口是否停止扩展并开始缩小。
方法是通过维护一个哈希表,使用哈希表记录目标字符串的字符和数量,如果窗口内有这个字符,那就给这个字符(键)的值减1,这样如果都是小于等于0,那就OK了。但是这样又牵扯到怎么判断该窗口内哈希表的值都为0,如果遍历那复杂度也太高了,稍微长一点都不行,所以引入一个变量,当哈希表内某个键的在窗口内,那就给这个变量加1,这样如果窗口内都包含目标字符串的话,这个变量的值就是目标字符串的长度。

整体思路就是:
从开始滑动窗口,左右指针,右指针遍历,维护窗口内哈希表和记录数。
当记录数等于字符串长度,说明该停止扩展窗口了。
开始移动左指针缩小窗口,要注意判断当前左指针位置的字符是不是目标字符串的,如果是,那就要对应值-1,因为要从这滑走了。
然后继续滑动右指针,继续判断是否需要停止扩展窗口。

import collections
class Solution:
    def minWindow(self, s: str, t: str) -> str:
        # 初始化need字典和窗口变量
        need = collections.defaultdict(int)
        # 将目标字符串的字符和数量添加进哈希表
        for c in t:
            need[c] += 1

        # 初始化左右指针和匹配计数器
        i = 0
        min_len = float('inf')  # 用于记录最小窗口的长度
        min_start = 0  # 记录最小窗口的起始位置
        match = 0  # 记录满足需求的字符种类数量

        # 开始扩展右边界j
        for j, c in enumerate(s):
            # 如果遍历到的字符在哈希表内,那就把这个键的值减1
            if c in need:
                need[c] -= 1
                if need[c] == 0:  # 当某个字符需求刚好被满足,以0为界
                    match += 1 # 

            # 当所有字符需求都被满足,尝试缩小窗口
            while match == len(need):
                if j - i + 1 < min_len:  # 更新最小窗口
                    min_len = j - i + 1
                    min_start = i # 更新左指针
                
                # 尝试移动左边界以缩小窗口
                if s[i] in need:
                    need[s[i]] += 1 # 增加值。因为要移动左指针,如果说左指针在的位置是
                    if need[s[i]] > 0:  # 如果移动左边界导致字符不满足需求
                        match -= 1 # 
                i += 1

        # 检查是否找到了满足条件的子串
        if min_len != float('inf'): 
            res = s[min_start:min_start + min_len] # 字符串就是从左指针到最小长度的切片
        else:
            res = ""
        return res
  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

灵海之森

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值