Maxw的小站

Maxw学习记录

56. 合并区间

错误分析

  1. 合并逻辑错误(关键问题):
    1
    intervals[i] = (intervals[i-1][0], intervals[i][1]); // 错误写法
    • 语法错误:使用逗号运算符 (a, b) 实际返回 b,导致区间被错误赋值为 [0, intervals[i][1]](起始点丢失)。
    • 逻辑错误:合并时结束点未取两者最大值。正确做法:结束点应为 max(intervals[i-1][1], intervals[i][1]),否则可能丢失覆盖范围(如合并 [1,5][2,3] 会得到 [1,3] 而非 [1,5])。
  2. 重叠条件不精确
    1
    if(intervals[i-1][1] >= intervals[i][0]) // 边界情况处理不当
    • 题目要求:若前区间结束点 等于 后区间起始点(如 [1,2][2,3]),应合并为 [1,3]。当前条件 >= 正确,但合并操作错误导致结果异常。
  3. 性能问题
    • 拷贝开销cmp 函数参数为传值(vector<int> a, b),应改为 传引用const vector<int>& a, b)避免不必要的拷贝。
    • 额外空间:使用 incl 标记数组非必需,经典解法可优化空间复杂度。
  4. 边界处理缺失
    • 当输入为空时,应直接返回空数组,但代码未显式处理 intervals.size() == 0 的情况(依赖 size <= 1 返回,逻辑正确但不够清晰)。

比较合并贪心解法(可以ac)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution {
public:
static bool cmp(const vector<int>& a, const vector<int>& b) { // 传引用
return a[0] < b[0] || (a[0] == b[0] && a[1] < b[1]);
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
if (intervals.size() <= 1) return intervals;
vector<bool> incl(intervals.size(), true);
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i-1][1] >= intervals[i][0]) {
// 正确合并:起始点取前一个,结束点取两者最大值
intervals[i][0] = intervals[i-1][0];
intervals[i][1] = max(intervals[i-1][1], intervals[i][1]);
incl[i-1] = false;
}
}
vector<vector<int>> result;
for (int j = 0; j < intervals.size(); j++) {
if (incl[j]) result.push_back(intervals[j]);
}
return result;
}
};

经典贪心解法思路

  1. 按起始点排序区间
  2. 动态合并
    • 初始化结果数组 merged
    • 遍历每个区间:
      • merged 为空 当前区间与 merged 最后一个区间不重叠(current[0] > merged.back()[1]),直接加入。
      • 否则,更新 merged 最后一个区间的结束点max(merged.back()[1], current[1])

经典贪心解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.empty()) return {};
sort(intervals.begin(), intervals.end());
vector<vector<int>> merged;
for (const auto& interval : intervals) {
if (merged.empty() || merged.back()[1] < interval[0]) {
merged.push_back(interval);
} else {
merged.back()[1] = max(merged.back()[1], interval[1]);
}
}
return merged;
}
};

738.单调递增的数字

题目要求小于等于N的最大单调递增的整数,那么拿一个两位的数字来举例。 例如:98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想strNum[i - 1]--,然后strNum[i]给为9,这样这个整数就是89,即小于98的最大的单调递增整数。 > 这一点如果想清楚了,这道题就好办了。

此时是从前向后遍历还是从后向前遍历呢? 从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。 这么说有点抽象,举个例子,数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。 那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299

处理逻辑易错

  • 当发现 digits[i-1] < digits[i] 时,代码只修改了当前低位(设为9)和当前高位(减1),但没有处理更高位的影响
    • 高位减1后可能破坏与更前高位的单调性(例:100 的处理)
    • 未将所有受影响低位置为 9(例:100 的个位未置9)
  • 示例验证(输入 n=100): 分解 digits = [0, 0, 1] # 个位=0, 十位=0, 百位=1 遍历 i=1:0==0 → 跳过 遍历 i=2:0<1 → 十位置9, 百位减1 → digits=[0,9,0] 结果 = 010⁰ + 910¹ + 0*10² = 90 # 预期应为99

数字组合计算易错

  • 错误代码res += 10 * (i-1) * digits[i-1]
  • 问题
    • 错误使用乘法 10*(i-1) 而非指数 10^(i-1)
    • 导致计算结果完全错误(如个位贡献 10*0*digits[0]=0
  • 正确计算:应使用 pow(10, i-1) * digits[i-1]

💡 黄金法则:需要修改数字的每一位时,优先考虑 to_string → 字符串处理 → stoi 流程,比数学运算更直观可靠!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int monotoneIncreasingDigits(int n) {
string str = to_string(n);
int flag = str.size();
for(int i = (str.size() - 1);i>0;i--){
if(str[i-1] > str[i]){
flag = i;
str[i-1]--;
}
}
for(int i = 0; i<str.size();i++){
if(i >= flag){
str[i] = '9';
}
}
return stoi(str);
}
};

968.监控二叉树

其实我的思路和题解的想法已经很接近了,但是总结地没有那么到位,导致缺漏数量的情况

确定遍历顺序

在二叉树中可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。

1
2
3
4
5
6
7
8
int traversal(TreeNode* cur) {
// 空节点,该节点有覆盖
if (终止条件) return ;
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
逻辑处理 // 中
return ;
}

返回状态类型

  • 0:该节点不在摄像头覆盖范围
  • 1:本节点有摄像头
  • 2:本节点在摄像头覆盖范围

递归的终止条件应该是遇到了空节点,此时应该返回2(有覆盖),这样就可以在叶子节点的父节点放摄像头了。

1
2
// 空节点,该节点有覆盖
if (cur == NULL) return 2;
完整代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
int cmr = 0;
int coverStat(TreeNode* root){
if(!root)return 2;
int left = coverStat(root->left);
int right = coverStat(root->right);
if(left == 2 && right == 2){
return 0;
}
if(left == 0 || right == 0){
cmr++;
return 1;
}
if(left == 1 || right == 1){
return 2;
}
return -1;
}
int minCameraCover(TreeNode* root) {
int rcv = coverStat(root);
if(rcv == 0)cmr++;
return cmr;
}
};

452. 用最少数量的箭引爆气球

💡 题解逻辑如下:

  • 如果当前气球的 起点 > 上一个交集的最右端点,说明它不能和之前的箭共用,需要再发一支箭。
    • 更新 end 为当前气球的 end,建立新的交集窗口。
  • 否则,当前气球和前面的箭是重叠的,它们能一起被射中。
    • 更新 end = min(end, points[i][1]) 是为了缩小交集范围,因为未来能共用这支箭的气球,必须跟所有已合并气球都有交集。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      class Solution {
      public:
      static bool cmp(vector<int> a, vector<int> b){
      return a[0] < b[0];

      }
      int findMinArrowShots(vector<vector<int>>& points) {
      sort(points.begin(), points.end(), cmp);
      int arr = 1;
      if(points.size() == 0)return 0;
      int end = points[0][1];
      for(int i = 1; i<points.size();i++){
      if(end < points[i][0]){
      end = points[i][1];
      arr++;
      }else{
      end = min(end, points[i][1]);
      }
      }
      return arr;
      }
      };

