面试大厂leetcode重点题型简洁明快复习(dfs/bfs,动态规划,链表,滑动窗口/双指针,回溯,ACM型输入输出,二分)

面试leetcode重点题型简洁明快复习

前言:

笔者正在应聘大厂 的春招岗位,笔者筛选了一下经常遇到的面试题型重点dfs/bfs动态规划链表滑动窗口/双指针回溯ACM型输入输出二分),笔者这里也尽量挑里面的最重点题型,从问题入手,对这些题型进行简洁明快的复习,其他的一些数据结构的题,也考,但笔者认为不是重点,这里就不提了。

题型一:dfs/bfs

1.1 dfs:

典型问题 :岛屿数量(leetcode 200):
思路 :遇到一块陆地,就用 DFS 把和它连着的陆地全部淹掉,然后岛屿数量 +1。
常见关键词:连通块、区域、岛屿、从一个点扩散、遍历所有可能路径、树/图的深度遍历。

dfs简洁模板

复制代码
def dfs(x,y):
    if 越界 or 不合法:
        retrurn

    标记当前点已访问:
    
    dfs(x+1,y)
    dfs(x-1,y)
    dfs(x,y+1)
    dfs(x,y-1)

岛屿数量的完整解答:

复制代码
def numIsLands(grid):
    if not grid:    # 边界条件,如果不是陆地
        return 0
    
    m,n = len(grid),len(grid[0])    # 获取矩阵的行数和列数 ,这也是标准模板了
    count = 0    # 记录岛屿数量

    def dfs(i,j):
        # 1. 越界:
        if i < 0 or i >= m or j < 0  or j>=n:
            return
        # 2. 不是陆地:
        if grid[i][j] != '1'
            return 

        # 3. 标记已访问
        grid[i][j] = '0'
        
        # 4. 四个方向继续搜索
        dfs(i+1,j)
        dfs(i-1,j)
        dfs(i,j+1)
        dfs(i,j-1)
    
    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                count += 1
                dfs(i,j)

    return count

1.2 BFS

BFS经典题:二叉树的层序遍历(leetcode 102)

输入输出示例:

复制代码
# 输入
    3
   / \
  9  20
     / \
    15  7

# 输出:
[[3], [9, 20], [15, 7]]

BFS关键词:最短路径、最少步数、层序遍历、一圈一圈扩散、从起点到终点最近距离。(按层扩展,一层一层扩散)

层序遍历模板 (二叉树层序遍历就是最标准的 BFS):

(一层一层的添加)

复制代码
from collections import deque

queue = deque([起点])

while queue:
    当前层大小 = len(queue)

    for _ in range(当前层大小):
        node = queue.popleft()

        处理node

        把node的邻居加入queue

二叉树的层序遍历完整实现:

复制代码
from collections import deque

def levelOrder(root):
    if not root:
        return []
    
    res = []
    queue = deque([root])

    while queue:
        level = []
        size = len(queue)

        for _ in range(size):
            node = queue.popLeft()
            level.append(node.val)

            if node.left:
                queue.append(node.left)
            if node.right:
                queue.append(node.right)
        
        res.append(level)

    return res

作业题:

岛屿的最大面积:

优先想:

DFS / BFS。

这题和"岛屿数量"很像,只不过:

复制代码
岛屿数量:发现一个岛,答案 +1
最大面积:发现一个岛,算出面积,更新最大值

dfs写法:

复制代码
def dfs(i,j):
    if 越界 or grid[i][j] == 0:
        return 0
    
    grid[i][j] = 0   #涂色
    
    area = 1
    area += dfs(i+1,j)
    area += dfs(i-1,j)
    area += dfs(i,j+1)
    area += dfs(i,j-1)

    return area

题型二:动态规划

怎么识别是 DP?

  • 最大/最小、多少种方法、能不能到达、方案数、最优解、子问题会重复。

典型题:70. 爬楼梯:

  • 到第 i 阶,可以从第 i-1 阶走一步上来,也可以从第 i-2 阶走两步上来,所以:dp[i] = dp[i - 1] + dp[i - 2](数形结合百般好,画出决策树,每条链路就是一种决策)

  • (先画两条最小的决策树。)

DP 五步模板

  1. 定义状态:dp[i] 表示什么?
  2. 写转移方程:dp[i] 从哪里来?
  3. 初始化:dp[0]、dp[1] 是什么?
  4. 遍历顺序:从小到大还是从大到小?
  5. 返回答案。

爬楼梯这道题的完整实现:

复制代码
def clibStairs(n):
    if n <= 2:
        return n
    
    dp = [0] * (n+1)

    dp[1] = 1
    dp[2] = 2

    for i in range(3, n + 1):    #左闭右开
        dp[i] = dp[i-1] + dp[i-2]
    
    return dp[n]

