AI算法工程师面试宝典【大厂手撕代码与数据结构高频实战】【第一部分】

前言
AI算法工程师面试,最令人头疼的往往不是手撕Transformer或推导YOLO,而是看似基础却暗藏玄机的数学问题。很多候选人能熟练调用PyTorch搭建模型,却被“解释PCA的数学原理”或“L1和L2正则化的本质区别”问得哑口无言。
大厂面试的残酷真相是:基础能力模块是淘汰率最高的环节。面试官通过这几道题,就能快速判断你是“调包侠”还是真正理解底层原理的工程师。
一、基础能力模块(必考题,大厂重点考察功底)
1. 数学基础(算法的底层支撑,高频必考)
线性代数
线性代数是机器学习的“语言”。特征向量、矩阵运算、空间变换,这些概念贯穿PCA、SVD、神经网络等所有核心算法。
矩阵运算
矩阵乘法:C = A × B,其中C[i][j] = Σ(A[i][k] * B[k][j])。要求A的列数等于B的行数。几何意义:对向量依次施加两次线性变换。
转置:(A^T)[i][j] = A[j][i]。用于改变数据维度,如将行向量变为列向量。
逆矩阵:A × A^(-1) = I。只有方阵且行列式不为0时才可逆。用于求解线性方程组Ax = b → x = A^(-1)b。
行列式:衡量矩阵对空间体积的缩放因子。行列式为0表示矩阵将空间压缩到更低维度(信息丢失)。
对称矩阵:A = A^T。性质:特征值为实数,不同特征值对应的特征向量正交。协方差矩阵就是对称矩阵。
正交矩阵:Q^T = Q^(-1),列向量两两正交且模长为1。性质:Q^T Q = I,变换不改变向量长度(旋转/反射)。
特征值与特征向量
定义:对于方阵A,若存在非零向量v和标量λ满足A v = λ v,则λ是特征值,v是特征向量。
几何意义:特征向量是变换后方向不变的向量,特征值是对应的缩放倍数。
求解方法:解特征方程det(A - λI) = 0得λ,再代入(A - λI)v = 0求v。
在PCA中的应用:对协方差矩阵进行特征值分解,特征值大小表示对应主成分方向的方差大小,取前k大特征值对应的特征向量完成降维。
矩阵对角化:若A有n个线性无关的特征向量,则A = PDP^(-1),其中D是对角矩阵(对角线为特征值)。用于简化矩阵幂运算A^k = P D^k P^(-1)。
向量空间
内积(点积):⟨x,y⟩ = Σ x_i y_i。衡量两向量的相似度,cosθ = ⟨x,y⟩/(||x||·||y||)。
外积:向量a与b的外积得到矩阵a·b^T。在多变量分析中用于构建协方差结构。
范数:
- L1范数:
||x||₁ = Σ|x_i|。曼哈顿距离,产生稀疏解(Lasso回归)。 - L2范数:
||x||₂ = sqrt(Σ x_i²)。欧氏距离,防止过拟合(Ridge回归),计算梯度平滑。 - L∞范数:
||x||_∞ = max|x_i|。切比雪夫距离,用于最大偏差约束。
高频考题:PCA的数学原理(如何通过特征值分解降维)?
核心答案:
PCA的目标是找到数据方差最大的投影方向。步骤如下:
- 中心化:将每个特征减去均值,使数据均值为0。
- 计算协方差矩阵:
C = (1/(n-1)) X^T X,其中X是中心化后的数据矩阵(n个样本,m个特征)。协方差矩阵是对称矩阵。 - 特征值分解:求C的特征值
λ₁ ≥ λ₂ ≥ ... ≥ λ_m和对应的特征向量v₁, v₂, ..., v_m。 - 选择主成分:取前k个特征向量构成投影矩阵
W(m×k)。 - 降维:
X_new = X · W(n×k)。
为什么能降维:特征值λ代表该方向上的方差大小。方差小的方向可以认为是噪声或冗余信息,舍弃后信息损失最小。
代码示例:
import numpy as np
def pca_from_scratch(X, k):
"""
手动实现PCA降维
参数:
X: 输入数据矩阵,shape (n_samples, n_features)
k: 保留的主成分个数
返回:
X_reduced: 降维后的数据,shape (n_samples, k)
explained_variance_ratio: 各主成分解释的方差比例
"""
# 1. 中心化:减去每个特征的均值
X_mean = np.mean(X, axis=0) # 计算每列均值
X_centered = X - X_mean # 中心化
# 2. 计算协方差矩阵
n_samples = X.shape[0]
cov_matrix = (X_centered.T @ X_centered) / (n_samples - 1)
# 3. 特征值分解
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
# 4. 按特征值从大到小排序
idx = np.argsort(eigenvalues)[::-1]
eigenvalues = eigenvalues[idx]
eigenvectors = eigenvectors[:, idx]
# 5. 选择前k个特征向量构成投影矩阵
W = eigenvectors[:, :k]
# 6. 降维投影
X_reduced = X_centered @ W
# 计算解释方差比例(用于评估信息保留程度)
explained_variance_ratio = eigenvalues[:k] / np.sum(eigenvalues)
return X_reduced, explained_variance_ratio
# 测试代码
if __name__ == "__main__":
# 生成测试数据:100个样本,5个特征
np.random.seed(42)
X_test = np.random.randn(100, 5)
# 降维到2维
X_pca, var_ratio = pca_from_scratch(X_test, k=2)
print(f"原始数据形状: {X_test.shape}")
print(f"降维后形状: {X_pca.shape}")
print(f"前2个主成分解释方差比例: {var_ratio}")
print(f"总解释方差比例: {np.sum(var_ratio):.4f}")
高频考题:L1和L2范数的区别及各自的正则化效果?
核心答案:
| 特性 | L1正则化(Lasso) | L2正则化(Ridge) |
|---|---|---|
| 公式 | 损失 + λ·Σ|w_i| | 损失 + λ·Σ w_i² |
| 梯度 | λ·sign(w) | 2λ·w |
| 解的稀疏性 | 稀疏(很多w_i=0) | 稠密(w_i接近0但不等于0) |
| 几何原因 | 菱形约束易与坐标轴相交 | 圆形约束切点各方向非零 |
| 适用场景 | 特征选择、高维稀疏数据 | 防止过拟合、共线性处理 |
| 鲁棒性 | 对异常值较敏感 | 更稳定 |
为什么L1产生稀疏解:优化问题的约束区域是菱形(L1球),菱形顶点在坐标轴上,等高线更容易在这些顶点处与菱形相交,导致某些系数为0。
代码演示:
import numpy as np
import matplotlib.pyplot as plt
def compare_l1_l2_regularization():
"""
演示L1和L2正则化对权重的影响
使用简单线性回归 + 正则化
"""
np.random.seed(42)
n_samples, n_features = 50, 10
# 生成数据:只有前3个特征真正有用,其余是噪声
X = np.random.randn(n_samples, n_features)
true_weights = np.zeros(n_features)
true_weights[:3] = [2.0, -1.5, 0.8]
y = X @ true_weights + np.random.randn(n_samples) * 0.1
# 闭式解:普通最小二乘 (X^T X)^(-1) X^T y
w_ols = np.linalg.inv(X.T @ X) @ X.T @ y
# L2正则化闭式解:(X^T X + λI)^(-1) X^T y
lambda_l2 = 0.5
w_l2 = np.linalg.inv(X.T @ X + lambda_l2 * np.eye(n_features)) @ X.T @ y
# L1正则化使用坐标下降法(简化实现)
def lasso_coordinate_descent(X, y, lambda_l1, max_iter=1000, tol=1e-4):
n_samples, n_features = X.shape
w = np.zeros(n_features)
for _ in range(max_iter):
w_old = w.copy()
for j in range(n_features):
# 计算残差(除去第j个特征的影响)
residual = y - (X @ w - X[:, j] * w[j])
# 单变量最小二乘解
rho = X[:, j] @ residual
# 软阈值操作
if rho > lambda_l1:
w[j] = (rho - lambda_l1) / (X[:, j] @ X[:, j])
elif rho < -lambda_l1:
w[j] = (rho + lambda_l1) / (X[:, j] @ X[:, j])
else:
w[j] = 0
if np.linalg.norm(w - w_old) < tol:
break
return w
lambda_l1 = 0.5
w_l1 = lasso_coordinate_descent(X, y, lambda_l1)
print("各方法得到的权重系数(前5个特征展示):")
print(f"真实权重: {true_weights[:5]}")
print(f"普通最小二乘: {np.round(w_ols[:5], 4)}")
print(f"L2正则化(Ridge): {np.round(w_l2[:5], 4)}")
print(f"L1正则化(Lasso): {np.round(w_l1[:5], 4)}")
print("\nL1正则化非零系数个数:", np.sum(np.abs(w_l1) > 1e-6))
print("L2正则化非零系数个数:", np.sum(np.abs(w_l2) > 1e-6))
if __name__ == "__main__":
compare_l1_l2_regularization()
概率论与数理统计
概率论是处理不确定性的数学工具。贝叶斯定理、分布假设、参数估计是朴素贝叶斯、EM算法、变分推断的基石。
基础概念
- 概率:事件发生的可能性,0~1之间。
- 期望:
E[X] = Σ x·P(x),随机变量的加权平均。 - 方差:
Var(X) = E[(X-μ)²],衡量离散程度。 - 协方差:
Cov(X,Y) = E[(X-μ_x)(Y-μ_y)],衡量两变量的线性相关性。 - 条件概率:
P(A|B) = P(A∩B)/P(B),B发生下A的概率。 - 贝叶斯定理:
P(A|B) = P(B|A)·P(A)/P(B),后验 = 似然×先验/证据。 - 独立:
P(A∩B)=P(A)P(B),互斥:P(A∩B)=0。
常见分布
| 分布 | 适用场景 | 概率函数 |
|---|---|---|
| 正态分布(N) | 误差、身高、噪声 | f(x) = 1/(σ√(2π))·exp(-(x-μ)²/(2σ²)) |
| 二项分布(B) | 抛硬币n次中正面次数 | P(X=k)=C(n,k)p^k(1-p)^(n-k) |
| 泊松分布§ | 单位时间内事件发生次数 | P(X=k)=e^(-λ)λ^k/k! |
| 均匀分布(U) | 等概率随机数 | f(x)=1/(b-a), x∈[a,b] |
参数估计
- 极大似然估计(MLE):
θ_MLE = argmax P(D|θ),找使观测数据概率最大的参数。只依赖数据,不依赖先验。 - 最大后验估计(MAP):
θ_MAP = argmax P(θ|D) = argmax P(D|θ)·P(θ),在MLE基础上乘先验,相当于L2正则化(高斯先验)或L1正则化(拉普拉斯先验)。
假设检验
- 原假设(H₀):通常表示“无差异”或“无效果”。备择假设(H₁):与原假设对立。
- P值:在H₀成立下,观察到当前或更极端结果的概率。P<0.05则拒绝H₀。
- t检验:比较两组均值是否显著不同(如AB测试)。
- 卡方检验:检验分类变量之间的独立性。
高频考题:贝叶斯定理在朴素贝叶斯算法中的应用?
核心答案:
朴素贝叶斯分类器直接应用贝叶斯公式:P(y|x₁,x₂,...,x_n) = P(y)·Π P(x_i|y) / P(x)。
朴素假设:特征之间在给定类别下条件独立。这大大简化了计算,使得原本需要O(2^n)的参数估计降为O(n)。
分类决策:取后验概率最大的类别,即ŷ = argmax_y P(y)·Π P(x_i|y)(分母P(x)对所有y相同,可忽略)。
示例:垃圾邮件分类。先验P(垃圾)可从训练集统计;似然P(单词|垃圾)用词频估计;新邮件计算各单词的条件概率乘积,判断是否为垃圾。
高频考题:极大似然估计和MAP的核心差异?
核心答案:
| 维度 | MLE | MAP |
|---|---|---|
| 优化目标 | `P(D | θ)` |
| 是否使用先验 | 否 | 是 |
| 小样本表现 | 容易过拟合 | 更稳定(先验起约束作用) |
| 对应正则化 | 无 | L2(高斯先验)/ L1(拉普拉斯先验) |
| 频率派vs贝叶斯派 | 频率派(θ是固定常数) | 贝叶斯派(θ是随机变量) |
举例:抛硬币10次全正面。MLE估计p=1(认为下次必正面),MAP若用Beta(2,2)先验(偏向p=0.5),则估计值≈0.917,更合理。
高频考题:如何用正态分布拟合数据?
核心答案:
- 假设数据服从正态分布
X ~ N(μ, σ²)。 - 估计参数:
- MLE估计:
μ̂ = (1/n)Σ x_i,σ̂² = (1/n)Σ (x_i-μ̂)²。 - 无偏估计(Bessel校正):样本方差
s² = (1/(n-1))Σ (x_i-μ̂)²。
- MLE估计:
- 检验拟合优度:
- Q-Q图:样本分位数 vs 理论分位数,若点落在直线上则服从正态。
- Shapiro-Wilk检验(小样本)或Kolmogorov-Smirnov检验(大样本)。
- 应用:得到参数后可用
μ̂ ± 1.96σ̂估计95%置信区间。
代码示例:
import numpy as np
from scipy import stats
def fit_normal_demonstration():
"""
演示正态分布拟合:估计参数、Q-Q图、置信区间
"""
np.random.seed(42)
# 生成真实分布 N(5, 2²) 的1000个样本
true_mean, true_std = 5.0, 2.0
data = np.random.normal(true_mean, true_std, 1000)
# 1. 参数估计
estimated_mean = np.mean(data)
estimated_std = np.std(data, ddof=1) # ddof=1使用n-1分母
print(f"真实参数: μ={true_mean}, σ={true_std}")
print(f"估计参数: μ̂={estimated_mean:.4f}, σ̂={estimated_std:.4f}")
# 2. 置信区间估计(均值的95%置信区间)
n = len(data)
std_error = estimated_std / np.sqrt(n)
margin_of_error = stats.t.ppf(0.975, df=n-1) * std_error
ci_lower = estimated_mean - margin_of_error
ci_upper = estimated_mean + margin_of_error
print(f"均值95%置信区间: [{ci_lower:.4f}, {ci_upper:.4f}]")
# 3. 拟合优度检验(Shapiro-Wilk)
shapiro_stat, shapiro_p = stats.shapiro(data)
print(f"Shapiro-Wilk检验: p值={shapiro_p:.4f}")
if shapiro_p > 0.05:
print("结论: 不能拒绝数据来自正态分布 (p>0.05)")
else:
print("结论: 数据可能不服从正态分布 (p<=0.05)")
# 4. 利用拟合分布做概率预测
# 预测 P(X < 6)
prob_less_than_6 = stats.norm.cdf(6, loc=estimated_mean, scale=estimated_std)
print(f"P(X < 6) ≈ {prob_less_than_6:.4f}")
return estimated_mean, estimated_std
if __name__ == "__main__":
fit_normal_demonstration()
微积分
微积分是优化的语言。梯度下降、反向传播、凸优化都离不开导数、梯度和链式法则。
导数与偏导数
- 导数:
f'(x) = lim_{h→0} (f(x+h)-f(x))/h,函数在某点的变化率。 - 偏导数:多元函数对一个变量的导数,其他变量视为常数。
- 链式法则:
∂f/∂x = (∂f/∂g)·(∂g/∂x),深度学习反向传播的数学基础。
梯度下降相关
- 梯度:
∇f = (∂f/∂x₁, ∂f/∂x₂, ..., ∂f/∂x_n),指向函数值增长最快的方向。负梯度方向是下降最快的方向。 - 凸函数:满足
f(θx+(1-θ)y) ≤ θf(x)+(1-θ)f(y),局部最小值就是全局最小值。判定:二阶导数≥0(一元)或Hessian矩阵半正定(多元)。 - 非凸函数:存在多个局部最小值,如深度神经网络的损失函数。
- Hessian矩阵:
H[i][j] = ∂²f/∂x_i∂x_j,包含二阶偏导。用于判断极值点类型(正定→局部最小,负定→局部最大,不定→鞍点)。
积分
- 定积分:
∫_a^b f(x)dx,曲线下面积。在概率中用于计算累积分布函数(CDF)。 - 不定积分:原函数族。在概率密度函数(PDF)中,积分得到概率质量。
高频考题:为什么梯度下降能找到最小值?
核心答案:
梯度下降的迭代公式:x_{t+1} = x_t - η ∇f(x_t)。
原因:一阶泰勒展开f(x+Δ) ≈ f(x) + ∇f(x)^T Δ。若取Δ = -η ∇f(x)(η>0),则f(x+Δ) ≈ f(x) - η||∇f(x)||²。只要η足够小,函数值必然下降。
收敛条件:对于凸函数且学习率合适,梯度下降保证收敛到全局最优;对于非凸函数,收敛到某个局部极小点(或鞍点)。
高频考题:凸函数的判定条件?
核心答案:
- 一阶条件:
f(y) ≥ f(x) + ∇f(x)^T (y-x),对所有x,y成立。即切线在函数下方。 - 二阶条件(更常用):Hessian矩阵半正定(
H ≽ 0,所有特征值≥0)。对于一元函数,即f''(x) ≥ 0。
例子:f(x)=x²(凸),f(x)=x³(非凸,二阶导=6x可负),f(x)=log x(凹,二阶导<0)。
高频考题:链式法则在反向传播中如何具体计算?
核心答案:
以两层神经网络为例:loss = L(y, ŷ),ŷ = σ(W₂·h + b₂),h = σ(W₁·x + b₁)。
反向传播从loss开始,逐层向前计算梯度:
- 输出层:
∂L/∂ŷ,∂L/∂W₂ = (∂L/∂ŷ)·(∂ŷ/∂z₂)·(∂z₂/∂W₂),其中z₂ = W₂·h+b₂,∂ŷ/∂z₂ = σ'(z₂)。 - 隐藏层:
∂L/∂h = (∂L/∂ŷ)·(∂ŷ/∂z₂)·W₂^T。 - 继续传播:
∂L/∂W₁ = (∂L/∂h)·(∂h/∂z₁)·(∂z₁/∂W₁)。
链式法则本质:复合函数L(f(g(x)))对x的导数 = L'·f'·g',反向逐层相乘。
代码示例:
import numpy as np
def backpropagation_demo():
"""
演示两层神经网络的反向传播(链式法则)
网络结构: 输入x(2维) -> 隐藏层(3神经元, sigmoid) -> 输出层(1神经元, sigmoid)
损失函数: 均方误差 MSE
"""
np.random.seed(42)
# 前向传播
x = np.array([0.5, -0.2]) # 输入 (2,)
y_true = np.array([1.0]) # 真实标签
# 初始化权重和偏置
W1 = np.random.randn(2, 3) # (2,3)
b1 = np.zeros(3) # (3,)
W2 = np.random.randn(3, 1) # (3,1)
b2 = np.zeros(1) # (1,)
# 隐藏层
z1 = x @ W1 + b1 # (3,)
h = 1 / (1 + np.exp(-z1)) # sigmoid激活
# 输出层
z2 = h @ W2 + b2 # (1,)
y_pred = 1 / (1 + np.exp(-z2)) # sigmoid激活
# 损失 (MSE)
loss = np.mean((y_true - y_pred) ** 2)
print(f"前向传播结果: y_pred={y_pred[0]:.4f}, loss={loss:.6f}")
# 反向传播 (链式法则)
# dL/dy_pred
dL_dy_pred = -2 * (y_true - y_pred) / 1 # 均值的倒数 (1个样本)
# dy_pred/dz2 (sigmoid导数: σ(z)*(1-σ(z)))
dy_pred_dz2 = y_pred * (1 - y_pred)
# dz2/dW2 = h^T, dz2/db2 = 1, dz2/dh = W2^T
dL_dz2 = dL_dy_pred * dy_pred_dz2 # 标量
dL_dW2 = np.outer(h, dL_dz2) # (3,1) 外积
dL_db2 = dL_dz2 # (1,)
dL_dh = dL_dz2 * W2.T # (3,)
# dh/dz1 (sigmoid导数)
dh_dz1 = h * (1 - h) # (3,)
dL_dz1 = dL_dh * dh_dz1 # 逐元素乘 (3,)
# dz1/dW1 = x^T, dz1/db1 = 1
dL_dW1 = np.outer(x, dL_dz1) # (2,3)
dL_db1 = dL_dz1 # (3,)
print("\n梯度计算结果:")
print(f"dL/dW2 形状: {dL_dW2.shape}, 前几个值: {dL_dW2.flatten()[:3]}")
print(f"dL/dW1 形状: {dL_dW1.shape}, 前几个值: {dL_dW1.flatten()[:3]}")
# 验证链式法则的正确性:数值梯度对比
def compute_loss(W1, b1, W2, b2):
z1 = x @ W1 + b1
h = 1/(1+np.exp(-z1))
z2 = h @ W2 + b2
y_pred = 1/(1+np.exp(-z2))
return np.mean((y_true - y_pred)**2)
epsilon = 1e-5
numeric_grad_W2 = np.zeros_like(W2)
for i in range(W2.shape[0]):
for j in range(W2.shape[1]):
W2_plus = W2.copy()
W2_minus = W2.copy()
W2_plus[i,j] += epsilon
W2_minus[i,j] -= epsilon
numeric_grad_W2[i,j] = (compute_loss(W1, b1, W2_plus, b2) -
compute_loss(W1, b1, W2_minus, b2)) / (2*epsilon)
print(f"\n数值梯度与解析梯度最大差异: {np.max(np.abs(dL_dW2 - numeric_grad_W2)):.2e}")
print("差异极小 → 链式法则实现正确")
if __name__ == "__main__":
backpropagation_demo()
2. 编程基础(大厂筛选的第一道门槛,校招重点)
如果说数学基础决定你能走多远,那么编程基础决定你能不能迈进门。大厂面试的第一轮通常是线上笔试或现场手撕代码,这一关淘汰率超过60%。很多候选人能侃侃而谈模型原理,却在写一个简单的topK时卡壳。
编程基础考核的核心不是奇技淫巧,而是代码的规范性、对数据结构的理解、以及算法复杂度的敏感度。本章将带你系统梳理Python核心特性、经典数据结构和算法,并提供大厂高频手撕题的模板代码。
Python核心
Python是AI工程化的第一语言。面试官会通过几个小问题快速判断你对这门语言的掌握深度——是否只停留在调包水平。
基础语法
列表(list):动态数组,可变的、有序的元素集合。支持增删改查、切片、迭代。时间复杂度:索引O(1),尾部追加O(1)均摊,插入/删除O(n)。
字典(dict):哈希表实现,键值对存储。键必须可哈希(不可变类型)。时间复杂度:增删改查平均O(1)。Python 3.7+ 保证插入顺序。
元组(tuple):不可变的列表。一旦创建不能修改,可作为字典的键。通常用于固定长度的记录(如坐标点、函数多返回值)。
集合(set):无序、不重复的元素集合。基于哈希表实现,支持并交差运算。时间复杂度:增删查平均O(1)。
高效操作示例
# 列表推导式(比for循环快2-3倍)
squares = [x**2 for x in range(10) if x % 2 == 0] # [0, 4, 16, 36, 64]
# 字典合并(Python 3.9+)
dict1 = {"a": 1, "b": 2}
dict2 = {"c": 3, "d": 4}
merged = dict1 | dict2 # {'a':1, 'b':2, 'c':3, 'd':4}
# 集合去重并保持顺序(Python 3.7+字典有序)
deduped = list(dict.fromkeys([1, 2, 2, 3, 1, 4])) # [1, 2, 3, 4]
# Counter统计频率
from collections import Counter
freq = Counter("abracadabra") # Counter({'a':5, 'b':2, 'r':2, 'c':1, 'd':1})
装饰器(Decorator)
原理:装饰器是一个接收函数作为参数、返回新函数的可调用对象。本质是函数 = 装饰器(函数)的语法糖。
应用场景:日志记录、执行时间统计、权限校验、缓存(@lru_cache)、重试机制。
import time
import functools
def timer_decorator(func):
"""统计函数执行时间的装饰器"""
@functools.wraps(func) # 保留原函数的元信息(名称、文档)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} 执行耗时: {elapsed:.6f}秒")
return result
return wrapper
@timer_decorator
def slow_function(n):
"""模拟耗时计算"""
total = 0
for i in range(n):
total += i ** 2
return total
# 带参数的装饰器(进阶)
def retry(max_attempts=3, delay=1.0):
"""自动重试装饰器"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
if attempt == max_attempts - 1:
raise
time.sleep(delay)
print(f"重试 {attempt+1}/{max_attempts}")
return None
return wrapper
return decorator
if __name__ == "__main__":
slow_function(1000000)
迭代器(Iterator)与生成器(Generator)
迭代器:实现了__iter__()和__next__()方法的对象。可被for循环遍历。iter(iterable)将可迭代对象转为迭代器。
生成器:使用yield的函数,惰性求值,每次产生一个值。比列表节省内存,适合处理无限序列或大规模数据。
# 生成器:逐行读取大文件(避免内存溢出)
def read_large_file(file_path, chunk_size=1024):
"""生成器方式读取大文件,每次返回一行"""
with open(file_path, 'r', encoding='utf-8') as f:
while True:
line = f.readline()
if not line:
break
yield line.strip()
# 生成器表达式(类似列表推导式但使用圆括号)
squares_gen = (x**2 for x in range(10)) # 不立即计算,返回生成器对象
print(next(squares_gen)) # 0
print(next(squares_gen)) # 1
# 斐波那契数列生成器(无限序列)
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
first_10 = [next(fib) for _ in range(10)] # [0,1,1,2,3,5,8,13,21,34]
面向对象编程(OOP)
AI算法中,模型通常封装为类(如PyTorch的nn.Module)。理解OOP有助于组织复杂代码。
class BaseModel:
"""抽象基类示例"""
def __init__(self, name):
self.name = name
self._trained = False # 约定私有属性(单下划线)
def train(self, data):
"""训练方法,子类必须实现"""
raise NotImplementedError("子类需实现train方法")
def predict(self, x):
raise NotImplementedError
@property
def is_trained(self):
"""属性装饰器,使方法可像属性一样访问"""
return self._trained
@is_trained.setter
def is_trained(self, value):
if not isinstance(value, bool):
raise TypeError("必须是布尔值")
self._trained = value
class LogisticRegression(BaseModel):
"""继承和多态示例"""
def __init__(self, name, learning_rate=0.01):
super().__init__(name) # 调用父类构造
self.lr = learning_rate
self.weights = None
def train(self, data):
print(f"{self.name} 开始训练,学习率={self.lr}")
# 模拟训练
self.weights = [0.1, 0.2, 0.3]
self.is_trained = True
def predict(self, x):
if not self.is_trained:
raise ValueError("模型未训练")
return sum(w * xi for w, xi in zip(self.weights, x)) > 0
# 使用
model = LogisticRegression("LR_v1", learning_rate=0.001)
model.train(None)
print(model.predict([1, 2, 3])) # True
异常处理
def safe_divide(a, b):
"""健壮的除法函数"""
try:
result = a / b
except ZeroDivisionError:
print("错误:除数不能为零")
return float('inf')
except TypeError:
print("错误:参数必须是数字")
return None
else:
# 没有异常时执行
print("计算成功")
return result
finally:
# 无论是否异常都执行(如关闭文件)
print("除法操作完成")
高频考题:列表和元组的区别?
| 特性 | 列表(list) | 元组(tuple) |
|---|---|---|
| 可变性 | 可变(增删改) | 不可变 |
| 内存占用 | 较大(预留空间) | 较小(紧凑存储) |
| 速度 | 较慢 | 较快(创建和访问) |
| 作为字典键 | 不可以(不可哈希) | 可以 |
| 适用场景 | 动态数据集合 | 固定结构(坐标、配置) |
深入回答:元组的不可变性使其可以作为字典的键,而列表不能。元组通常用于函数返回多个值(本质是解包)。Python内部对短元组(长度≤20)有缓存优化,创建速度更快。
高频考题:装饰器的实现原理?
核心答案:装饰器是闭包的一种应用。Python解释器遇到@deco语法时,等价于func = deco(func)。装饰器函数内部定义一个wrapper函数,wrapper内部调用原函数并添加额外逻辑,最后返回wrapper。使用functools.wraps可保留原函数的元数据(__name__、__doc__),否则调试时会丢失信息。
高频考题:如何高效处理大规模数据(避免内存溢出)?
核心答案:
- 使用生成器:逐块读取而非一次性加载全部。如
pd.read_csv(chunksize=10000)。 - 惰性求值:使用生成器表达式代替列表推导式。
- 内存映射文件:
numpy.memmap将大文件映射到虚拟内存。 - 外部排序:数据分块排序后归并。
- 使用高效数据结构:
array.array(同质数字)或__slots__限制实例属性。 - 数据库/数据仓库:超出内存时使用SQLite、DuckDB或Spark。
# 示例:使用chunksize处理大型CSV
import pandas as pd
def process_large_csv(file_path, chunk_size=10000):
"""分块处理大文件,避免内存爆炸"""
total_sum = 0
chunk_count = 0
for chunk in pd.read_csv(file_path, chunksize=chunk_size):
# 只保留需要的列,减少内存
chunk = chunk[['col1', 'col2']]
total_sum += chunk['col1'].sum()
chunk_count += 1
if chunk_count % 10 == 0:
print(f"已处理 {chunk_count} 块")
return total_sum
数据结构与算法
数据结构与算法是编程面试的绝对核心。大厂面试中,手撕代码环节占30-40分钟,题目通常来自LeetCode Top 100或经典题型。
基础数据结构速览
| 数据结构 | 实现 | 查找 | 插入 | 删除 | 适用场景 |
|---|---|---|---|---|---|
| 数组 | 连续内存 | O(1)索引 | O(n) | O(n) | 随机访问频繁 |
| 链表 | 节点+指针 | O(n) | O(1)头插 | O(1)已知前驱 | 频繁插入删除 |
| 栈 | 数组/链表 | O(1)栈顶 | O(1) | O(1) | 括号匹配、DFS |
| 队列 | 数组/链表 | O(1)队首 | O(1) | O(1) | BFS、任务调度 |
| 哈希表 | 哈希函数+桶 | O(1)均摊 | O(1)均摊 | O(1)均摊 | 快速查找、去重 |
| 二叉树 | 节点+左右孩子 | O(log n)平衡 | O(log n) | O(log n) | 搜索、排序 |
| 红黑树 | 自平衡BST | O(log n) | O(log n) | O(log n) | C++ map、Java TreeMap |
| 图 | 邻接矩阵/表 | O(1)矩阵 | O(1)表 | O(1)表 | 网络关系、路径规划 |
高频算法
排序算法(重点掌握快排、归并、堆排)
def quick_sort(arr, left=0, right=None):
"""
快速排序:分治思想,原地排序
时间复杂度:平均O(n log n),最坏O(n²)(已排序时)
空间复杂度:O(log n)递归栈
优化:随机选pivot、三数取中、小数组切插入排序
"""
if right is None:
right = len(arr) - 1
if left >= right:
return
# 三数取中优化(避免最坏情况)
mid = (left + right) // 2
if arr[left] > arr[mid]:
arr[left], arr[mid] = arr[mid], arr[left]
if arr[left] > arr[right]:
arr[left], arr[right] = arr[right], arr[left]
if arr[mid] > arr[right]:
arr[mid], arr[right] = arr[right], arr[mid]
# 将中位数放到right-1位置作为pivot
pivot = arr[mid]
arr[mid], arr[right-1] = arr[right-1], arr[mid]
# 分区操作(双指针)
i, j = left, right - 1
while i < j:
i += 1
while arr[i] < pivot:
i += 1
j -= 1
while arr[j] > pivot:
j -= 1
if i < j:
arr[i], arr[j] = arr[j], arr[i]
# 将pivot放到正确位置
arr[i], arr[right-1] = arr[right-1], arr[i]
quick_sort(arr, left, i-1)
quick_sort(arr, i+1, right)
def merge_sort(arr):
"""
归并排序:分治+合并,稳定排序
时间复杂度:O(n log n),空间复杂度:O(n)
适合链表排序、外部排序
"""
if len(arr) <= 1:
return arr
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
def heap_sort(arr):
"""
堆排序:利用堆性质,原地排序
时间复杂度:O(n log n),空间O(1)
不稳定(堆调整会打乱相等元素顺序)
"""
def heapify(n, i):
"""调整以i为根的子树为最大堆"""
largest = i
left = 2 * i + 1
right = 2 * i + 2
if left < n and arr[left] > arr[largest]:
largest = left
if right < n and arr[right] > arr[largest]:
largest = right
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i]
heapify(n, largest)
n = len(arr)
# 建堆(从最后一个非叶子节点开始)
for i in range(n // 2 - 1, -1, -1):
heapify(n, i)
# 依次取出堆顶
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i]
heapify(i, 0)
if __name__ == "__main__":
test_arr = [3, 7, 2, 8, 1, 9, 4, 6, 5]
quick_sort(test_arr.copy())
print("快排结果:", test_arr)
查找算法
def binary_search(arr, target):
"""
二分查找:有序数组
时间复杂度:O(log n)
返回目标索引,不存在返回-1
"""
left, right = 0, len(arr) - 1
while left <= right:
mid = left + (right - left) // 2 # 防止溢出
if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
# 哈希查找(O(1)平均)
def two_sum(nums, target):
"""
两数之和:使用哈希表一次遍历
返回满足 nums[i]+nums[j]=target 的索引对
"""
seen = {}
for i, num in enumerate(nums):
complement = target - num
if complement in seen:
return [seen[complement], i]
seen[num] = i
return []
动态规划(DP)核心思想
核心思想:将原问题分解为重叠子问题,通过保存子问题的解避免重复计算。适用于最优子结构问题。
三要素:
- 状态定义:
dp[i]表示什么? - 状态转移方程:
dp[i] = f(dp[i-1], dp[i-2], ...) - 边界条件:初始状态的值
def longest_increasing_subsequence(nums):
"""
最长递增子序列(LIS)
时间复杂度 O(n log n),贪心+二分
返回长度
"""
import bisect
tails = [] # tails[i] 表示长度为i+1的递增子序列的最小末尾值
for num in nums:
pos = bisect.bisect_left(tails, num)
if pos == len(tails):
tails.append(num)
else:
tails[pos] = num
return len(tails)
def knapsack_01(weights, values, capacity):
"""
0-1背包问题:动态规划
时间复杂度 O(n * capacity)
返回最大价值
"""
n = len(weights)
dp = [0] * (capacity + 1)
for i in range(n):
# 倒序遍历防止重复使用同一个物品
for w in range(capacity, weights[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weights[i]] + values[i])
return dp[capacity]
BFS/DFS
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def level_order_traversal(root):
"""
二叉树的层序遍历(BFS)
返回二维列表,每层一个子列表
LeetCode 102
"""
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.popleft()
current_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(current_level)
return result
def binary_tree_dfs(root):
"""二叉树深度优先遍历(前序)"""
def dfs(node):
if not node:
return
print(node.val, end=" ")
dfs(node.left)
dfs(node.right)
dfs(root)
大厂高频编程题
1. 两数之和(LeetCode 1)
def two_sum(nums, target):
"""哈希表一次遍历"""
seen = {}
for i, num in enumerate(nums):
if target - num in seen:
return [seen[target - num], i]
seen[num] = i
return []
2. 二叉树的层序遍历(LeetCode 102)
见上述level_order_traversal函数。
3. 最长递增子序列(LeetCode 300)
见上述longest_increasing_subsequence函数。
4. 背包问题(经典DP)
见上述knapsack_01函数。
5. TopK问题(堆实现 / 快排改进)
import heapq
def topk_heap(nums, k):
"""
使用最小堆求TopK(最大K个数)
时间复杂度 O(n log k),空间 O(k)
适合k较小或数据流场景
"""
if not nums or k <= 0:
return []
heap = []
for num in nums:
if len(heap) < k:
heapq.heappush(heap, num)
else:
if num > heap[0]:
heapq.heapreplace(heap, num)
return heap
def topk_quickselect(nums, k):
"""
基于快排partition的快速选择算法
时间复杂度 平均O(n),最坏O(n²)
原地修改,返回前k大元素(无序)
"""
if not nums or k <= 0:
return []
k = min(k, len(nums))
def partition(left, right):
"""分区,返回pivot的最终位置(降序)"""
pivot = nums[right]
i = left
for j in range(left, right):
if nums[j] >= pivot: # 降序:大的放左边
nums[i], nums[j] = nums[j], nums[i]
i += 1
nums[i], nums[right] = nums[right], nums[i]
return i
def quick_select(left, right, k_idx):
if left == right:
return
p = partition(left, right)
if p == k_idx:
return
elif p < k_idx:
quick_select(p+1, right, k_idx)
else:
quick_select(left, p-1, k_idx)
quick_select(0, len(nums)-1, k-1)
return nums[:k]
# 测试
if __name__ == "__main__":
data = [3, 2, 1, 5, 6, 4, 8, 7, 9]
print("堆方法 Top3:", topk_heap(data, 3))
print("快选方法 Top3:", topk_quickselect(data.copy(), 3))
高频考题:快排的时间复杂度及优化方法?
核心答案:
- 时间复杂度:平均O(n log n),最坏O(n²)(当每次pivot都是最小或最大值,如已排序数组)。
- 优化方法:
- 随机选择pivot:避免固定顺序导致的最坏情况。
- 三数取中:取左端、中间、右端三个元素的中位数作为pivot。
- 小数组切换插入排序:当子数组长度小于阈值(如10-20)时,插入排序效率更高。
- 三路快排:将数组分为小于、等于、大于pivot三部分,处理大量重复元素时性能优异。
高频考题:哈希表的冲突解决方式?
核心答案:
- 链地址法:每个桶是一个链表(或红黑树)。Java 8+中,链表长度超过8转为红黑树。Python dict采用伪随机探测+链地址混合。
- 开放地址法:冲突时探测下一个空闲位置。探测方式有线性探测(
i+1)、二次探测(i+c·k²)、双重哈希(hash2(key))。 - 再哈希法:使用多个哈希函数,冲突时换另一个。
- 建立公共溢出区:将冲突元素放入单独的溢出表。
Python的dict使用伪随机探测(基于j = ((5*j) + 1) & mask模式)结合开放地址法,且当负载因子超过2/3时会扩容。
高频考题:动态规划的核心思想及应用场景?
核心答案:
核心思想:将复杂问题分解为重叠子问题,通过记忆化(memoization)或自底向上填表避免重复计算。核心判断依据:问题是否具有最优子结构(大问题的最优解包含子问题的最优解)和重叠子问题(子问题被反复计算)。
应用场景:
- 最优化问题(最长路径、最小编辑距离、背包最大价值)
- 计数问题(不同路径、爬楼梯方式数)
- 可行性问题(能否分割等和子集、正则表达式匹配)
经典例子:斐波那契数列、最长公共子序列(LCS)、编辑距离(Levenshtein)、股票买卖最佳时机。
常用工具库
AI算法工程师日常离不开NumPy、Pandas、Matplotlib。面试中常考察核心操作的熟练度。
NumPy(数组运算)
import numpy as np
# 创建数组
arr = np.array([1, 2, 3, 4])
zeros = np.zeros((3, 4))
ones = np.ones((2, 3))
eye = np.eye(5) # 单位矩阵
lin = np.linspace(0, 1, 10) # 等间距10个点
rand = np.random.randn(100, 5) # 标准正态分布
# 数组运算(广播)
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([10, 20, 30])
result = a + b # 广播:b自动扩展为2x3
# [[11,22,33], [14,25,36]]
# 索引与切片
arr = np.arange(12).reshape(3, 4)
# [[0,1,2,3],
# [4,5,6,7],
# [8,9,10,11]]
col2 = arr[:, 1] # [1,5,9]
sub = arr[0:2, 1:3] # [[1,2], [5,6]]
mask = arr > 5
arr[mask] = 0 # 布尔索引赋值
# 统计操作
mean = np.mean(arr, axis=0) # 按列求均值
std = np.std(arr, axis=1) # 按行求标准差
cov = np.cov(arr.T) # 协方差矩阵
高频考题:NumPy中广播机制的原理?
核心答案:广播是NumPy对不同形状数组进行算术运算的规则。原理是:从尾部维度开始比较,两个数组的维度要么相等,要么其中一个维度为1,要么缺失的维度视为1。满足条件时,维度为1的数组会沿该方向“复制”扩展。
示例:形状(3,4)的数组与形状(4,)的数组相加,后者自动扩展为(3,4);形状(3,1)与(1,4)相加得到(3,4)。广播避免了显式循环和内存复制,效率高。
Pandas(数据清洗、聚合)
import pandas as pd
# 创建DataFrame
df = pd.DataFrame({
'name': ['Alice', 'Bob', 'Charlie', 'Diana'],
'age': [25, 30, 35, None],
'score': [85, 92, 78, 88],
'city': ['NY', 'LA', 'NY', 'LA']
})
# 查看信息
df.head()
df.info()
df.describe()
# 处理缺失值
df.isnull().sum()
df_filled = df.fillna({'age': df['age'].median()}) # 中位数填充
df_dropped = df.dropna() # 删除缺失行
# 数据筛选与变换
high_score = df[df['score'] > 80]
df['age'] = df['age'].astype(int)
df['score_bin'] = pd.cut(df['score'], bins=[0,60,80,100], labels=['低','中','高'])
# 分组聚合
grouped = df.groupby('city').agg({
'age': 'mean',
'score': ['max', 'min']
})
print(grouped)
# 合并操作
df2 = pd.DataFrame({'name': ['Alice', 'Bob'], 'salary': [70000, 80000]})
merged = pd.merge(df, df2, on='name', how='left')
# 数据透视表
pivot = pd.pivot_table(df, values='score', index='city', columns='score_bin', aggfunc='count')
高频考题:如何用Pandas处理缺失值?
核心答案:
- 检测:
df.isnull().sum()查看每列缺失数量。 - 删除:
df.dropna(axis=0, subset=['col1','col2'])删除指定列有缺失的行。 - 填充:
- 数值列:
df.fillna({'col': df['col'].median()})(中位数抗异常值)或mean()。 - 分类列:
df['col'].fillna(df['col'].mode()[0])(众数)或固定值'Unknown'。 - 前向/后向填充:
df.fillna(method='ffill')(时间序列常用)。 - 插值:
df.interpolate(method='linear')。
- 数值列:
- 建模预测填充:将缺失列作为目标,用其他特征预测(如KNN、随机森林)。
Matplotlib / Seaborn(可视化)
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体(避免乱码)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 折线图(趋势分析)
x = [1, 2, 3, 4, 5]
y = [2, 3, 5, 7, 11]
plt.figure(figsize=(10, 6))
plt.plot(x, y, marker='o', linewidth=2, label='训练损失')
plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('训练曲线')
plt.legend()
plt.grid(True, alpha=0.3)
# 直方图(分布分析)
data = np.random.randn(1000)
plt.figure()
plt.hist(data, bins=30, density=True, alpha=0.7, color='steelblue', edgecolor='black')
plt.title('数据分布直方图')
plt.xlabel('值')
plt.ylabel('频率密度')
# 热力图(相关性矩阵)
df_corr = df[['age', 'score']].corr()
plt.figure(figsize=(6, 5))
sns.heatmap(df_corr, annot=True, cmap='coolwarm', vmin=-1, vmax=1,
square=True, linewidths=0.5)
plt.title('特征相关性热力图')
# 散点图(关系分析)
plt.figure()
sns.scatterplot(x='age', y='score', hue='city', data=df, s=100)
plt.title('年龄与分数的关系')
plt.show()
面试准备建议:
- 手撕代码训练:每天2-3道LeetCode(优先Top 100和剑指Offer),重点掌握数组、链表、哈希表、树、动态规划、贪心。写代码时注意边界条件、命名规范、复杂度分析。
- 深入理解而非死记:不仅要能写出快排,还要能分析最坏情况、说出优化方法;不仅会用装饰器,还要能解释闭包和
wraps的作用。 - 工具库熟练度:能够在不查文档的情况下用NumPy完成矩阵运算,用Pandas完成数据清洗和分组聚合。面试中可能会要求手写Pandas操作。
- 复杂度敏感:每道题主动分析时间和空间复杂度,面试官非常看重这一点。
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)