435. 无重叠区间

思路步骤:

  1. 排序区间(预处理):
    • 使用自定义比较器 cmp 对所有区间进行排序。
    • 排序规则:按起始点升序(优先比较 intervals[i][0])。若起始点相同,则按结束点升序(比较 intervals[i][1])。
    • 按结束点排序效率可以更高
  2. 初始化关键变量
    • 最大不重叠区间计数器 intv = 1(至少包含第一个区间)。
    • 当前覆盖的结束点 end = intervals[0][1](以第一个区间的结束点作为初始基准)。
  3. 遍历处理每个区间(从第二个区间开始,即 i = 1):
    • 检查重叠:比较当前区间起始点 intervals[i][0] 与当前结束点 end
      • 情况1:有重叠intervals[i][0] < end):
        • 更新 end = min(end, intervals[i][1])(关键贪心选择:取最小结束点)。
        • 不增加计数器 intv(因为重叠区间不能同时保留,这里隐含选择了结束点更小的区间以正确计算无重叠区间个数)。
      • 情况2:无重叠intervals[i][0] >= end):
        • 更新 end = intervals[i][1](将结束点设为当前区间的结束点)。
        • 增加计数器 intv++(表示成功添加一个新的不重叠区间)。
  4. 计算结果
    • 需要移除的最小区间数 = 总区间数 intervals.size() - 最大不重叠区间数 intv
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
static bool cmp(vector<int> a, vector<int>b){
if(a[0] < b[0]){
return true;
}else if(a[0] == b[0] && a[1] < b[1]){
return true;
}
return false;
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end(), cmp);
// for(auto i: intervals){
// cout<<i[0]<<" "<<i[1]<<", ";
// }
int intv = 1;
int end = intervals[0][1];
if(intervals.size() <= 1)return 0;
for(int i = 1; i< intervals.size();i++){
if(intervals[i][0] < end){
end = min(end, intervals[i][1]);
}else{
end = intervals[i][1];
intv++;
}
}
return intervals.size() - intv;
}
};

763.划分字母区间

由题解: 在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。 可以分为如下两步:

  • 统计每一个字符最后出现的位置
  • 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
vector<int> partitionLabels(string s) {
int hash[27] = {0};
for(int i = 0;i<s.size();i++){
hash[s[i] - 'a'] = i;
}
vector<int> result;
int right = 0;
int end = 0;
for(int i = 0;i<s.size();i++){
right = max(right, hash[s[i] - 'a']);
if(right == i){
result.push_back(right - end +1);
right++;
end = right;
}
}
return result;
}
};

134. 加油站

初始思路:

  1. 先找一段正收益连续区间(油量盈余最大的起点)
    • curMax 累加正收益段,记录最大盈余时的起点 maxSt
    • 如果遇到油不够的站点,就重置 curMax 并将 start 改为下一个站点。
    • 如果到最后一站没找完,循环一圈回来继续查找。
  2. 再从 maxSt 模拟绕一圈
    • maxSt 出发,逐站加减油量,确认是否能绕完整圈。
    • 只要中途油量不足就返回 -1

⚠️ 存在的问题:

其实这种逻辑完善完善也是可以做出来的,题解中有,就是比较繁琐

✅ 更优的做法(贪心 + 一次遍历):

只要 总油量 ≥ 总花费,一定有解。解的起点是:当前累加油量一旦为负,就从下一个站点重启。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int canCompleteCircuit(vector<int>& gas, vector<int>& cost) {
int tank = 0;
int startInd = 0;
int total = 0;
for(int i = 0; i < gas.size(); i++){
int store = gas[i] - cost[i];
total += store;
tank += store;
if(tank < 0){
startInd = i + 1;
tank = 0;
}
}
return total < 0 ? -1 : startInd;
}
};

135. 分发糖果

这在leetcode上是一道困难的题目,其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼。 题解采用了两次贪心的策略: * 一次是从左到右遍历,只比较右边孩子评分比左边大的情况。 * 一次是从右到左遍历,只比较左边孩子评分比右边大的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
int candy(vector<int>& ratings) {
vector<int> candy;
candy.push_back(1);
for(int i = 1;i<ratings.size();i++){
if(ratings[i] > ratings[i-1]){
candy.push_back(candy[i-1] + 1);
}else{
candy.push_back(1);
}
}
int total = 0;
for(int i = (ratings.size()-1); i>0;i--){
if(ratings[i-1] > ratings[i] && candy[i-1] <= candy[i]){
candy[i-1] = candy[i] + 1;
}
total += candy[i];
}
total += candy[0];
return total;
}
};

860.柠檬水找零

这题不用map,用两个整数表示5和10的钞票数也是可以的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
bool lemonadeChange(vector<int>& bills) {
map<int, int> changes;
changes[5] = 0;
changes[10] = 0;
for(int i = 0; i<bills.size();i++){
int exc = bills[i] - 5;
if(exc == 0){
changes[5]++;
}else if(exc == 5){
changes[5]--;
changes[10]++;
}else if(exc == 15){
if(changes[10] >= 1){
changes[10]--;
changes[5]--;
}else{
changes[5] -= 3;
}
}
if(changes[5] < 0)return false;
}
return true;
}
};

406.根据身高重建队列

看题解可知,当身高是按照从高到矮,第二个数字从小到大排列的时候,按顺序插入即可满足条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
public:
static bool cmp(const vector<int> a, const vector<int> b){
if(a[0] > b[0]){
return true;
}else if(a[0] == b[0] && a[1] < b[1]){
return true;
}
return false;
}
vector<vector<int>> reconstructQueue(vector<vector<int>>& people) {
sort(people.begin(), people.end(), cmp);
list<vector<int>> que;
for(int i = 0; i<people.size();i++){
int insPos = people[i][1];
std::list<vector<int>>::iterator p = que.begin();
while(insPos > 0){
p++;
insPos--;
}
que.insert(p, people[i]);
}
return vector<vector<int>>(que.begin(), que.end());
}
};

122.买卖股票的最佳时机II

和之前的贪心算法题目一样,注意到局部最佳和整体的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
int maxProfit(vector<int>& prices) {
int prof = 0;
for(int i = 0; i < prices.size(); i++){
if(i > 0){
int left = prices[i] - prices[i-1];
if(left > 0)prof += left;
}
}
return prof;
}
};

55. 跳跃游戏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
bool canJump(vector<int>& nums) {
int maxi = 0;
for(int i = 0;i<nums.size();i++){
if(i < nums.size()-1 && nums[i] == 0 && maxi <= i)return false;
int fari = i + nums[i];
maxi = max(maxi, fari);
}
if(maxi >= nums.size() - 1){
return true;
}else{
return false;
}
}
};