作业题:

题型三:链表

链表题:
关键词 :(看到这些操作,优先想链表模板):反转、删除节点、合并链表、找中点、判断环、倒数第 k 个节点。
链表题最重要的是 :别丢节点,改指针前一定先保存 next

典型题:

链表反转:

复制代码
def reverseList(head):
    prev = None
    cur = head

    while cur:
        nxt = cur.next # 先保存next,下一个节点
        cur.next = prev # 反转当前指针
        prev = cur # 同时prev往前走
        cur = nxt # 同时cur也往前走 

    return prev

作业题:

复制带随机指针的链表(每个链表节点有两个指针):

复制代码
class Node:
    def__init__(self,val):
        self.val = val
        self.next = None
        self.random = None

其中:

复制代码
next   -> 指向下一个节点
random -> 随机指向链表中的某个节点,也可能是 None

怎么解决:深拷贝:

第一遍:复制所有节点,只复制 val

第二遍:补上 next 和 random

复制代码
class Node:
    def __init__(self,val,next = None,random = None):
        self.val = val
        self.next = next 
        self.random = random

def copyRandomList(head):
    if not head:
        return None
    
    old_to_new = {}    #建立一个dict

    # 第一遍:创建所有新节点:
    cur = head
    while cur:
        old_to_new[cur] = Node(cur.val)
        cur = cur.next
    
    # 第二遍:连接next和random
    cur = head
    while cur:
        new_node = old_to_new[cur]
        new_node.next = old_to_new.get(cur.next)
        new_node.random = old_to_new.get(cur.random)
        cur = cur.next
    
    return old_to_new[head]

题型五:滑动窗口/双指针

怎么快速识别滑动窗口?

关键词::最长子串,最短子数组,连续区间,满足某个条件,右边扩张,左边收缩

  • 特别是最长 / 最短 + 连续子串 / 连续子数组

滑动窗口核心思想 :维护一个窗口:

s[left : right + 1]

之后:

  • right 负责扩大窗口
  • left 负责缩小窗口
  • 如果当前窗口不合法,就移动左指针并删除字符,直到窗口重新满足合法条件。每次窗口合法时移动右指针,更新最大长度。(注意:有时候我们要先处理重复,再加入当前字符。)
  • max/mini控制

滑动窗口万能记忆模板

复制代码
def sliding_window(s):
    left = 0
    window = set()
    ans = 0

    for right in range(len(s)):
        # 加入或准备加入s[right]

        while 窗口不合法:   # 缩小窗口
            # 移除s[left]
            left += 1

        # 更新答案
        ans = max(ans,right - left + 1)
    
    return ans

典型题:"无重复字符的最长子串"完整实现。

  • 给定一个字符串,找出其中不含重复字符的最长子串长度。

    def lengthOfLongestSubstring(s):
    left = 0
    window = set()
    ans = 0

    复制代码
      for right in range(len(s)):    # 原来right从左到右遍历字符串
          char = s[right]
    
          # 如果 char 已在当前窗口里,就不断移动left
    
          while char in window:   # 缩小窗口
              window.remove(s[left])
              left += 1
          
          # 此时窗口里没有重复char,可以加入
          window.add(char)
    
          # 更新最长长度
          ans = max(ans,right - left + 1)

题型六:回溯

看到这些关键词,优先想回溯:

  • 所有排列
  • 所有组合
  • 所有子集
  • 所有路径
  • 所有方案
  • 找出所有可能

只要题目让你返回"所有结果",而非一个最大值、最小值,就很可能是回溯。

回溯的核心思想:

复制代码
做选择
递归
撤销选择

回溯万能记忆模板

python 复制代码
res = []
path = []

def backtrack():
    if 结束条件:
        res.append(path.copy())
        return

    for 选择 in 所有选择:
        if 选择不合法:
            continue

        path.append(选择)
        backtrack()
        path.pop()

最核心的三行是:

python 复制代码
path.append(选择)
backtrack()
path.pop()

这就是:

text 复制代码
做选择 → 递归 → 撤销选择

典型题:全排列

即: 给你一个数组:nums = [1, 2, 3]

返回它的所有排列:

复制代码
[
    [1, 2, 3],
    [1, 3, 2],
    [2, 1, 3],
    [2, 3, 1],
    [3, 1, 2],
    [3, 2, 1]
]

核心思想:

比如 nums = [1, 2, 3]

第一层可以选:

text 复制代码
1 或 2 或 3

如果第一层选了 1,第二层只能选:

text 复制代码
2 或 3

如果第二层选了 2,第三层只能选:

