面试leetcode重点题型简洁明快复习
- 前言:
- 题型一:dfs/bfs
-
- [1.1 dfs:](#1.1 dfs:)
- [1.2 BFS](#1.2 BFS)
- 作业题:
- 题型二:动态规划
- 题型三:链表
- 题型五:滑动窗口/双指针
- 题型六:回溯
- 题型七:acm输入输出:
- 题型八:二分查找
前言:
笔者正在应聘大厂 的春招岗位,笔者筛选了一下经常遇到的面试题型重点 (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 五步模板
- 定义状态:dp[i] 表示什么?
- 写转移方程:dp[i] 从哪里来?
- 初始化:dp[0]、dp[1] 是什么?
- 遍历顺序:从小到大还是从大到小?
- 返回答案。
爬楼梯这道题的完整实现:
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 = 0for 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没有):
实际上输入输出的题,大部分只掌握这三种类型就够了
- 首行给
n、后面跟n行; - 一行字符串自己解析;
- 一直读到 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道重点题足矣:
- HJ3 (明明的随机数 ([牛客网][1])):固定
n行输入。 - HJ8 (合并表记录 ([牛客网][4])):固定
n行 + 哈希排序。 - HJ17(坐标移动 ([牛客网][5])):一行字符串解析。
- HJ19(简单错误记录 ([牛客网][6])):一直读到 EOF。
- 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 应该插在 1 和 3 中间。
本质:搜索插入位置,其实是在找:
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。