45.跳跃游戏II

最开始的思路:

  • 定义了一个 minJumps 数组,其中 minJumps[i] 表示到达位置 i 所需的最少跳跃次数。
  • 初始用 minJumps[i] = i,默认每一步都跳一步。
  • 然后你从前向后遍历每个位置 i,对于它可以跳到的最远位置 reach = i + nums[i],尝试用 minJumps[i] + 1 来更新 minJumps[reach]
  • 最后返回的是 minJumps[n - 1]

问题和可改进点:

  1. 只更新了 reach,没更新从 i+1reach 的所有中间位置
    • 你只更新了 minJumps[reach],但跳到 [i+1, reach] 的每一个点其实都可能是通过 i 达到的,因此都可能能更新为 minJumps[i] + 1
  2. 时间复杂度仍是 O(n^2) 的最坏情况
    • 因为如果要更新 [i+1, reach] 中的所有点需要嵌套循环。

题解思路遍历过程

每次跳跃的起点在哪里不重要,关键是这次最远跳跃是否可以次数内完成 * 对每个位置 i,更新 far = max(far, i + nums[i]),表示当前范围内下一跳能跳到的最远位置。 * 当遍历到边界 i == beg 时,说明当前跳跃结束,需要再跳一次,并更新新的边界 beg = far。 * 如果新的 beg 已经到达或超过数组末尾,则可以提前 break

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int jump(vector<int>& nums) {
int minJump = 0;
int beg = 0;
int far = 0;
if(nums.size() == 1)return 0;
for(int i = 0; i<nums.size();i++){
far = max(far, i + nums[i]);
if(i == beg){
minJump++;
beg = far;
if(beg >= nums.size()-1)break;
}
}
return minJump;
}
};

1005.K次取反后最大化的数组和

最开始的思路

用了一个递归函数 lsum(nums, k) 来处理问题,思路如下: 1. 遍历数组找出最小值 minN 及其下标 minI; 2. nums[minI] 取反(也就是对当前最小值执行一次取反操作); 3. 继续递归调用 lsum(nums, k - 1); 4. 递归终止条件k == 0,此时返回整个数组的和; 5. 优化点尝试:当数组中最小值已经 ≥ 0,尝试用 k % 2 代替多余的取反操作(只取反一次或不取反)以减少递归次数。

当前实现存在的问题:

  1. 每次递归都重新遍历整个数组,效率低
    • 时间复杂度接近 O(k * n),最坏情况下可能是 10^5 * 10^3,会超时。
  2. 递归开销大
    • 每次递归都复制 nums 数组(因为是传引用再递归),可能带来严重的性能瓶颈。
  3. 贪心策略不够高效

推荐的优化思路:

采用排序 + 贪心策略来完成任务,流程如下: 1. 先将数组按绝对值从大到小排序; 2. 从左到右遍历数组,把负数取反,直到 k 用完或者没有负数了; 3. 如果还有剩余的 k 且是奇数,说明还有一次取反机会,把当前最小值(绝对值最小的数)再取反一次; 4. 最后返回数组的总和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int largestSumAfterKNegations(vector<int>& nums, int k) {
sort(nums.begin(), nums.end(), [](int a, int b){return abs(a) > abs(b);});
for(int i = 0; i<nums.size(); i++){
if(k == 0)break;
if(nums[i] < 0){
nums[i] = 0 - nums[i];
k--;
}
}
if(k % 2 == 1)nums[nums.size()-1] = 0 - nums[nums.size()-1];
int sum = 0;
for(int n: nums){
sum += n;
}
return sum;
}
};

455.分发饼干

这题题解上两个思路:

  • 思路1:优先考虑饼干,小饼干先喂饱小胃口
  • 思路2:优先考虑胃口,先喂饱大胃口

我这里优先考虑饼干

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int findC(vector<int>& g, vector<int>& s, int gStart, int sStart){
int count = 0;
if(gStart == g.size() || sStart == s.size())return count;
for(int i = sStart; i<s.size();i++){
if(s[i] >= g[gStart]){
count += 1;
count += findC(g, s, gStart+1, i+1);
break;
}
}
return count;
}
int findContentChildren(vector<int>& g, vector<int>& s) {
sort(g.begin(), g.end());
sort(s.begin(), s.end());
int cnt = findC(g, s, 0, 0);
return cnt;
}
};

376. 摆动序列

按题解思路,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度) 这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int waveCnt = 1;
vector<bool> isPos;
if(nums.size() == 1)return waveCnt;
for(int i = 1; i<nums.size();i++){
if(nums[i-1] == nums[i])continue; // 需要跳过相等的情况,不计入后面的判断条件中
if(nums[i-1] < nums[i]){
isPos.push_back(true);
}else if(nums[i-1] > nums[i]){
isPos.push_back(false);
}
if(i >= 1 && !isPos.empty() && isPos.size() == 1)waveCnt++;
if(!isPos.empty() && isPos.size() >= 2){
if(isPos[isPos.size()-1] != isPos[isPos.size()-2])waveCnt++;
}
}
return waveCnt;
}
};

53.最大子序和

由题解可以得知 如果 -2 1 在一起,计算起点的时候,一定是从 1 开始计算,因为负数只会拉低总和,这就是贪心贪的地方!

  • 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素“连续和”只会越来越小。
  • 全局最优:选取最大“连续和”

区间的终止位置,其实就是如果 count 取到最大值了,及时记录下来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int maxCnt = nums[0];
int cnt = 0;
for(int i=0; i<nums.size();i++){
cnt = cnt + nums[i];
if(cnt > maxCnt){// 要先赋值,再判断是否小于零,不然全是负数的情况会不正确
maxCnt = cnt;
}
if(cnt < 0){
cnt = 0;
continue;
}
}
return maxCnt;
}
};

把面试可能遇到的题目和回答存一下,面完来看看有哪些问到了

我的简历项目有四个:通用操作系统框架(Rust,研究生阶段跟着导师组里做的项目),多线程 TCP 服务器开发(Rust),基于贝叶斯优化的系统参数自动化调优框架(Python),智能菜市场物联网数据平台后端(Spring Boot,本科阶段实习的项目)

模拟面试题目:

一、 自我介绍与简历深挖

1. 请用3分钟左右的时间介绍一下你自己 (考察总结和表达能力,引导面试官关注你的亮点)

面试官您好!我是来自XX大学计算机科学专业的硕士研究生XX,非常荣幸能参加贵公司云架构平台部后台开发岗位的面试。

(背景 & 核心项目 - 1 min) 在研究生阶段,我的核心研究聚焦于构建高性能、可扩展的**操作系统框架**。我主导了其中**调度算法模块**的设计与实现。我的任务是将导师提出的新型**联合调度模型**落地到基于Rust开发的**微内核**框架中。该模型的核心是根据任务的**CPU占用**特征(高/低利用率)**动态匹配**最优调度策略(如EDF或Rate-Monotonic),以提升系统在超负荷以及**实时调度**场景下的整体效用。