text 复制代码
3

得到:

python 复制代码
[1, 2, 3]

然后撤销选择,尝试别的路。


全排列完整实现

python 复制代码
def permute(nums):
    res = []
    path = []
    used = [False] * len(nums)

    def backtrack():
        # 结束条件:path 长度等于 nums 长度
        if len(path) == len(nums):
            res.append(path.copy())
            return

        for i in range(len(nums)):
            # 如果这个数字已经用过,跳过
            if used[i]:
                continue

            # 做选择
            path.append(nums[i])
            used[i] = True

            # 递归
            backtrack()

            # 撤销选择
            path.pop()
            used[i] = False

    backtrack()
    return res

PS:注意:为什么要 path.copy()

这里非常重要。

不能写:

python 复制代码
res.append(path)

因为 path 后面会继续变化。

要写:

python 复制代码
res.append(path.copy())

意思是把当前路径复制一份存进去。(这里就是浅拷贝和深拷贝,大家一查就懂,不用讲了)

题型七:acm输入输出:

有时候会出现acm类型的(而非leetcode类型的),需要自己写读入读出的那种题,这里也整理一下类似的写法和典型题(牛客有这种输入输出的典型题,leetcode没有):

实际上输入输出的题,大部分只掌握这三种类型就够了

  1. 首行给 n、后面跟 n 行;
  2. 一行字符串自己解析;
  3. 一直读到 EOF

问:.strip()是什么意思?

答:.strip() 是 Python 字符串的方法,作用是:

去掉字符串两端的空白字符

这里的"空白字符"包括:

  • 空格 " "
  • 换行 "\n"
  • 制表符 "\t"

这3种题的Python读入模板

python 复制代码
# 1) 首行 n,后面 n 行
n = int(input())
for _ in range(n):
    x = input().strip()

# 2) 一行读完,自己 split
s = input().strip()
arr = s.split(';')

# 3) 一直读到 EOF
import sys
for line in sys.stdin:
    line = line.strip()
    if not line:
        continue

在牛客上搜5道重点题足矣:

  1. HJ3 (明明的随机数 ([牛客网][1])):固定 n 行输入。
  2. HJ8 (合并表记录 ([牛客网][4])):固定 n 行 + 哈希排序。
  3. HJ17(坐标移动 ([牛客网][5])):一行字符串解析。
  4. HJ19(简单错误记录 ([牛客网][6])):一直读到 EOF。
  5. HJ16(购物单 ([牛客网][7])):进阶背包题。

典型题1)牛客HJ3 明明的随机数

  • 题型:第一行 n,后面 n 行,每行一个数 ;做法是 去重 + 排序
python 复制代码
import sys

n = int(sys.stdin.readline().strip())
nums = set()

for _ in range(n):
    nums.add(int(sys.stdin.readline().strip()))

for x in sorted(nums):
    print(x)

重点:set 去重,sorted 排序,然后逐行输出。

典型题2)牛客HJ4 字符串分隔

  • 题型:输入一行字符串 ,每 8 个字符一组,不够补 0。官方题页可直接点开。
python 复制代码
s = input().strip()

while len(s) % 8 != 0:
    s += '0'

for i in range(0, len(s), 8):
    print(s[i:i+8])

重点:先补齐到 8 的倍数,再每 8 个切一刀。

典型题3):牛客HJ5 进制转换

  • 题型:输入一行十六进制字符串,输出十进制;官方题页可直接点开。
python 复制代码
s = input().strip()
print(int(s, 16))

典型题4): HJ8 合并表记录

  • 题型:第一行 n,后面 n 行每行两个整数 key value;相同 key 合并,最后按 key 升序输出。该题在牛客官方题单中就是"合并表记录"。
python 复制代码
n = int(input().strip())
mp = {}

for _ in range(n):
    k, v = map(int, input().split())
    mp[k] = mp.get(k, 0) + v

for k in sorted(mp.keys()):
    print(k, mp[k])

重点:哈希表累加,最后按 key 排序输出。

典型题5):牛客HJ17 坐标移动:

  • 题型:输入一整行指令字符串,按分号切开,合法指令才处理,非法忽略,最后输出坐标。官方题页可直接点开。
python 复制代码
s = input().strip()
x, y = 0, 0

for item in s.split(';'):
    if len(item) < 2 or len(item) > 3:
        continue
    if item[0] not in 'ASWD':
        continue
    if not item[1:].isdigit():
        continue

    step = int(item[1:])
    if item[0] == 'A':
        x -= step
    elif item[0] == 'D':
        x += step
    elif item[0] == 'W':
        y += step
    elif item[0] == 'S':
        y -= step

print(f"{x},{y}")

