跳过正文
  1. posts/

重生之我还在做 HOT100

·3160 字·7 分钟·
算法与数据结构
陈驰水
作者
陈驰水
感谢您看到这里,祝您生活愉快
目录

HOT100 从 23 年开始零零散散做过,但其中 trick 与脑筋急转弯比较多,还是很容易忘,特此总结成复习笔记。

此外,还有些面试常见题也会写在后面。

和 ‘算法题 - 分类索引’ 有一定的重复。

HOT 100 的分类其实有点问题,暂时先按顺序写了,比如很多题的知识点和类别就关系不大。

哈希
#

1. 两数之和

题干:给一个数组,找的任意两数之和等于目标值

思路:一轮遍历,用哈希记录当前遍历过哪些数,然后当前元素查哈希表即可

class Solution:
    def twoSum(self, nums: List[int], target: int) -> List[int]:
        m = defaultdict(lambda : -1) # 哈希表缺省值
        n = len(nums)
        for i in range(n):
            if m[target - nums[i]] != -1:
                return [i, m[target - nums[i]]]
            m[nums[i]] = i
        return []

49. 字母异位词分组

题干:有字符串数组,对其中每个字符串按其中出现的元素分类

输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]

输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]

思路:对每个字符串排序后,哈希 key 为排序后的值

class Solution:
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        n = len(strs)
        m = defaultdict(list)
        for i in range(n):
            tmp_s = ''.join(sorted(strs[i])) # 注意下 str 的 join 用法
            m[tmp_s].append(strs[i])
        ans = []
        for i, v in m.items():
            ans.append(v)
        return ans

128. 最长连续序列

题干:给一个数组,找的其中所有元素排序后最长的连续序列

输入:nums = [100,4,200,1,3,2] 输出:4 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。

思路:不能使用排序。用桶重构数组,遍历每个连续序列的首值,记录每个序列的长度。最终使数组中每个元素最多遍历一边。

# 桶 + 技巧
class Solution:
    def longestConsecutive(self, nums: List[int]) -> int:
        m = set(nums)
        ans = 0
        for x in m: # !注意此处是在 set 中遍历,在 nums 中遍历最差是 On2
            if x - 1 in m:
                continue
            xx = x
            length = 0
            while xx in m:
                length += 1
                xx += 1
            ans = max(ans, length)
        return ans

双指针
#

283. 移动零

题干:不改变数组顺序去情况下,将零全部移动到末尾,要求操作次数最少

输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]

思路:将一个指针视作栈顶,即目前有多少非零的数,栈顶一定指向零,当遍历到非零元素交换。

注意:和 荷兰国旗问题比较接近,但此题要求有序,因此解法不同。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        n = len(nums)
        stack_size = 0
        for i in range(n):
            if nums[i] != 0:
                nums[i], nums[stack_size] = nums[stack_size], nums[i]
                stack_size += 1

11. 盛最多水的容器

题干:找数组两个“隔板”之间能容纳最多的水,注意不同与接雨水,不用考虑“隔板”本身的体积。

输入:[1,8,6,2,5,4,8,3,7] 输出:49

思路:双指针从两边向中间走,哪边数小就缩哪边

class Solution:
    def maxArea(self, height: List[int]) -> int:
        n = len(height)
        l = 0
        r = n - 1
        ans = 0
        while l < r:
            ans = max(ans, (r - l) * min(height[l], height[r]))
            if height[l] < height[r]:
                l += 1
            else:
                r -= 1
        return ans

15. 三数之和

题干:记录数组中所有 x + y + z == 0 的值,需要去重。

思路:排序后三指针,遍历 x 作为基数,由于 y 和 z 一定有序,还是从两边缩小到中间的思路。此外可以遍历过程中去重,也可以用 tuple 做 hash 的 key,后者思维量小。

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        n = len(nums)
        nums.sort()
        ans = set()
        for i, x in enumerate(nums):
            l = i + 1
            r = n - 1
            while l < r:
                if nums[l] + nums[r] == -x:
                    ans.add((nums[i], nums[l], nums[r]))
                    # break 
                    # 注意下此处不能直接 break,因为同一个 i,可以对应不同的 l r 做答案
                    l += 1 
                    r -= 1
                elif nums[l] + nums[r] < -x:
                    l += 1
                else:
                    r -= 1
        res = []
        for x in ans:
            res.append([x[0], x[1], x[2]])
        return res

42. 接雨水

题干:过于经典,不赘述了

思路:

  1. 前缀后缀法:通法,思维量较小。统计每个数的最大前后缀
  2. 双指针实际上就说前缀后缀的优化版,核心就是哪侧前或后缀更小,就缩那测
  3. 单调递减栈,找上一个更大的,然后填坑