为了实现并验证该模型:
充分利用**Rust的所有权机制**实现安全高效的**任务状态管理**
我设计了模块化的调度器接口,支持灵活集成和对比不同策略,并通过Linux内核工程实践(在Raspberry Pi上进行**内核模块定制**、交叉编译) 搭建了真实的测试环境。
我利用**perf**等工具采集并分析任务响应时间、CPU利用率等指标,量化验证了该联合调度模型在极端负载下的性能优势,达到了理论预期。这个项目不仅锻炼了我系统级编程(Rust)、内核原理和性能调优的能力,也让我深刻理解了高并发、实时性保障的挑战。

(其他项目经验 - 突出相关技术 - 45 sec) 除了研究项目,我也积极将技术应用于实践:
我独立设计并开发了一个高性能多线程TCP服务器(Rust)。采用Reactor模式和**非阻塞I/O**处理并发连接,精心设计了**线程安全的任务队列**和共享数据模型,降低了系统调度开销。通过优化,服务器在压力测试下峰值处理能力达到300+请求/秒,并保持了99.9%的运行时间可靠性。这强化了我对网络编程、并发控制和系统稳定性的理解。

在XX公司实习期间,我参与了智能菜市场物联网数据平台后端(Spring Boot/Java) 的开发。针对日均5000+设备数据的高并发接入场景,我参与了**分库分表方案设计**,并通过优化复杂查询(使用联合索引)降低关键接口耗时。我还负责了服务的Docker容器化部署,实现了资源的有效利用和快速扩容。这让我熟悉了企业级Java开发、数据库性能优化(MySQL/MyBatis)和容器化技术。

(技能总结 & 匹配岗位 - 30 sec) 通过这些经历,我熟练掌握**C++、Python**,并具备扎实的**Rust系统级开发**经验;深入理解操作系统、网络、并发编程原理;在分布式系统设计(分库分表)、高并发优化等方面有实际项目积累;并具备良好的问题定位和解决能力。

(结尾 - 表达热情与匹配 - 15 sec) 我了解到贵部门专注于**构建和优化云平台**的核心服务与基础设施,这正是我的兴趣所在。我的系统底层经验、性能优化意识、对分布式和云原生技术的热情,以及扎实的后端开发技能,都与这个岗位高度契合。我非常渴望能加入团队,贡献我的力量,并持续学习成长。谢谢

2. 通用操作系统框架(Rust):

*   你提到“**使用联合调度模型,为不同资源占用的任务匹配调度方式**”。能具体解释一下这个模型的设计思路吗?如何定义“不同资源占用”?调度策略具体有哪些?如何“匹配”?

> “好的,面试官。这个联合调度模型的设计核心思想是**根据周期性实时任务的特性,将其分类并分配到最适合其特性的调度策略上运行,以最大化整体系统的调度效率和资源利用率**。
>
> 1.  **定义‘不同资源占用’:** 我们主要依据任务的 **CPU占用率 (Utilization `U_i = C_i / T_i`)** 来区分任务类型。
>     *   **‘高资源占用’任务:** 通常指 `U_i` 较高(例如,接近或超过经典RM调度可调度上限~69%)的任务。这类任务往往是 **CPU密集型** 且对 **时间要求严格(如关键控制循环)**。
>     *   **‘低资源占用’任务:** 指 `U_i` 较低的任务。这类任务通常计算量小、周期相对较短,属于 **轻量级周期性任务(如数据采集、状态监控)**。
>
> 2.  **调度策略:** 我们为两类任务分别应用最匹配的经典实时调度算法:
>     *   **对于高资源占用/CPU密集型任务:** 采用 **EDF (最早截止时间优先)** 算法。EDF **动态地** 选择当前就绪队列中截止时间 (`D_i`) 最早的任务执行。它能在可调度条件下 **达到最高的CPU利用率(理论100%)**,非常适合于需要高利用率保障的关键任务。
>     *   **对于低资源占用/轻量级任务:** 采用 **RM (单调速率)** 算法。RM **静态地** 根据任务周期 (`T_i`) 分配优先级——周期越短(速率越高),优先级越高。它实现 **简单**,并且在满足可调度性条件下(`ΣU_i <= n(2^(1/n) - 1)`)能保证所有任务满足截止时间,特别适合大量低开销的周期任务。
>
> 3.  **如何‘匹配’:**
>     *   **任务分类与分配:** 在任务创建时(或系统初始化时),根据其声明的周期 (`T_i`) 和最坏执行时间 (`WCET, C_i`),计算其占用率 `U_i`。
>     *   **划分调度域:** 系统维护 **两个逻辑上的调度域 (Scheduling Domains)**:
>         *   **EDF 域:** 容纳所有被分类为“高资源占用”的任务。该域内部使用 **纯EDF算法** 进行调度。
>         *   **RM 域:** 容纳所有被分类为“低资源占用”的任务。该域内部使用 **纯RM算法** 进行调度。
>     *   **域间调度 (关键点):** 两个域之间的任务如何竞争CPU资源?我们采用了一种 **全局固定优先级仲裁** 策略:
>         *   **EDF域被赋予比RM域更高的全局固定优先级。** 这意味着,只要EDF域中有就绪任务,就先调度EDF域的任务(由EDF算法在其域内选一个)。仅当EDF域没有就绪任务时,才调度RM域的任务(由RM算法在其域内选一个)。
>         *   *(注:这是常见策略之一,也可以设计其他策略如全局EDF,但固定优先级隔离更简单,利于分析域间干扰)*。
>
> **设计思路总结与优势:**
> *   **核心目标:** 让 **CPU密集型且时间关键的任务 (高`U_i`) 享受EDF的高利用率优势**,同时让 **大量轻量级任务 (低`U_i`) 在RM的简单、鲁棒框架下运行**。
> *   **优势:** 相比于单一使用RM,该模型 **允许系统整体承载更高利用率** 的任务集(因为高U任务用了EDF)。相比于单一使用EDF,该模型对 **大量低U任务的管理可能更简单、更鲁棒**(RM的静态优先级在任务数多时开销可能更低,行为更易预测),并且 **天然隔离了不同SLA要求的任务**。通过这种‘分而治之’的策略,**联合模型旨在结合EDF和RM各自的优点,达到系统整体效用的帕累托优化**。

*   “**Rust异步I/O机制优化任务处理速度,减少20%-40%系统调用开销**”:请详细说明你是如何使用Rust异步I/O(比如`async/await` + `tokio`/`async-std`)来实现优化的?具体减少了哪些类型的系统调用?如何测量和验证这个20-40%的提升?