重点:split(';') 之后逐段校验:长度、方向字符、数字部分。

典型题6):HJ19 简单错误记录

题型:不告诉你有多少行,要一直读到 EOF;每行是"路径 + 行号",只保留文件名,文件名只留最后 16 个字符,统计次数,最后输出最后 8 条。官方题页明确写了"需要一直读入直到文件结尾"。([牛客网][6])

python 复制代码
import sys
from collections import OrderedDict

mp = OrderedDict()

for line in sys.stdin:
    line = line.strip()
    if not line:
        continue

    path, line_no = line.split()
    file_name = path.split('\\')[-1]
    file_name = file_name[-16:]
    key = (file_name, line_no)

    if key in mp:
        mp[key] += 1
    else:
        mp[key] = 1

items = list(mp.items())[-8:]

for (file_name, line_no), cnt in items:
    print(file_name, line_no, cnt)

这题重点:

  • 会不会 for line in sys.stdin
  • 会不会处理路径
  • 会不会维护插入顺序

题型八:二分查找

如何快速识别二分?

关键词:

text 复制代码
有序数组
排序数组
查找某个值
第一个大于等于
最后一个小于等于
最小可行值
最大可行值

最经典的是:

text 复制代码
有序 + 查找

二分核心思想

每次看中间:

python 复制代码
mid = (left + right) // 2

然后判断:

text 复制代码
target 在左边?
target 在右边?
还是刚好找到?

二分万能记忆模板

找第一个满足条件(第一个大于等于 target,左边界lower_bound)的位置:

python 复制代码
def binary_search(nums, target):
    left = 0
    right = len(nums)

    while left < right:
        mid = (left + right) // 2

        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid

    return left

典型题:搜索插入位置:

  • 给你一个升序数组 和一个目标值 target。如果目标值存在,返回它的下标。如果不存在,返回它应该插入的位置。

例如:

python 复制代码
nums = [1, 3, 5, 6]
target = 5

答案:

python 复制代码
2

再比如:

python 复制代码
nums = [1, 3, 5, 6]
target = 2

答案:

python 复制代码
1

因为 2 应该插在 13 中间。


本质:搜索插入位置,其实是在找:

text 复制代码
第一个 >= target 的位置

比如:

python 复制代码
nums = [1, 3, 5, 6]
target = 2

第一个大于等于 2 的数是 3,下标是 1

所以答案是 1

完整实现

python 复制代码
def searchInsert(nums, target):
    left = 0
    right = len(nums)

    while left < right:
        mid = (left + right) // 2

        if nums[mid] < target:
            left = mid + 1
        else:
            right = mid

    return left

问题:为什么最后返回 left

因为循环结束时:

python 复制代码
left == right

这个位置就是:

text 复制代码
第一个 >= target 的位置

也就是 target 应该出现的位置。

举个例子

python 复制代码
nums = [1, 3, 5, 6]
target = 2

初始:

text 复制代码
left = 0
right = 4

第一次:

text 复制代码
mid = 2
nums[2] = 5
5 >= 2
right = mid = 2

第二次:

text 复制代码
left = 0
right = 2
mid = 1
nums[1] = 3
3 >= 2
right = mid = 1

第三次:

text 复制代码
left = 0
right = 1
mid = 0
nums[0] = 1
1 < 2
left = mid + 1 = 1

结束:

text 复制代码
left = right = 1

返回 1

相关推荐
QD_ANJING1 小时前
普及一下五月AI前端面试需要达到的强度....
前端·javascript·vue.js·人工智能·面试·职场和发展
Chase_______2 小时前
【算法】LeetCode 1052 & 3679:定长滑动窗口进阶——增益最大化与频率约束贪心
算法·leetcode
凯瑟琳.奥古斯特2 小时前
力扣1367:二叉树中查找链表路径
数据结构·算法·leetcode·链表
Chase_______2 小时前
LeetCode 3 & 3090 题解:不定长滑动窗口——从“不重复“到“最多两次“,一个模板搞定频次约束问题
算法·leetcode
阿Y加油吧2 小时前
吃透 RAG 检索:纯向量短板、BM25 混合检索、RRF 融合与重排序
人工智能·leetcode
qq_296553272 小时前
【LeetCode】最大子数组乘积:三种解法从暴力到最优
数据结构·算法·leetcode·职场和发展·动态规划·柔性数组
程序员小白条2 小时前
AI 编程辅助,从入门到真香
java·开发语言·数据库·人工智能·面试·职场和发展
MonkeyKing71552 小时前
iOS 开发 UIView 与 CALayer 关系及渲染流程
ios·面试
MonkeyKing71552 小时前
iOS 开发 事件响应链与手势识别原理
面试·objective-c