【Hot100刷题计划】Day1 哈希表、双指针、数组……(持续更新)

  • LeetCode Hot 100 是最常被考察的题目集合,涵盖了面试中常见的算法和数据结构问题。
  • 刷 Hot 100可以让你在有限的时间内集中精力解决最常考的问题。
  • 不仅要写出代码,还要理解问题的本质、优化解法和复杂度分析。
  • 多复习多回顾,欢迎大家评论区交流!

从一些看似简单的题目开始

哈希表

在刷题时候,经常体现为字典或者集合。

1.两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

暴力解法: 直接枚举,两个循环。时间复杂度O(n**2),空间复杂度O(1).

哈希解法: 遍历到字典,每次去字典查是否有值能匹配。时间复杂度O(n),空间复杂度O(n).

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        record = {}   # 用于存遍历过的元素
        result = []   # 存放两个结果索引

        for i in range(len(nums)):
            if target-nums[i] not in record:  # 当前元素之前是否有匹配的元素
                record[nums[i]] = i           # 没有则将当前元素放入字典,键为元素值,值为索引
            else:
                result.append(i)              # 匹配上了将两个索引找出,放到结果数组
                result.append(record[target-nums[i]])
        return result

简化版:

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        record = {}   # 用于存遍历过的元素
        for i,x in enumerate(nums):
            if target-x in record:  # 当前元素之前是否有匹配的元素
                return [i, record[target-x]]  # 匹配上了将两个索引找出,放到结果数组 
            record[x] = i   # 保存nums[i]和i

49. 字母异位词分组

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

**思路:**给每个字符串元素排序存起来,放到字典里;遍历就往排序后相同的加。时间复杂度O(n),空间复杂度O(n).

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        d = defaultdict(list) # 创建字典,defaultdict无键时默认空列表

        for s in strs:
            d[''.join(sorted(s))].append(s)  # 按排序后的作为键加入
        return list(d.values()) # 按输出格式要求,把视图转化成列表
  • defaultdict:在访问一个不存在的键时,defaultdict 会创建该键并初始化为一个空列表。
  • d.values() 是Python 中字典对象的方法,用于返回字典中所有值的一个视图对象。它不包括键,只返回字典中存储的值。

128. 最长连续序列

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

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

思路: 用哈希集合方便查找;找序列起点,分别判断有多长。时间复杂度O(n),空间复杂度O(n).

class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        st = set(nums)     # 将所有元素存入集合,快速查找
        ans = 0

        for x in st:
            if x-1 not in st:   # 判断是否是序列起点
                cur_x = x        # 记录下来序列起点
                cur_len = 1       # 起点序列长度至少为1

                while cur_x + 1 in st: # 不断判断之后的数字是否在集合中
                    cur_x += 1         # 序列长度和数字都加1
                    cur_len += 1
                
                ans = max(ans, cur_len) # 更新最长序列长度
        return ans
                        

双指针

283. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

思路: 双指针,快指针遍历,遇到非0即存到慢指针;最后将慢指针后所有空位置为0. 时间复杂度O(n),空间复杂度O(1).

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        slow = fast = 0

        for fast in range(len(nums)):  # 快指针遍历
            if nums[fast] != 0:        # 找到非零元素靠慢指针移动位置
                nums[slow] = nums[fast]
                slow += 1
        for i in range(slow,len(nums)):  # 所有非零元素前移后,剩下位置补0
            nums[i] = 0

另一思路: 找到非零元素后,就将快慢指针位置元素调换。时间复杂度O(n),空间复杂度O(1).

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        i0 = 0
        for i in range(len(nums)):
            if nums[i]:
                nums[i], nums[i0] = nums[i0], nums[i]
                i0 += 1

15. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

思路: 排序数组,固定一个元素,双指针找另外俩元素。注意排除重复元素。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()   # 先排序,方便比较找
        res = []

        for i in range(len(nums)): # 固定一个数
            if i>0 and nums[i] == nums[i-1]:
                continue
            
            target = 0 - nums[i]
            left, right = i+1, len(nums)-1 # 双指针,根据前后相加大小移动

            while left < right:
                sum_2 = nums[left] + nums[right]

                if sum_2 < target:         # 和小,右移左指针
                    left += 1
                elif sum_2 > target:       # 和大,左移右指针
                    right -= 1
                elif sum_2 == target:
                    res.append([nums[i], nums[left], nums[right]])

                    while left < right and nums[left] == nums[left+1]:  # 跳过相同元素
                        left += 1
                    while left < right and nums[right] == nums[right-1]: # 跳过相同元素
                        right -= 1
                    
                    left += 1    # 分别移动指针
                    right -= 1

        return res

类似题目:167. 两数之和Ⅱ-输入有序数组

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        numbers.sort()    # 排序方便比较查找
        left, right = 0, len(numbers)-1

        while left < right:  # 双指针,从前后分别逼近
            sum = numbers[left] + numbers[right]
            if sum < target:
                left += 1
            elif sum > target:
                right -= 1
            elif sum == target:
                return [left+1, right+1]