*   “**实现调度策略对比测试...完成内核恐慌日志分析与修复**”:你测试了哪些调度策略?对比的指标是什么?遇到了什么样的内核恐慌(Kernel Panic)?你是如何定位和修复的?请描述调试过程。
*   这个框架的“**适应性**”和“**扩展性**”具体体现在哪些方面?架构上是如何设计的?
*   为什么选择Rust而不是C/C++来做这个项目?Rust在系统编程中的优势(所有权、生命周期、无畏并发)在你的项目中是如何体现的?
    **“面试官,我们选择Rust而非C/C++主要基于三个核心优势,这些优势在**系统级开发**中直接解决了传统语言的痛点:**

    #### **1. 所有权机制(Ownership)与内存安全:**
    * **项目痛点:** 在开发**操作系统微内核**和**多线程TCP服务器**时,手动管理内存(如C/C++)极易引入`use-after-free`、`double-free`或内存泄漏,导致内核崩溃或服务宕机。
    * **Rust解决方案:**  
        * 通过**编译期所有权规则**(移动语义、借用检查器),强制保证:
        * 每个值只有一个所有者,避免悬垂指针。
        * 作用域结束时自动释放资源(无GC开销)。
        * **项目体现:**  
        * 在**TCP服务器的线程安全队列**中,`Arc<Mutex<T>>` 在编译期保证多线程共享数据的安全性,避免竞态条件(Race Conditions)。
        * 在**操作系统任务调度器**中,任务句柄的所有权转移确保资源不被非法访问。

    #### **2. 无畏并发(Fearless Concurrency):**
    * **项目痛点:** C/C++的并发需依赖开发者经验(如手动加锁),调试难度大,易死锁。
    * **Rust解决方案:**  
        * 所有权机制天然支持并发安全:
        * `Send` trait 允许跨线程传递所有权。
        * `Sync` trait 允许多线程安全共享引用。
        * **项目体现:**  
        * **TCP服务器**采用 `tokio` 异步运行时,基于 `async/await` 实现非阻塞I/O。编译器确保**异步任务间无数据竞争**,我们轻松实现 **300+ QPS 高并发处理** 且 **99.9% 可用性**。
        * **OS调度器**的联合调度模型中,Rust保证跨核任务调度无并发错误。

    #### **3. 零成本抽象与性能:**
    * **项目痛点:** C++的抽象可能引入运行时开销(如虚函数),C则缺乏现代抽象能力。
    * **Rust解决方案:**  
        * 所有权、trait、模式匹配等特性在**编译期优化**,生成代码效率匹敌C/C++。
        * **项目体现:**  
        * 在**OS框架中**,用Rust异步I/O减少 **20%-40% 系统调用开销**(对比同步阻塞模型)。
        * **TCP服务器**的Reactor模式通过 `mio` 库实现**零额外开销**的事件驱动。

    #### **对比C/C++的额外优势:**
    * **工具链现代化:** `Cargo` 管理依赖、构建、测试(对比Makefile/CMake)。
    * **模式匹配与错误处理:** `Result`/`Option` 强制处理错误(避免C/C++未检查错误)。
    * **无未定义行为(Undefined Behavior):** 编译期拦截空指针、越界访问(内核开发关键)。
  • 1. 多线程 TCP 服务器开发(Rust):

    • 采用Reactor模式实现非阻塞I/O”:请解释一下你实现的Reactor模式的核心组件(Reactor, Demultiplexer, Event Handlers)是如何工作的?你使用了哪个库(如mio, tokio)?
    • 设计线程安全的任务队列和数据共享”:你具体采用了什么机制来保证线程安全?(如Arc<Mutex<T>>, Arc<RwLock<T>>, crossbeam-channel, tokio::sync::mpsc等)选择它们的理由?有没有考虑无锁队列?
    • 峰值处理能力达300+请求/秒”:这个测试是在什么环境下进行的(硬件配置、并发客户端数、请求类型/大小)?瓶颈主要在哪里?CPU、内存、网络I/O?
    • 解决竞争条件”:你遇到了什么样的竞争条件?是如何发现(如死锁、数据错乱)和解决的?
    • 分层架构实现模块解耦”:具体分成了哪几层?层与层之间如何通信?这样设计的好处?
    • 如何确保“99.9% 运行时间”(高可用)?做了哪些容错或恢复机制?
  1. 基于贝叶斯优化的系统参数自动化调优框架(Python):
    • 这个框架是用来优化什么系统的参数?目标性能指标是什么?(如延迟、吞吐量、资源利用率)
    • 提高贝叶斯优化的超参数搜索效率”:你具体采用了什么技术或策略来提高效率?(如代理模型选择、采集函数优化、并行化、热启动等)
    • 数据变换和核函数优化,使 BIC 评分优化30%”:具体做了哪些数据变换?优化了哪个贝叶斯优化模型的核函数?为什么这些优化能提升BIC评分?BIC评分在这里代表什么?
    • 对比贝叶斯优化和随机搜索,在学习曲线均值差距上具体有哪些显著差异?说明了什么问题?
  2. 智能菜市场物联网数据平台后端(Spring Boot):
    • 针对高并发数据接入场景(日均5000+条设备数据)...采用分库分表策略”:日均5000+条数据并不算特别大,当时为什么决定采用分库分表?是基于对未来增长的预估吗?具体如何设计分片键(Sharding Key)?采用了什么分库分表中间件(如ShardingSphere)还是自研?
    • 通过联合索引将查询耗时从300-400ms降低至80-150ms”:请描述这个慢查询的具体场景(SQL语句片段、表结构、数据量)?你创建的联合索引是哪些字段?为什么这个索引有效?(结合最左前缀原则、覆盖索引等解释)
    • 设计模块化分层架构(Controller-Service-DAO)”:在“设备管理、数据管理、告警处理等模块”的抽象过程中,如何保证模块间的边界清晰?有没有使用到设计模式(如工厂模式、策略模式)?
    • Docker容器化部署...单节点资源占用减少30%”:相比传统部署方式,容器化是如何实现资源占用减少的?(资源隔离、更轻量级、避免环境差异等)有使用编排工具(如K8s)吗?
    • 作为实习生,你在整个项目中承担的具体职责和贡献的比例大概是多少?遇到的最大的挑战是什么?