# 前缀后缀
class Solution:
    def trap(self, height: List[int]) -> int:
        n = len(height)
        pre = [0] * n
        pre[0] = height[0]
        suf = [0] * n
        suf[-1] = height[-1]
        for i in range(1, n):
            pre[i] = max(pre[i - 1], height[i])
        
        for i in range(n - 2, -1, -1):
            suf[i] = max(suf[i + 1], height[i])
        ans = 0
        for i in range(n):
            ans += (min(pre[i], suf[i]) - height[i])
        return ans
# 双指针,谁小谁移动
class Solution:
    def trap(self, height: List[int]) -> int:
        n = len(height)
        l = 0
        r = n - 1
        pre_max = height[l]
        suf_max = height[r]
        ans = 0
        while l < r:
            if pre_max < suf_max:
                ans += (pre_max - height[l])
                l += 1
            else:
                ans += (suf_max - height[r])
                r -= 1
            pre_max = max(pre_max, height[l]) 
            suf_max = max(suf_max, height[r])
        return ans
# 单调栈做法
class Solution:
    def trap(self, height: List[int]) -> int:
        stack = []
        n = len(height)
        ans = 0
        for i, x in enumerate(height):
            while stack and x >= height[stack[-1]]:
                bottom = height[stack[-1]]
                stack.pop()
                if not stack:
                    break
                l = stack[-1]
                h = min(height[l], x) - bottom
                ans += h * (i - l - 1)
            stack.append(i)
        return ans

滑动窗口
#

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

题干:找到数组中无重复字符的最长子串

思路:滑动窗口 + 哈希的模板题

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        n = len(s)
        m = defaultdict(int)
        l = r = ans = 0
        while l <= r and r < n:
            m[s[r]] += 1
            # print(m)
            while l <= r and m[s[r]] != 1:
                # print(l, r, m)
                m[s[l]] -= 1
                l += 1
            ans = max(ans, r - l + 1)
            r += 1
        return ans

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

题干:找到数组中所有 “目标字符 cnt ”等于”字串字符 cnt“ 的字串下标

思路:稍微变形的滑窗板子,注意当缩减左窗口后,字串长度与目标相同时,一定两者 cnt 相同。

# 滑动窗口板子
class Solution:
    def findAnagrams(self, s: str, p: str) -> List[int]:
        n = len(s)
        l = r = 0
        t = defaultdict(int)
        for i in list(p):
            t[i] += 1
        m = defaultdict(int)
        ans = []
        while l <= r and r < n:
            m[s[r]] += 1
            while l <= r and m[s[r]] > t[s[r]]: # 注意下缩左窗口的条件
                m[s[l]] -= 1
                l += 1
            # print(r, l, m)
            if r - l + 1 == len(p):
                ans.append(l)
            r += 1
        return ans

字串
#

560. 和为 K 的子数组

题干:找子串和为 K 的数量

思路:O1 求区间和 -> 需要前缀和 -> 哈希记录出现的指定前缀数量 -> 和两数之和思路一样

# 求区间和 -> 前缀和 -> 哈希记录出现的指定前缀数量
class Solution:
    def subarraySum(self, nums: List[int], k: int) -> int:
        n = len(nums)
        prefix = [0] * (n + 1)
        m = defaultdict(int)
        m[0] = 1
        ans = 0
        # prefix[x] - prefix[y] == k
        # 即找 m[prefix[i] - k] 的数量
        for i in range(1, n + 1):
            prefix[i] = prefix[i - 1] + nums[i - 1]
            ans += m[prefix[i] - k]
            m[prefix[i]] += 1
        return ans
         

239. 滑动窗口最大值

题干:找所有定长字串的最大值,要求 On

思路:经典单调队列,仅字串中保持递减的一个队列,右侧插入与单调栈一致。当左侧和首元素下标相同时,队列进行 O1 移除。从而使每个队列的首元素一定是当前字串的最大值。

单调队列相比单调栈出现的更少,注意其基本都是在栈中存下标,然后注意栈中的严格单调性。

此外,此题还有个 ST 表 做法,但复杂度不比单调队列好。

# 单调队列
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        arr = deque()
        n = len(nums)
        r = 0
        ans = []
        while r < n:
            # print(r, arr)
            if r >= k:
                ans.append(nums[arr[0]])
                if arr[0] == r - k: # 维护左下标
                    arr.popleft() 
            
            while arr and nums[r] >= nums[arr[-1]]: # 入队列操作和单调栈一样
                arr.pop()
            arr.append(r)
            r += 1
        ans.append(nums[arr[0]])
        return ans

76. 最小覆盖子串

题干:找到所有字串中,“目标串元素 cnt “ 能覆盖的”长度最小字串元素 cnt “是什么?

输入:s = “ADOBECODEBANC”, t = “ABC” 输出:“BANC”

思路:如果只要 O(52*n) 即滑窗的过程每次遍历单词表,那就是滑窗板子,和438. 找到字符串中所有字母异位词 完全一样。