数组

53. 最大子数组和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。

思路 :记录加上元素后是否大于最大值,大于就更新。如果之前加小于0,对后续没有贡献,重置为0.

class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        result = float("-inf")  # 设置全局最大值为负无穷
        sum = 0                 # 以num结尾的连续子数组的和

        for num in  nums:
            sum += num          # 大于0,则累加新的元素
            if sum > result:    # 累加值超过result,出现新的大和数组
                result = sum 

            if sum < 0:         # 之前的和为负数,无贡献,置为0
                sum = 0
        return result      

56. 合并区间
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。

思路: 按照左边界大小给各区间排序,然后判断是否重叠,重叠则更新右边界。

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort(key = lambda x:x[0]) # 按照左边界排序,方便合并
        res = []

        res.append(intervals[0]) # 先将最小左边界放进来

        for i in range(1, len(intervals)):
            if intervals[i][0] > res[-1][1]:  # 判断是否重叠,不重叠直接放入
                res.append(intervals[i])
            else:
                res[-1][1] = max(res[-1][1], intervals[i][1]) # 有重叠则比较右边界,更新

        return res

链表

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

思路: 分别用cur、pre指针,改变链表方向反转。

# Definition for singly-linked list.
# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
        cur = head  # 指针cur
        pre = None  # 指针pre
        
        while cur:
            nxt = cur.next  # 记录当前节点cur的next
            cur.next = pre  # 反转,将当前节点的next指向pre
            pre = cur   # 向后移动指针
            cur = nxt   # 向后移动指针
        return pre

回溯

46. 全排列

思路:

  1. 需要搞清楚什么时候出现一个答案,我们把它存下来?(本题中,路径和=元素数,即是一个排列,满足条件)
  2. 回溯具体怎么操作【核心思路是递归】?(将本层元素纳入,递归返回后,撤销所有操作尝试其他可能选择)
  3. 什么情况是不需要处理的情况?(使用一个列表used记录使用过的元素,前面层用过元素后面层不再用)
class Solution:
    def permute(self, nums: List[int]) -> List[List[int]]:
        path = []
        self.result = []             # 类的属性,可以不用作为参数传递
        used = [0] * len(nums)
        self.backtracking(path, nums, used)
        return self.result
        
    def backtracking(self, path, nums, used):
        if len(nums) == len(path):     # 判断出现了结果,需要加入结果列表
            self.result.append(path[:])

        for i in range(len(nums)):    # 全排列,从0开始遍历,每次每个都有机会取到
            if used[i] == 1:          # 遇到标记之前层取过的,这层需要跳过
                continue
            path.append(nums[i])      # 加入path之后,标记取过
            used[i] = 1              
            self.backtracking(path, nums, used)  # 进入下一层
            used[i] = 0               # 回溯,回到本层没有操作的状态
            path.pop()                # 回溯

动态规划

70. 爬楼梯

class Solution:
    def climbStairs(self, n: int) -> int:
        dp = [0]*(n+1)    # 爬楼梯,动态规划
        dp[0] = 1
        dp[1] = 1

        for i in range(2, n+1):
            dp[i] = dp[i-1] + dp[i-2]

        return dp[n]

416. 分割等和子集

给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

思路:

  • 正整数数组分割两部分,和为奇数排除。和为偶数,判断其中部分元素能否刚好凑成和的一半。
  • 给定一些元素,给定一个目标值,判断目标约束下的最大价值是否等于目标值。【01背包问题】
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        if sum(nums) % 2 != 0:  # 排除和不为偶数的情况
            return False
        target = sum(nums)//2   # 动态规划,01背包,能否填满target
        dp = [0] * (target+1)   # 目标:dp[target]刚好等于target, True
        
        for num in nums:        # 01背包,遍历物品
            for j in range(target, num-1, -1):  # 01背包,遍历容量
                dp[j] = max(dp[j],dp[j-num]+num)
        return dp[-1] == target

第二个循环倒序原因:dp[j] 更新依赖于dp [j-num],如果从前向后遍历,可能会重复使用。

  • dp一维数据的解法,需要先遍历物品,再遍历容量。
  • dp二维数组的解法,先遍历物品和先遍历容量没有区别:
class Solution:
    def canPartition(self, nums: List[int]) -> bool:
        if sum(nums) % 2 != 0:  # 排除和不为偶数的情况
            return False
        target = sum(nums)//2   # 动态规划,01背包,能否填满target
        dp = [[0]*(target+1) for _ in range(len(nums)+1)] # 前i个物品中,能装入容量j的最大价值

        for i in range(1, len(nums)+1):  # 遍历物品
            num = nums[i-1]
            for j in range(target+1):    # 遍历容量
                if j >= num:
                    dp[i][j] = max(dp[i-1][j], dp[i-1][j-num] + num)  # 更新最大价值
                else:
                    dp[i][j] = dp[i-1][j]
        return dp[len(nums)][target] == target
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值