二、 核心技术基础 (重点考察“必须具备的”和“加分项”)

  1. 编程语言:
    • C++: 你熟悉到什么程度?了解RAII、智能指针、移动语义、多态、模板元编程吗?能简单举例说明。
    • Rust: 所有权和生命周期机制的核心思想是什么?它们如何帮助解决内存安全和数据竞争问题?SendSync trait的作用?你在项目中遇到的最典型的借用检查器挑战是什么,如何解决的?
    • Java: Spring Boot的核心优势是什么?IoC和AOP是如何工作的?你了解的垃圾回收算法有哪些?(如G1, CMS, ZGC的特点)
    • Python: 主要用在什么地方?(脚本、数据分析、框架)了解GIL吗?对多线程编程有什么影响?
  2. 网络:
    • TCP和UDP的核心区别是什么?各自适用于什么场景?
    • 详细描述TCP三次握手和四次挥手的过程。为什么需要三次握手?TIME_WAIT状态的作用是什么?
    • 什么是TCP粘包/拆包?常见的解决方案有哪些?(如消息头包含长度、特定分隔符)
    • 在Rust TCP服务器项目中,如何处理大量并发连接?(Reactor/Proactor, 线程池)
    • HTTP协议是基于TCP还是UDP?HTTP/1.1, HTTP/2, HTTP/3的主要区别?
    • 了解常见的RPC框架(如gRPC, Thrift)吗?它们解决了什么问题?
  3. 操作系统:
    • 进程和线程的区别?协程(Coroutine)呢?Rust的async/await底层可以看作协程吗?
    • 进程间通信(IPC)有哪些方式?(管道、消息队列、共享内存、信号量、Socket)各有什么优缺点?你在项目中用过哪些?
    • 线程同步机制有哪些?(互斥锁、读写锁、条件变量、信号量、原子操作)解释死锁及其必要条件、预防/避免方法。
    • 虚拟内存是什么?有什么作用?(内存管理、内存保护、内存共享)
    • 系统调用(System Call)是什么?用户态和内核态切换的代价是什么?为什么Rust异步I/O能减少系统调用开销?(批处理、用户态调度)
    • 你理解的“微内核”架构和“宏内核”(如Linux)的主要区别是什么?各自的优缺点?
  4. 数据结构与算法:
    • 数组和链表的区别?各自的适用场景?
    • 哈希表(Hash Table)的原理是什么?如何解决哈希冲突?(开放寻址法、链地址法)影响哈希表性能的关键因素?
    • 常见的树结构有哪些?(二叉树、二叉搜索树、平衡二叉树如AVL/红黑树、B树/B+树)MySQL索引通常用什么结构?(B+树)为什么?
    • 描述快速排序(QuickSort)的思想和平均/最坏时间复杂度。如何优化最坏情况?
    • 写一个简单的算法题(面试官现场出题,可能涉及数组、字符串、链表、树、二分查找、简单DP等)。
  5. 数据库:
    • 数据库事务的ACID特性是什么?
    • MySQL的InnoDB存储引擎有哪些特性?(事务、行锁、MVCC)
    • 数据库索引的作用?什么情况下索引会失效?(如对索引列进行函数操作、使用!=NOT IN、未满足最左前缀原则等)
    • 解释一下MVCC(多版本并发控制)是如何工作的?
    • 数据库锁有哪些类型?(共享锁、排他锁、意向锁)什么是死锁?如何避免?
    • 分库分表是为了解决什么问题?(水平扩展、性能瓶颈)会带来哪些挑战?(分布式事务、跨库查询、全局唯一ID)
    • NoSQL数据库(如Redis, MongoDB)的特点和适用场景?Redis常用数据类型及其应用场景?
  6. 软件工程 & 设计模式:
    • 你如何理解面向对象编程(OOP)的四大特性(抽象、封装、继承、多态)?
    • 了解哪些设计模式?能在你的项目经历中找到应用吗?(如Spring Boot项目中的分层架构体现了关注点分离,工厂模式可能用于创建对象,策略模式可能用于选择不同算法/策略)
    • 什么是SOLID设计原则?能简要解释每个原则吗?
    • 如何保证代码质量?(单元测试、集成测试、代码评审、静态代码分析、CI/CD)
  7. 分布式系统 & 高可用 & 云原生 (加分项重点):
    • 解释一下CAP定理和BASE理论。
    • 什么是分布式事务?常见的解决方案有哪些?(两阶段提交2PC、三阶段提交3PC、TCC、基于消息队列的最终一致性)
    • 如何设计一个高可用(High Availability)的服务?有哪些常用手段?(冗余/集群、负载均衡、故障转移Failover、熔断、降级、限流)
    • 负载均衡有哪些常见的算法?(轮询、加权轮询、最少连接、源IP哈希)
    • 什么是服务发现?为什么在微服务架构中需要它?
    • 你提到了“系统容灾设计”,谈谈你的理解?常见的容灾方案?(同城多活、异地多活、备份与恢复)
    • 对云原生相关技术有所了解”:你理解的云原生是什么?它的关键技术包括哪些?(容器化Docker、编排Kubernetes、服务网格Istio、微服务、声明式API、不可变基础设施)你用过哪些?
    • 你在简历中提到Amazon EC2,主要用它来做什么?(部署、测试)了解其他AWS服务吗?(如S3, RDS, Lambda)

三、 系统设计与场景题 (重点考察分析和设计能力)

  1. 设计一个短链接生成系统(类似TinyURL):
    • 核心功能:将长URL转成短URL;访问短URL重定向到原始长URL。
    • 需要考虑:高并发生成与访问、短URL的生成算法(如何保证不重复、长度短)、存储设计(用什么数据库?如何设计表?)、重定向性能(缓存?)、如何防止恶意攻击(刷接口)?
  2. 设计一个简单的消息队列:
    • 核心功能:生产者发送消息、消费者订阅并消费消息、消息持久化(可选)。
    • 需要考虑:如何保证消息不丢失?如何保证消息顺序(如果需要)?消费者如何处理失败的消息(重试、死信队列)?如何实现多消费者?如何扩展?
  3. 你开发的Rust TCP服务器,如果流量突然增长10倍(3000+ req/s),可能遇到哪些瓶颈?你会如何优化? (结合你的项目经验)
  4. 智能菜市场平台的后端,如果数据库写入成为瓶颈(比如大量设备同时上报数据),除了分库分表,还有哪些优化思路? (如消息队列削峰填谷、批量写入、优化数据库配置、使用更快的存储硬件/SSD、考虑时序数据库TSDB)

四、 问题排查与性能优化 (考察实战能力)

  1. 线上服务突然响应变慢,甚至部分超时,你会如何一步步排查定位问题? (思路:监控指标查看 - CPU/内存/磁盘IO/网络带宽、日志分析 - 错误/慢查询日志、链路追踪分析、数据库状态检查、代码Review最近变更、压测复现等)
  2. 数据库查询变慢,如何排查和优化? (思路:EXPLAIN分析SQL执行计划、检查索引是否有效/缺失、优化SQL语句、调整数据库配置参数、考虑读写分离、升级硬件)
  3. 在Rust项目中解决内核恐慌(Kernel Panic)的经历,体现了你的问题排查能力。能再举一个你解决过的复杂线上或开发中问题的例子吗?描述现象、分析过程和最终解决方案。

五、 软技能与职业发展

  1. 你如何理解这个岗位的职责?(结合岗位描述复述并加入自己的理解)
  2. 为什么选择我们公司/这个部门(云架构平台部)?你对云计算/云原生平台哪方面最感兴趣?
  3. 你平时是如何学习新技术的?最近在学习或关注什么技术?
  4. 在团队协作中,如果和产品经理或前端工程师在需求或实现方式上有分歧,你会如何处理?
  5. 你的职业发展规划是什么?(短期1-2年,长期3-5年)
  6. 你有什么问题想问我们?(务必准备2-3个有深度的问题,体现你对岗位和公司的兴趣和思考,例如:团队当前的技术栈和主要挑战?部门对云原生技术的落地规划?新人的培养机制?)

