1. 项目背景与核心价值
最近在整理C++机试题目时,发现t100-t103这组题目特别适合用来检验基础算法和数据结构的掌握程度。这四道题涵盖了字符串处理、动态规划、图论等经典题型,都是大厂笔试中的高频考点。我在实际面试辅导中发现,很多候选人在面对这类题目时容易陷入"知道思路但写不出完整代码"的困境。
这组题目最精妙的地方在于,它们表面看起来是独立的四道题,但实际上存在递进关系。t100考察基础编码能力,t101增加时间复杂度要求,t102引入空间优化思路,t103则综合前三个问题的技巧。这种设计方式与真实面试中的题目演进逻辑高度一致。
2. 题目详解与解题思路
2.1 T100:字符串模式匹配
题目要求实现一个支持通配符的字符串匹配函数,其中'?'匹配任意单个字符,'*'匹配任意长度字符串(包括空串)。这是LeetCode 44题的变种,也是实际开发中文件搜索功能的简化版。
暴力递归解法时间复杂度O(2^(m+n)),显然无法通过。正确的DP解法需要构建(m+1)*(n+1)的二维数组:
cpp复制bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
vector<vector<bool>> dp(m+1, vector<bool>(n+1, false));
dp[0][0] = true;
for(int j=1; j<=n; j++) {
if(p[j-1] == '*') dp[0][j] = dp[0][j-1];
}
for(int i=1; i<=m; i++) {
for(int j=1; j<=n; j++) {
if(p[j-1] == '*') {
dp[i][j] = dp[i][j-1] || dp[i-1][j];
} else if(p[j-1] == '?' || s[i-1] == p[j-1]) {
dp[i][j] = dp[i-1][j-1];
}
}
}
return dp[m][n];
}
关键点:初始化第一行时要特殊处理连续的'',因为空字符串可以匹配多个''
2.2 T101:矩阵中的最长递增路径
这道题是LeetCode 329的变种,给定一个整数矩阵,找到最长严格递增路径的长度。从任意单元格出发,只能向上下左右移动。
记忆化DFS是最直观的解法,但需要注意三个优化点:
- 使用dirs数组简化方向处理
- 用memo矩阵缓存已计算结果
- 提前判断邻居是否有效
cpp复制vector<vector<int>> dirs = {{-1,0},{1,0},{0,-1},{0,1}};
int longestIncreasingPath(vector<vector<int>>& matrix) {
if(matrix.empty()) return 0;
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> memo(m, vector<int>(n, 0));
int res = 0;
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
res = max(res, dfs(matrix, i, j, memo));
}
}
return res;
}
int dfs(vector<vector<int>>& mat, int i, int j, vector<vector<int>>& memo) {
if(memo[i][j] != 0) return memo[i][j];
int maxLen = 1;
for(auto& dir : dirs) {
int x = i + dir[0], y = j + dir[1];
if(x>=0 && x<mat.size() && y>=0 && y<mat[0].size() && mat[x][y]>mat[i][j]) {
maxLen = max(maxLen, 1 + dfs(mat, x, y, memo));
}
}
memo[i][j] = maxLen;
return maxLen;
}
2.3 T102:会议室安排问题
这是典型的区间调度问题,给定一组会议室的预约时间[start,end],计算最多能安排多少个不冲突的会议。LeetCode 253的简化版。
贪心算法按结束时间排序是最优解:
cpp复制int maxMeetings(vector<vector<int>>& intervals) {
if(intervals.empty()) return 0;
sort(intervals.begin(), intervals.end(), [](auto& a, auto& b){
return a[1] < b[1];
});
int count = 1, end = intervals[0][1];
for(int i=1; i<intervals.size(); i++) {
if(intervals[i][0] >= end) {
count++;
end = intervals[i][1];
}
}
return count;
}
常见误区:按开始时间排序会导致错误结果。例如[[1,10],[2,3],[4,5]],正确结果是2而非1
2.4 T103:二叉树中的最大路径和
LeetCode 124原题,要求找到二叉树中任意节点到任意节点的路径,使得路径和最大。路径至少包含一个节点。
后序遍历的递归解法需要注意:
- 空节点返回0
- 左右子树返回值如果是负数则舍弃
- 全局变量记录最大值
cpp复制struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
int maxPathSum(TreeNode* root) {
int maxSum = INT_MIN;
helper(root, maxSum);
return maxSum;
}
int helper(TreeNode* node, int& maxSum) {
if(!node) return 0;
int left = max(0, helper(node->left, maxSum));
int right = max(0, helper(node->right, maxSum));
maxSum = max(maxSum, left + right + node->val);
return max(left, right) + node->val;
}
3. 核心算法对比与优化技巧
3.1 四种题型的时空复杂度分析
| 题目 | 最佳解法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|---|
| T100 | 动态规划 | O(mn) | O(mn) | 通配符匹配 |
| T101 | 记忆化DFS | O(mn) | O(mn) | 矩阵搜索 |
| T102 | 贪心算法 | O(nlogn) | O(1) | 区间调度 |
| T103 | 后序遍历 | O(n) | O(h) | 树形DP |
3.2 调试技巧与边界条件
-
字符串匹配:特别注意空字符串与通配符的匹配关系
- 测试用例:s="", p="*****" 应该返回true
- 测试用例:s="a", p="" 应该返回false
-
矩阵DFS:矩阵为空或单元素的情况要单独处理
- 测试用例:matrix=[[]] 应该返回0
- 测试用例:matrix=[[1]] 应该返回1
-
区间问题:区间完全重叠和边界相等的情况
- 测试用例:[[1,2],[1,2]] 应该返回1
- 测试用例:[[1,2],[2,3]] 应该返回2
-
二叉树路径:节点值为负数时的处理
- 测试用例:[-3] 应该返回-3
- 测试用例:[2,-1] 应该返回2
4. 实战经验与避坑指南
4.1 机试中的常见失误
-
过度优化:在T101中过早尝试用拓扑排序,反而增加了实现难度。记忆化DFS在笔试中更容易快速实现。
-
全局变量滥用:T103的maxSum如果作为类成员变量,可能在多次调用时产生错误。更好的做法是作为引用参数传递。
-
STL使用不当:
cpp复制// 错误写法:可能越界 for(int i=0; i<=matrix.size(); i++) // 正确写法 for(int i=0; i<matrix.size(); i++)
4.2 代码风格建议
-
统一命名规范:
- DP表用dp命名
- 临时变量用tmp前缀
- 结果值用res命名
-
防御性编程:
cpp复制// 不安全的访问 int val = matrix[i][j]; // 安全写法 if(i>=0 && i<matrix.size() && j>=0 && j<matrix[0].size()) { int val = matrix[i][j]; } -
注释技巧:
- 在复杂逻辑前用//标记关键步骤
- 在函数开头用///说明参数含义
- 避免每行都注释,只在关键算法处说明
5. 扩展训练建议
5.1 同类题目推荐
-
字符串匹配进阶:
- LeetCode 10:正则表达式匹配
- LeetCode 139:单词拆分
-
矩阵搜索变种:
- LeetCode 130:被围绕的区域
- LeetCode 200:岛屿数量
-
区间问题扩展:
- LeetCode 56:合并区间
- LeetCode 435:无重叠区间
-
树形DP进阶:
- LeetCode 337:打家劫舍III
- LeetCode 543:二叉树直径
5.2 系统训练方法
-
分类刷题法:按算法类型集中练习,比如连续两周专攻动态规划
-
模拟考试法:设置2小时完成4道题,模拟真实笔试环境
-
错题重做法:建立错题本,每周重做之前的错误题目
-
代码复审法:写完代码后,用下面的检查清单自查:
- 边界条件是否处理?
- 变量初始化是否正确?
- 递归终止条件是否完备?
- 时间复杂度是否最优?
在实际面试中,解题速度固然重要,但代码的健壮性和可读性同样关键。建议平时练习时养成写单元测试的习惯,对每个函数至少设计3-5个测试用例,包括常规情况和边界情况。例如对T100的测试用例可以这样设计:
cpp复制void testIsMatch() {
assert(isMatch("aa", "a") == false);
assert(isMatch("aa", "*") == true);
assert(isMatch("cb", "?a") == false);
assert(isMatch("adceb", "*a*b") == true);
assert(isMatch("", "****") == true);
cout << "All test cases passed!" << endl;
}
这种训练方式虽然前期耗时较多,但长期来看能显著提高一次通过率。我在辅导学员的过程中发现,养成这种严谨习惯的候选人,在实际面试中的表现往往更加稳定出色。