但如果想 O(n) 还是有些难度,新提出一个 less 字符标志,判断单词表中有多少词仍未被覆盖,从而代替遍历单词表。

但要注意 less 的维护方法。

class Solution:
    def minWindow(self, s: str, t: str) -> str:
        n = len(s)
        l = r = 0
        target = defaultdict(int)
        for i in list(t):
            target[i] += 1
        m = defaultdict(int)

        less = len(target) # 开始时 target 中每个词都没被满足
        ans_l, ans_r = 0, inf
        while l <= r and r < n:
            m[s[r]] += 1
            if m[s[r]] == target[s[r]]: # 注意此处一定是等于,只想让 less 更新一次
                less -= 1

            while l <= r and less == 0: 
                # print(l, r, less, ans_l, ans_r)
                if (ans_r - ans_l) > (r - l): # 注意此时内层循环才满足答案吗要求
                    ans_l = l
                    ans_r = r
                m[s[l]] -= 1
                if m[s[l]] == target[s[l]] - 1:
                    less += 1 # 同样的,保证 less 只更新一次
                l += 1
            r += 1
        return s[ans_l:ans_r + 1] if ans_r != inf else ""

普通数组
#

53. 最大子数组和

题干:找字串中最大的 sum 是多少?

思路:前缀和 + 空间优化(也可以简单 DP)。记录前缀和的最小值即可,简单优化下就能空间 O1。

# 找个前缀就可以
class Solution:
    def maxSubArray(self, nums: List[int]) -> int:
        n = len(nums)
        prefix = 0
        pre_min = inf
        ans = -inf
        for i in range(1, n + 1):
            pre_min = min(prefix, pre_min)
            prefix = prefix + nums[i - 1]
            ans = max(ans, prefix - pre_min)
        return ans

56. 合并区间

题干:合并所有有重叠的区间

思路:对原始区间排序后,比较当前区间的右侧和下一个区间的左侧。大部分语言之间排就可以,小部分要写 lambda。

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        intervals.sort()
        ans = []
        l = intervals[0][0]
        r = intervals[0][1]
        for x, y in intervals:
            if x <= r:
                r = max(y, r)
            else:
                ans.append([l, r])
                l = x
                r = y
        ans.append([l, r])
        return ans

189. 轮转数组

题干:将数组右移动 k 位

思路:三次反转,先转最大区间,再转两个小区间

提示:如果是左移,其实就等于右移 -k 位

class Solution:
    def rotate(self, nums: List[int], k: int) -> None:
        """
        Do not return anything, modify nums in-place instead.
        """
        # 自带的 reverse 会产生多余空间,自己用双指针搓更合适
        def reverse(l, r):
            while l < r:
                nums[l], nums[r] = nums[r], nums[l]
                l += 1
                r -= 1
        n = len(nums)
        k %= n
        reverse(0, n - 1)
        reverse(0, k - 1)
        reverse(k, n - 1)

238. 除自身以外数组的乘积

题干:获得每个数对应的除了他自己以外全部元素的乘积。

思路:明显的前后缀,题目要求只使用一个额外数组,先后缀后前缀直接得到答案就可以

# 前缀后缀积,最好用一个数组解决
class Solution:
    def productExceptSelf(self, nums: List[int]) -> List[int]:
        n = len(nums)
        suf = [1] * n
        for i in range(n - 1, 0, -1):
            suf[i - 1] = suf[i] * nums[i]
        # print(suf)
        
        pre = 1
        for i in range(n):
            suf[i] *= pre
            pre *= nums[i]
        return suf

41. 缺失的第一个正数

题干:找数组中没出现的最小正数,要求 O(n) 时间,O1 空间。

思路: 经典技巧题,想让每个符合条件的位置满足 nums[i] == nums[nums[i]]。

这样就相当于让位置 i 的 x 一定回到 x 的位置上,此外交换过程中要考虑 swap 依赖问题。

class Solution:
    def firstMissingPositive(self, nums: List[int]) -> int:
        n = len(nums)
        for i in range(n): # 对齐下标
            nums[i] -= 1
        for i in range(n):
            while 0 <= nums[i] < n and nums[i] != nums[nums[i]]:
                j = nums[i] # 要让 swap 不被依赖
                nums[i], nums[j] = nums[j], nums[i]
        # print(nums)
        for i in range(n):
            if nums[i] != i:
                return i + 1
        return n + 1

矩阵
#

相关文章

算法题 - 分类索引
·8537 字·18 分钟
算法与数据结构
“资源分配型” 动态规划总结
·2570 字·6 分钟
算法与数据结构 动态规划 背包问题 记忆化搜索
蛋糕游戏题解 - 贪心博弈
·921 字·2 分钟
算法与数据结构 贪心 博弈 前缀和
基础数论 - 模板 & 分析
·1766 字·4 分钟
算法与数据结构 数论 模板