面试准备建议:

  1. STAR法则强化项目描述: 针对简历上的每一个项目点(尤其是你贡献最大的部分),用STAR法则(Situation, Task, Action, Result)准备清晰、具体的回答,重点突出你的思考、行动和可量化的成果
  2. 基础知识复盘: 系统复习操作系统、网络、数据库、数据结构算法、设计模式的核心概念。不仅要懂是什么,还要理解为什么。
  3. Rust深度准备: 作为你简历的显著亮点,务必深入理解Rust的核心特性(所有权、生命周期、借用检查、并发模型async/await)、常用库(tokio, serde等)以及你在项目中应用它们的细节和遇到的挑战。
  4. 分布式/云原生概念梳理: 理解CAP、负载均衡、服务发现、熔断降级限流、容器化、K8s核心概念等。即使没有深入项目经验,也要能清晰表达概念和基本原理。
  5. 设计题练习: 找一些经典的系统设计面试题进行思路练习,注意沟通、需求澄清、逐步推导的过程。
  6. 量化成果再确认: 回顾项目中的性能提升、资源节省等数据,确保清晰记得是如何得出的,并能解释其意义。
  7. 模拟面试: 找同学或朋友进行模拟面试,重点练习表达流畅度、项目描述的清晰度和应对压力问题的能力。
  8. 公司/部门研究: 深入了解目标公司云架构平台部的业务、技术博客(如果有)、使用的技术栈(如果能了解到),在回答“为什么选择我们”和提问环节体现出来。
  9. 准备问题: 准备好要问面试官的问题,展示你的主动性和兴趣。

特别注意:

  • 诚实第一: 对于不了解的知识点,坦诚承认并表示愿意学习,切忌不懂装懂。
  • 突出亮点: 反复强调Rust系统级开发、分布式架构设计(分库分表、高并发优化)、云原生实践(Docker)这几个核心竞争力。
  • 沟通清晰: 表达要有条理,技术术语使用准确,语速适中。
  • 展现热情: 表现出对后台开发、云技术、解决复杂技术问题的热情。

祝你面试顺利,拿到心仪的Offer!加油!

491.递增子序列

题解说这里用数组做哈希,效率会更高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Solution {
public:
vector<vector<int>> result;
void findSS(vector<int>& nums, vector<int> path, int left){
unordered_set<int> used; // 当前层去重
for(int i = left; i<nums.size();i++){
if(used.find(nums[i]) != used.end())continue;
if(path.empty()){
path.push_back(nums[i]);
used.insert(nums[i]);
findSS(nums, path, i+1);
path.pop_back();
}else if(!path.empty() && nums[i]>=path.back()){
// if(i>left && nums[i-1] == nums[i])continue; // 重复元素不一定相邻
path.push_back(nums[i]);
result.push_back(path);
used.insert(nums[i]);
findSS(nums, path, i+1);
path.pop_back();
}
}
return;
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
vector<int> path;
findSS(nums, path, 0);
return result;
}
};

46.全排列

  1. 索引限制错误
    • 使用 left 参数限制选择范围,导致只能生成索引递增的子序列而非全排列
    • 例如输入 [1,2,3] 时,代码只能生成 [1,2,3],无法生成 [2,1,3] 等排列
  2. 缺少元素使用状态跟踪
    • 排列需要从所有未使用元素中选择,而不仅仅是当前索引后的元素
    • 没有记录哪些元素已被使用,导致:
      • 无法重新选择左侧元素
      • 无法避免重复使用同一元素
  3. 递归逻辑错误
    • 每次递归都从 i+1 开始,使得路径只能向右扩展
    • 排列需要任意顺序组合,需能回头选择左侧元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
vector<vector<int>> result;

void perm(vector<int>& nums, vector<int>& path, vector<bool>& used) {
if (path.size() == nums.size()) {
result.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i++) { // 必须遍历所有元素
if (used[i]) continue; // 跳过已用元素
used[i] = true;
path.push_back(nums[i]);
perm(nums, path, used); // 注意:这里递归时不限制索引
path.pop_back();
used[i] = false; // 回溯
}
}

vector<vector<int>> permute(vector<int>& nums) {
vector<int> path;
vector<bool> used(nums.size(), false); // 添加使用状态数组
perm(nums, path, used);
return result;
}
};

47.全排列 II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Solution {
public:
vector<vector<int>> result;
void perm(vector<int>& nums, vector<int> path, vector<bool>& used){
if(path.size() == nums.size()){
result.push_back(path);
// return;
}
for(int i = 0; i< nums.size(); i++){
if(used[i] == true)continue;
// used[i - 1] == true,说明同一树枝nums[i - 1]使用过
// used[i - 1] == false,说明同一树层nums[i - 1]使用过
// 👇同一层使用过则跳过
if(i>0 && nums[i] == nums[i-1] && used[i-1] == false)continue;
path.push_back(nums[i]);
used[i] = true;
perm(nums, path, used);
path.pop_back();
used[i] = false;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<int> path;
vector<bool> used(nums.size(),false);
sort(nums.begin(), nums.end());
perm(nums, path, used);
return result;
}
};

93.复原IP地址

手动写了to_string()stoi(),发现字符串给整反了,改过来了也发现效率不如用现有函数 这题需要注意的限制条件确实很多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
vector<string> result;
string pathToIp(vector<int> path){
string ip;
for(int i = 0;i<path.size();i++){
ip += to_string(path[i]);
if(i != (path.size()-1))ip.push_back('.');
}
return ip;
}
void resIp(string s, vector<int> path, int div){
if(path.size() == 4){
if(div == s.size())result.push_back(pathToIp(path));
return;
}
for(int i = div; i<s.size();i++){
string cur = s.substr(div, i-div+1);
if(cur.size() > 3)continue;
int curN = stoi(cur);
if(cur.size()>1 && cur[0] == '0')continue;
if(curN > 255)continue;
path.push_back(curN);
resIp(s, path, i+1);
path.pop_back();
}
}
vector<string> restoreIpAddresses(string s) {
if (s.size() > 12) return result;
vector<int> path;
resIp(s, path, 0);
return result;
}
};

78. 子集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
vector<vector<int>> result;
void ssets(vector<int>& nums, vector<int> path, int left){
for(int i = left; i < nums.size();i++){
// if(i>left && nums[i] == nums[i-1])continue;
path.push_back(nums[i]);
result.push_back(path);
ssets(nums, path, i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
vector<int> path;
result.push_back(path); // 带上空集
ssets(nums, path, 0);
return result;
}
};

90.子集II

和上一篇中的40题一样,题解的used数组和i>left去重都可以,不过这回似乎i>left去重runtime也不低

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
vector<vector<int>> result;
void ssets(vector<int>& nums, vector<int> path, int left){
for(int i = left; i < nums.size();i++){
if(i>left && nums[i] == nums[i-1])continue;
path.push_back(nums[i]);
result.push_back(path);
ssets(nums, path, i+1);
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
vector<int> path;
result.push_back(path); // 带上空集
sort(nums.begin(), nums.end());
ssets(nums, path, 0);
return result;
}
};

39. 组合总和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<algorithm>
using namespace std;
class Solution {
public:
vector<vector<int>> result;
void combSum(vector<int> path, int left, vector<int>& candi, int t){
if(t == 0){
result.push_back(path);
return;
}else if(candi[0] > t){
return;
}
for(int i = left;i< candi.size();i++){
if(candi[i] <= t){
path.push_back(candi[i]);
combSum(path, i, candi, t-candi[i]);
path.pop_back();
}else{
break;
}
}
return;
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) { // candidate不是递增的
sort(candidates.begin(), candidates.end());
vector<int> path;
combSum(path, 0, candidates, target);
return result;
}
};

Introsort(IntroSort)

C++ 中的 std::sort()(定义在 <algorithm> 中)默认使用的是一种由 三种排序算法组合而成的混合排序算法

🔧 Introsort 的组成:

  1. 快速排序(Quicksort):在数据量大、分布均匀时使用,速度快,期望时间复杂度是 O(n log n)。

  2. 堆排序(Heapsort):当递归深度超过一定阈值(防止最坏情况 O(n²))时退化为堆排序,保证最坏时间复杂度也是 O(n log n)。

  3. 插入排序(Insertion Sort):在小数据范围(如 <16)时使用,性能优于快速排序。

40.组合总和II

i > left && candi[i] == candi[i-1]和题解中创建candidate长度的i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false都可以达到剪去重复,代码AC的效果 如果使用i > left && candidates[i] == candidates[i - 1] && used[i - 1] == false可以提高代码效率

注意题解中used数组只用来剪去同一层的重复数字,所以递归调用后used[i]要设置回false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
vector<vector<int>> result;
void comb(vector<int>& candi, int left, vector<int> path, int t){
if(t == 0){
result.push_back(path);
return;
}else if(t < 0){
return;
}
for(int i = left; i< candi.size();i++){
if(candi[i] > t)return;
if(i > left && candi[i] == candi[i-1])continue;
path.push_back(candi[i]);
comb(candi, i+1, path, t-candi[i]);
path.pop_back();
}
return;
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end());
vector<int> path;
comb(candidates, 0, path, target);
return result;
}
};

131.分割回文串

如题解所说,难点在于认识到切割问题和组合问题类似——每个位置是否“切”——每种切法就是一条树枝,还有求是否是回文串部分的优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Solution {
public:
vector<vector<bool>> pal;
void isPal(string s){
pal.resize(s.size(), vector<bool>(s.size(), false));
for(int i = (s.size()-1); i>=0;i--){
for(int j = i; j<= (s.size()-1);j++){
if(j == i){
pal[i][j] = true;
}else if((j - i) == 1){
pal[i][j] = s[i] == s[j] ? true : false;
}else{
pal[i][j] = (s[i] == s[j] && pal[i+1][j-1]);
}
}
}
}
vector<vector<string>> result;
void partPal(vector<string> path, string s, int div){
if(div == s.size()){
result.push_back(path);
return;
}
for(int i = div; i < s.size();i++){
if(pal[div][i] == true){
path.push_back(s.substr(div, i-div+1));
partPal(path, s, i+1);
path.pop_back();
}else{
continue;
}
}
return;
}
vector<vector<string>> partition(string s) {
vector<string> path;
isPal(s);
partPal(path, s, 0);
return result;
}
};

77. 组合

虽然runtime离谱地高,但是俺居然自己寻思出了怎么回溯,可喜可贺:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Solution {
public:
vector<vector<int>> res;
void comb(vector<int> curNums, int l, int r,int k){
if(curNums.size() == k){
res.push_back(curNums);
return;
}
// for(int i = l; i<= r;i++){// 能想明白l和r的关系我已经很不容易了
for(int i = l; i<= r-(k-curNums.size())+1;i++){
// 👆 看了题解发现我想不明白的地方(i最大取多少)应该每次循环重新算而不是代入递归,用于剪枝
curNums.push_back(i);
comb(curNums, i+1, r, k);
curNums.pop_back();// 最重要的一句
}
return;
}
vector<vector<int>> combine(int n, int k) {
vector<int> cur;
comb(cur, 1, n, k);
return res;
}
};
这种写法的时间复杂度不是 O(n * 2^n)

🚩实际代码做的事情:

这段代码实现的是 1n 中选择 k 个数的所有组合,即 组合数 C(n, k)

所以:

✅ 正确时间复杂度是:

\(O(C(n, k) \cdot k)\)

解释如下:

  • 一共会生成 \(C(n, k)\) 个组合,每个组合长度是 k

  • 每次 curNums 达到长度 k 就复制一次 curNums 到结果 res

  • 每次复制 k 个元素,因此时间复杂度为 \(O(k)\)

  • 总时间复杂度为: \(O(C(n, k) \cdot k)\)

❓那为什么会看到 O(n * 2^n) 的说法?

这个复杂度常出现在子集枚举问题(power set)中,比如“所有子集”或“子集和”等问题,使用类似回溯结构但允许选择/不选择每个元素,即二叉树结构:

1
2
3
for (int i = l; i <= r; i++) { 
// include or skip i —> 2^n branches
}

选出来的每个子集的长度从1到n的,而本题组合问题选出来的组合长度固定为K

216.组合总和III

和上题基本思想差不多,多了一些限制条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
vector<vector<int>> result;
void comb(vector<int> nums, int k, int l, int n){
if(k == 0){
if(n == 0){
result.push_back(nums);
return;
}else{
return;
}
}
int r = min(9, n);
for(int i = l; i<=r;i++){
nums.push_back(i);
comb(nums, k-1,i+1, n-i);
nums.pop_back();
}
return;
}
vector<vector<int>> combinationSum3(int k, int n) {
vector<int> nums;
comb(nums,k,1,n);
return result;
}
};

17.电话号码的字母组合

注意终止条件是索引数加一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
public:
const string digToStr[10] = {
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
vector<string> result;
void comb(string str, string digits, int ind){
if(ind == digits.size()){
result.push_back(str);
return;
}
int d = digits[ind] - '0';
ind++;
string s = digToStr[d];
for(char c : s){
str.push_back(c);
comb(str, digits, ind);
str.pop_back();
}
return;
}
vector<string> letterCombinations(string digits) {
string str;
if(digits.empty())return result;
comb(str, digits, 0);
return result;
}
};

0%