Maxw的小站

Maxw学习记录

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;
}
};

669.修剪二叉搜索树

重点是理解当某个节点要删除时,返回的节点的逻辑 我的思路是觉得剪除根两边的孩子节点和只取一个子树中的部分节点应该分开讨论,看了题解发现部分代码是可以一起的 题解的思路也可以理解为先处理root->val自身,后处理子树

如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。 如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。 将下一层处理完左子树的结果赋给root->left,处理完右子树的结果赋给root->right。 最后返回root节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if(!root)return nullptr;
if(root->val < low){
TreeNode* right = trimBST(root->right, low, high);
return right;
}else if(root->val > high){
TreeNode* left = trimBST(root->left, low, high);
return left;
}
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
};

108.将有序数组转换为二叉搜索树

🔍 问题一:cen 取值错误

1
int cen = (r - l) / 2;

这会导致始终只取左半段的中点没有加上 l,导致选到的 nums[cen] 不是 lr 区间的中点,而是从头算的。

🔧 应该写为:

1
int cen = l + (r - l) / 2;

🔍 问题二:递归边界判断不正确

1
if((cen - l) > 1) ...

这样写会漏掉一层子节点,导致不完整构建树。例如当子区间只剩一个元素时(cen - l == 1),你就跳过了构建那一侧的节点。

🔧 正确的写法是直接递归调用,只要 l <= r

1
2
root->left = arrayToBST(nums, l, cen - 1);
root->right = arrayToBST(nums, cen + 1, r);
已经在函数开头判断了 l > rreturn nullptr;,所以不需要重复判断。

最终解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
TreeNode* arrayToBST(vector<int>& nums, int l, int r){
if(l > r)return nullptr;
int cen = l + (r-l) / 2;
TreeNode* root = new TreeNode(nums[cen]);
root->left = arrayToBST(nums, l, cen-1);
root->right = arrayToBST(nums, cen+1, r);
return root;
}
TreeNode* sortedArrayToBST(vector<int>& nums) {
TreeNode* root = arrayToBST(nums, 0, nums.size()-1);
return root;
}
};

538.把二叉搜索树转换为累加树

题解提示用双指针,给我吓一跳,用回溯其实是可以解决的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Solution {
public:
int sumVal = 0;
void cvt(TreeNode* root){
if(!root)return;
cvt(root->right);
sumVal += root->val;
root->val = sumVal;
cvt(root->left);

}
TreeNode* convertBST(TreeNode* root) {
cvt(root);
return root;
}
};

235. 二叉搜索树的最近公共祖先

相对二叉树的最近公共祖先简单多了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <algorithm> // min 和 max
using namespace std;
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(!root)return nullptr;
int rt = root->val;
int minV = min(p->val, q->val);
int maxV = max(p->val, q->val);
if(rt>=minV && rt<=maxV){
return root;
}else if(rt < minV){
return lowestCommonAncestor(root->right, p, q);
}else{
return lowestCommonAncestor(root->left, p,q);
}
}
};

701.二叉搜索树中的插入操作

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
TreeNode* insNode = new TreeNode(val);
if(!root)return insNode;
if(root->val > val){
root->left = insertIntoBST(root->left, val);
}else{
root->right = insertIntoBST(root->right, val);
}
return root;
}
};

450.删除二叉搜索树中的节点

我的想法是:先找到应该删除的节点子树-找到它的左子树最右节点-找到最右节点的父节点-将最右节点的值替换给应该删除的节点并删除最右节点 题解虽然也类似地需要分情况讨论,但是简洁地多:

  • 第一种情况:没找到删除的节点,遍历到空节点直接返回了
  • 找到删除的节点
    • 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
    • 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
    • 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
    • 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if(!root)return nullptr;
if(root->val == key){
if(!root->left)return root->right;
if(!root->right)return root->left;
TreeNode* minR = root->right;
while(minR->left)minR = minR->left;
minR->left = root->left;
return root->right;
}
if(root->val > key){
root->left = deleteNode(root->left, key);
}else{
root->right = deleteNode(root->right, key);
}
return root;
}
};

HTTP中常见的状态码有哪些?

HTTP 状态码用于表明特定HTTP请求是否完成

1xx 信息响应(100–199)

✅“我听到了,继续吧”

表示请求已接收,继续处理

  • 100 Continue:初始部分已接收,客户端应继续发送。
  • 101 Switching Protocols:服务器:同意更改协议。 > - WebSocket:最常见,HTTP 升级为 WebSocket,实现持久化的全双工通信。 > - HTTP/2:部分场景下可以通过 Upgrade 机制从 HTTP/1.1 升级到 HTTP/2。 > - TLS/SSL:早期可以通过 Upgrade 升级到加密连接(现在一般用 HTTPS 直接建立)。
  • 102 Processing:服务器:已收到,正在处理。

2xx 成功响应(200–299)

表示请求已成功接收

🎉“你请求的事我办妥了”

  • 200 OK:请求成功。
  • 201 Created:请求成功并创建了新的资源(例如注册)
  • 202 Accepted:请求已接受,但未响应 -不会有一个异步的响应去表明当前请求的结果,预期另外的进程和服务去处理请求
  • 204 No Content:请求成功,但无返回内容。
  • 206 Partial Content:当从客户端发送Range范围标头以只请求资源的一部分时,使用此响应代码(例如断点续传)

3xx 重定向(300–399)

表示需要进一步操作以完成请求。

🔀“去别处找”

  • 301 Moved Permanently:请求的资源已永久移动到新位置。
  • 302 Found:请求的资源临时从不同的 URI 响应请求。
  • 303 See Other:请求应使用另一个 URI 获取资源。
  • 304 Not Modified:资源未修改,可使用缓存。
  • 307 Temporary Redirect:请求的资源临时从不同的 URI 响应请求,方法不变。
  • 308 Permanent Redirect:请求的资源已永久移动到新位置,方法不变。

4xx 客户端错误(400–499)

表示请求包含语法错误或无法完成

  • 400 Bad Request:请求无效,服务器无法理解。
  • 401 Unauthorized:请求要求客户端身份认证。
  • 403 Forbidden:服务器知道客户端身份,但客户端没有访问权限
  • 404 Not Found:请求的资源未找到。
  • 405 Method Not Allowed:请求方法被禁止。
  • 408 Request Timeout:请求超时。
  • 429 Too Many Requests:客户端发送的请求过多,已被限制。

5xx 服务器错误(500–599)

表示服务器未能完成合法的请求。

  • 500 Internal Server Error:服务器内部错误,无法完成请求。
  • 501 Not Implemented:服务器不支持请求的功能。
  • 502 Bad Gateway:服务器作为网关或代理,从上游服务器收到错误响应。
  • 503 Service Unavailable:服务器当前无法处理请求。
  • 504 Gateway Timeout:服务器作为网关或代理,未及时从上游服务器收到响应。

什么是强缓存和协商缓存

🌟一、强缓存(完全不发请求)

关键词:直接使用缓存,不访问服务器

✅ 特点:
  • 浏览器直接使用本地缓存资源
  • 不向服务器发请求。
  • 若命中强缓存,浏览器将会本地模拟一个状态码 200 (from memory cache)200 (from disk cache)
📌 实现方式:
字段 说明
Cache-Control: max-age=秒数 (相对时间,更准确)当前资源在 N 秒内有效✅推荐
Expires: GMT时间 (绝对时间,受本地时间影响)指定资源过期时间❌已过时
  1. 首次访问资源
    服务器返回资源时在响应头加上 Cache-Control: max-age=秒数,告知浏览器这个资源可以缓存多久。

  2. 再次访问时
    浏览器会用“当前时间 - 缓存时间”与 max-age 做比较:

    • 没过期 → 直接用缓存,不发请求
    • 过期 → 发起新请求,向服务器重新获取资源
  3. 服务器响应更新
    每次服务器响应时都会更新 Cache-Control,供下一轮缓存使用。

🔄二、协商缓存(发请求,服务器决定是否使用缓存)

关键词:发请求,比较“资源是否改过”

✅ 特点:
  • 请求时会向服务器询问资源是否有更新。
  • 如果没变,返回 304 Not Modified,继续用本地缓存。
  • 如果变了,返回 200 + 新资源
📌 两种主流方式:
方式 请求头字段 响应头字段 原理 优缺点简析
Last-Modified If-Modified-Since Last-Modified 比较“上次修改时间” ⛔秒级精度、不改内容也可能变时间
ETag If-None-Match ETag 比较“文件指纹/哈希” ✅更精准,内容变才更新

🔁三、常见误区与补充

  • 强缓存命中 → 不发请求
  • 强缓存失效 → 发请求,进入协商缓存阶段
  • 如果协商缓存也失效,才真正下载新资源。

530.搜索树的最小绝对差

遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了 尝试自己写遍历,虽然有很多莫名其妙的值,但是居然写对了

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:
TreeNode* maxNode = nullptr;
int absd = -1;
int getMinimumDifference(TreeNode* root) {
if(!root)return -1;
int left = -1;
int right = -1;
if(root->left)left = getMinimumDifference(root->left);
if(maxNode != nullptr){
int maxd = root->val - maxNode->val;
if(absd == -1){
absd = maxd;
}else if(maxd < absd){
absd = maxd;
}
}
maxNode = root;
if(root->right)right = getMinimumDifference(root->right);
return absd;
}
};

501.二叉搜索树中的众数

如果我自己写,就遍历数组取最大值好了。看题解发现可以中序遍历,按这种方法找: 弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。 而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。 如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),
频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。

注意计数和判定最大值要分开,并且确定好是判定当前节点

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
class Solution {
public:
TreeNode* pre = nullptr;
int cnt = 0;
int cntMax = 0;
void traverse(vector<int> &res, TreeNode* root){
if(!root)return;
traverse(res, root->left);
if(!pre){
cnt++;
}else if(pre->val == root->val){
cnt++;
}else{ // if(pre->val != root->val)
cnt = 1;
}
if(cnt == cntMax){
res.push_back(root->val);
}else if(cnt > cntMax){
res.clear();
cntMax = cnt;
res.push_back(root->val);
}
pre = root;
traverse(res, root->right);
return;
}
vector<int> findMode(TreeNode* root) {
vector<int> res;
traverse(res, root);
return res;
}
};

236. 二叉树的最近公共祖先

我的解法就是暴力解,找到p和q的路径并比较路径中不同的节点,前一个就是公共最小祖先了 看了题解发现还可以回溯:

遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。 那么二叉树如何可以自底向上查找呢? 回溯啊,二叉树回溯的过程就是从底到上。 后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。

搞清楚什么时候返回root也很重要

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(!root || root == p || root == q)return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if(left != nullptr && right != nullptr)return root;
if(left == nullptr)return right;
return left;
}
};

654.最大二叉树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
TreeNode* maxTree(vector<int>& nums, int l, int r){
int maxNum = 0;
int maxI = l;
for(int i = l;i <= r;i++){
if(nums[i] > maxNum){
maxNum = nums[i];
maxI = i;
}
}
TreeNode* root = new TreeNode(maxNum);
if(maxI > l)root->left = maxTree(nums, l, maxI-1);
if(maxI < r)root->right = maxTree(nums, maxI +1, r);
return root;
}
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
TreeNode* res = maxTree(nums, 0, nums.size()-1);
return res;
}
};

617.合并二叉树

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
TreeNode* mergeTrees(TreeNode* root1, TreeNode* root2) {
if(!root1 && !root2)return nullptr;
if(!root1)return root2;
if(!root2)return root1;
TreeNode* node = new TreeNode(root1->val + root2->val);
node->left = mergeTrees(root1->left, root2->left);
node->right = mergeTrees(root1->right, root2->right);
return node;
}
};

700.二叉搜索树中的搜索

不可以在root->val == val之前判定左右子树是否存在,会在搜索到叶子节点时提前结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
if(!root)return NULL;
// cout<<"cur val"<<root->val<<endl;
// if(!root->left && !root->right)return NULL; // --- 不可以在这里判定
if(root->val == val){
return root;
}else if(root->val > val){
return searchBST(root->left, val);
}else{
return searchBST(root->right, val);
}
}
};

98.验证二叉搜索树

还是掉坑里了,其实验证二叉树中序遍历是否递增就好。当然也要注意最小值如何取

1
2
3
4
5
6
7
8
9
10
11
12
class Solution {
public:
TreeNode* pre = NULL;
bool isValidBST(TreeNode* root) {
if(!root)return true;
bool left = isValidBST(root->left);
if(pre != NULL && pre->val >= root->val)return false;
pre = root;
bool right = isValidBST(root->right);
return left && right;
}
};

513. 找树左下角的值

迭代法

此题用迭代法做比较简单,掌握层序遍历即可

⚠️ 注意不可直接写:for (int i = 0; i < layer.size(); i++)

layer.size() 在循环过程中是动态变化的,因为在循环中同时 pop()push(),这会让 size() 的结果发生变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue<TreeNode*> layer;
layer.push(root);
int val;
while(!layer.empty()){
int sz = layer.size();
for(int i = 0 ; i< sz;i++){
TreeNode* cur = layer.front();
layer.pop();
if(cur->left)layer.push(cur->left);
if(cur->right)layer.push(cur->right);
if(i == 0)val = cur->val;
}
}
return val;
}
};

递归法

全局找值用全局变量即可,先从左子树开始递归,返回的就是左边节点

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:
int maxDepth = 0;
int leftVal = INT_MIN;
void findVal(TreeNode* root, int depth){
if(root)depth++;
if(depth > maxDepth){
leftVal = root->val;
maxDepth = depth;
}
if(root->left){
findVal(root->left, depth);
}
if(root->right){
findVal(root->right, depth);
}
}
int findBottomLeftValue(TreeNode* root) {
findVal(root, 0);
return leftVal;
}
};

112. 路径总和

1
2
3
4
5
6
7
8
9
10
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if(!root)return false;
if((!root->left && !root->right) && (root->val == targetSum))return true;
bool hasLeft = hasPathSum(root->left, targetSum - root->val);
bool hasRight = hasPathSum(root->right, targetSum - root->val);
return hasLeft || hasRight;
}
};

113. 路径总和ii

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
vector<vector<int>> result;
void pathSum(TreeNode* root, int target, vector<int> path){
path.push_back(root->val);
if((!root->left && !root->right) && root->val == target){
result.push_back(path);
}
if(root->left)pathSum(root->left, target - root->val, path);
if(root->right)pathSum(root->right, target - root->val, path);
}
vector<vector<int>> pathSum(TreeNode* root, int targetSum) {
if(!root)return result;
vector<int> path;
pathSum(root, targetSum, path);
return result;
}
};

106.从中序与后序遍历序列构造二叉树

有几种方法可以取出vector的一部分:

用迭代器构造 vector(构造函数)

1
std::vector<int> v2(v1.begin(), v1.begin() + 3);
  • 创建一个新的 vector
  • 是构造函数,用于初始化
  • 执行的是复制构造过程

assign 方法

1
v2.assign(v1.begin(), v1.begin() + 3);
  • 修改已有的 vector 内容
  • 是成员函数,不是构造
  • 会清除原内容,然后从指定范围赋值

std::span

1
2
3
std::vector<int> original = {10, 20, 30, 40, 50, 60};
// 创建一个 span,引用从 index 2 开始的 3 个元素(即 30, 40, 50)
std::span<int> subspan(original.data() + 2, 3);
  • std::span 仅在 C++20 及以上版本中可用
  • 本身不支持负索引,但可以通过手动计算偏移量

不重复定义vector,每次用下标索引来分割性能会更好

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
class Solution {
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if(inorder.empty())return nullptr;
if(inorder.size() == 1){
TreeNode* rt = new TreeNode(inorder[0]);
return rt;
}
int rootVal = postorder[postorder.size()-1];
TreeNode* root = new TreeNode(rootVal);
for(int i=0;i<inorder.size();i++){
if(inorder[i] == rootVal){
if(i>0){
vector<int> leftIn(inorder.begin(), inorder.begin()+i);
vector<int> leftPost(postorder.begin(), postorder.begin()+i);
root->left = buildTree(leftIn, leftPost);
}
if(i<(inorder.size()-1)){
vector<int> rightIn(inorder.begin()+i+1, inorder.end());
vector<int> rightPost(postorder.begin()+i, postorder.end()-1);
root->right = buildTree(rightIn, rightPost);
}
break;
}
}
return root;
}
};

105.从前序与中序遍历序列构造二叉树

尝试了一下,发现span的性能也就那样,并不理想。以后可以试试构造map和传区间下标的方式

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<span>
using namespace std;
class Solution {
public:
TreeNode* buildT(span<int> preorder, span<int> inorder){
TreeNode* root = new TreeNode(preorder[0]);
for(int i = 0; i<inorder.size();i++){
if(preorder[0] == inorder[i]){
if(i > 0){
span<int> left_p(preorder.data()+1,preorder.data()+i+1);
span<int> left_i(inorder.data(), inorder.data()+i);
root->left = buildT(left_p, left_i);
}
int right_l = inorder.size() - 1 - i;
if(right_l>0){
span<int> right_p(preorder.data()+i+1, preorder.data()+preorder.size());
span<int> right_i(inorder.data()+i+1, inorder.data()+inorder.size());
root->right = buildT(right_p, right_i);
}
}
}
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
span<int> pre(preorder.data(), preorder.data()+preorder.size());
span<int> in(inorder.data(), inorder.data()+inorder.size());
TreeNode* root = buildT(pre, in);
return root;
}
};

110.平衡二叉树

看到这题我的思路就是求高度算差值,看了题解才发现可以在求深度的同时就判定是否不平衡并剪枝 最后没想到的是居然还能犯打错字符的错,看来以后变量命名还是少偷懒

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 depth(TreeNode* root){
int d = 0;
if(!root)return 0;
d++;
int l = depth(root->left);
if(l == -1)return -1;
int r = depth(root->right);
if(r == -1)return -1;
if(abs(l-r)>1){
return -1;
}else{
return d + max(l, r);
}
}
bool isBalanced(TreeNode* root) {
if(depth(root) == -1)return false;
return true;
}
};

257. 二叉树的所有路径

完全不知道该怎么写回溯啊...看了题解提示,有一次带push的递归,就有一次回溯,似乎有点理解了 询问chatgpt,得知如果我一定要用迭代法,应该也是类似递归的逻辑:

  1. 每次将当前节点和路径压入栈。
  2. 如果当前节点是叶子节点,则把路径加入结果。
  3. 否则,将右子树和左子树(如果存在)分别入栈,并将路径扩展。

如果删改执行逻辑,就算多弹出一次节点,到了外层while的下一个循环又会从左节点开始重复遍历

1
s.push_back(path[i]); // ❌

string 中直接用 push_back(int),会被当作 ASCII 字符写进去,而不是数字!
应该改成:

1
s += to_string(path[i]);

这里我path传值不传引用,隐式使用了迭代法函数栈的特性来回溯path:

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
#include<stack>
using namespace std;
class Solution {
public:
string vecString(vector<int>& path){
string s;
for(int i = 0;i<path.size();i++){
s += to_string(path[i]);
if(i != (path.size()-1))s.append("->");
}
return s;
}
void traverse(TreeNode* root, vector<int>path, vector<string>&result){
path.push_back(root->val);
if(!root->left && !root->right){
result.push_back(vecString(path));
}
if(root->left){
traverse(root->left, path, result);
}
if(root->right){
traverse(root->right, path, result);
}
}
vector<string> binaryTreePaths(TreeNode* root) {
vector<int> path;
vector<string> result;
traverse(root, path, result);
return result;
}
};

404.左叶子之和

我直接防御型编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
int sum = 0;
if(!root){
return 0;
}else if(!root->left){
sum += 0;
}else if(!root->left->left && !root->left->right){
sum += root->left->val;
}else{
sum += sumOfLeftLeaves(root->left); // if(root->left)
}
if(root->right)sum += sumOfLeftLeaves(root->right);
return sum;
}
};

222.完全二叉树的节点个数

\(O(n)\)写法:

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
int countNodes(TreeNode* root) {
int cnt = 0;
if(!root)return cnt;
cnt++;
if(root->left)cnt += countNodes(root->left);
if(root->right)cnt += countNodes(root->right);
return cnt;
}
};
使用完全二叉树的性质: 完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。 完全二叉树1 * 对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。 * 对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算

在完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,那说明就是满二叉树 完全二叉树2 可以达到\(O(log n × log n)\)时间复杂度

✅ 推荐修正版本如下:

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 countNodes(TreeNode* root) {
if (!root) return 0;
TreeNode* left = root;
TreeNode* right = root;
int leftDepth = 0, rightDepth = 0;
while (left) {
leftDepth++;
left = left->left;
}
while (right) {
rightDepth++;
right = right->right;
}
if (leftDepth == rightDepth) {
// 是满二叉树
return (1 << leftDepth) - 1;
}
// 不是满二叉树,递归统计
return 1 + countNodes(root->left) + countNodes(root->right);
}
};

今天找到了一个将chatGPT内容markdown化的插件(chrome插件商店搜索chatGPT to Markdown),这样整理每天的错题就方便直观多了

堆对象 vs 栈对象 初始化

C++ 中,structclass 在语法上几乎没有区别,主要区别只有两点:

  1. 默认访问权限:struct 默认是 publicclass 默认是 private

  2. 默认继承权限:struct 默认是 public 继承,class 默认是 private 继承

👉 所以:struct 并不“必须”用 new 来创建对象!

写法 内存位置 生命周期 示例
Class c; 栈 (stack) 自动管理 classstruct 都可以
Class* c = new Class(); 堆 (heap) 需要手动 delete classstruct 都可以

在需要动态分配的场景使用new(如链表节点)

1
2
3
4
5
6
struct Node {
int val;
Node* next;
};

Node* head = new Node(); // ✅ 堆上分配,适合动态数据结构

102. 二叉树的层序遍历

昨天的层序遍历练一练

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>> levelOrder(TreeNode* root) {
queue<TreeNode*> lev;
vector<vector<int>> res;
if(!root)return res;
lev.push(root);
while(!lev.empty()){
int len = lev.size();
vector<int> levVar;
while(len > 0){
len--;
TreeNode* cur = lev.front();
lev.pop();
levVar.push_back(cur->val);
if(cur->left)lev.push(cur->left);
if(cur->right)lev.push(cur->right);
}
res.push_back(levVar);
}
return res;
}
};

226.翻转二叉树

❌ 问题一:错误创建了无用的 TreeNode

1
2
TreeNode* rightT = new TreeNode();
if(root->right) rightT = root->right;

在这里无论 root->right 是否为空,都会创建一个新的 TreeNode 实例。但实际上你只需要记录 root->right 的原始指针。没有 delete 掉那个多余的 new TreeNode(),会造成内存泄漏。在递归中创建新的 TreeNode,可能无限递归,造成 stack overflow

要删除通过 new TreeNode() 动态创建的对象,只需要调用 delete 并传入该指针即可。例如:

1
2
3
4
TreeNode* node = new TreeNode();
// 使用 node 做一些操作
delete node; // 释放内存
node = nullptr; // 避免悬空指针(可选但推荐)

✅ 注意事项:

  1. 每一个 new 都要对应一个 delete,否则会导致内存泄漏。

  2. 删除后最好将指针设为 nullptr,防止以后误访问已释放的内存。

  3. 如果你创建的是数组,用 new[],则要用 delete[]

例如:

1
2
int* arr = new int[10];
delete[] arr;

❌ 问题二:判断逻辑混乱,递归不完整

1
2
3
4
5
6
if(root->left){
root->right = invertTree(root->left);
}
if(rightT != nullptr){
root->left = invertTree(rightT);
}

如果 root->leftnullptr,就跳过了对 root->right 的赋值

正确的做法是把翻转和赋值分隔开来:

1
2
3
4
5
6
7
8
9
10
11
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(!root)return root;
TreeNode* rightT = invertTree(root->right);
TreeNode* leftT = invertTree(root->left);
root->left = rightT;
root->right = leftT;
return root;
}
};

101. 对称二叉树

别忘了判定值是否一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
bool isSym(TreeNode* left, TreeNode* right){
if(!left && !right)return true;
if((!left && right) || (!right && left))return false;
if(left->val != right->val)return false;
bool isIn = isSym(left->right, right->left);
bool isOut = isSym(left->left, right->right);
return isIn && isOut;
}
bool isSymmetric(TreeNode* root) {
if(!root)return true;
return isSym(root->left, root->right);
}
};

104.二叉树的最大深度

  • 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始)
  • 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数或者节点数(取决于高度从0开始还是从1开始)

注意边界条件对代码的影响

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int maxDepth(TreeNode* root) {
int maxD = 0;
if(root){
maxD++;
}else{
return maxD;
}
int maxL = root->left ? maxDepth(root->left) : 0;
int maxR = root->right ? maxDepth(root->right) : 0;
maxD += maxL > maxR ? maxL : maxR;
return maxD;
}
};

111.二叉树的最小深度

迭代法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int minDepth(TreeNode* root) {
int minD = 0;
if(!root)return minD;
minD++;
if(!root->left && !root->right)return minD;
if(!root->left && root->right){
return minD + minDepth(root->right);
}else if(root->left && !root->right){
return minD + minDepth(root->left);
}else{
int minL = minDepth(root->left);
int minR = minDepth(root->right);
minD += minL > minR ? minR : minL;
return minD;
}
}
};

HTTP请求报文和响应报文是怎样的,有哪些常见的字段?

HTTP报文分为请求报文和响应报文。

请求报文

请求报文主要由请求行、请求头、空行、请求体构成。 请求行包括如下字段:

  • 方法(Method):指定要执行的操作,如 GET、POST、PUT、DELETE 等。
  • 资源路径(Resource Path):请求的资源的URI(统一资源标识符)。
  • HTTP版本(HTTP Version):使用的HTTP协议版本,如 HTTP/1.1 或 HTTP/2.0。

请求头的字段较多,常使用的包含以下几个:

  • Host:请求的服务器的域名。
  • Accept:客户端能够处理的媒体类型。
  • Accept-Encoding:客户端能够解码的内容编码。
  • Authorization:用于认证的凭证信息,比如token数据。
  • Content-Length:请求体的长度。
  • Content-Type:请求体的媒体类型。(如 application/jsontext/html 等)
  • Cookie:存储在客户端的cookie数据。
  • If-None-Match:与 ETag 配合使用,用于缓存控制。
  • Connection:管理连接的选项,如 keep-alive。

方便记忆:

1. 基本身份/环境信息(我是谁、能接受什么)
  • Host:我要访问哪个服务器(主机名)
  • User-Agent:我是哪个浏览器(可选提)
  • Accept / Accept-Encoding:我能接受的内容类型/压缩方式
2. 身份认证/状态维持
  • Authorization:我的身份凭证(如 token)
  • Cookie:我本地保存的 cookie,带回来给你看
3. 请求体相关(仅 POST/PUT 用)
  • Content-Type:我发给你是什么格式(如 JSON)
  • Content-Length:我发给你的内容多大
4. 缓存控制(和上次请求有关)
  • If-None-Match:上次你的资源 ETag 是 XX,看看现在变没
  • If-Modified-Since:上次的修改时间是 XX,资源有变吗?
  • Cache-Control:我是否希望使用缓存?

空行是请求头部和请求主体之间的空行,用于分隔请求头部和请求主体。

请求体通常用于 POST 和 PUT 请求,包含发送给服务器的数据。

响应报文

HTTP响应报文是服务器向客户端返回的数据格式,用于传达服务器对客户端请求的处理结果以及相关的数据。一个标准的HTTP响应报文通常包含状态行、响应头、空行、响应体。

状态行包含HTTP版本、状态码和状态消息。例如:HTTP/1.1 200 OK

响应头部也是以键值对的形式提供的额外信息,类似于请求头部,用于告知客户端有关响应的详细信息。一些常见的响应头部字段包括:

  • Content-Type:指定响应主体的媒体类型。
  • Content-Length:指定响应主体的长度(字节数)。
  • Server:指定服务器的信息。
  • Expires: 响应的过期时间,之后内容被认为是过时的。
  • ETag: 响应体的实体标签,用于缓存和条件请求。
  • Last-Modified: 资源最后被修改的日期和时间。
  • Location:在重定向时指定新的资源位置。
  • Set-Cookie:在响应中设置Cookie。
  • Access-Control-Allow-Origin: 跨源资源共享(CORS)策略,指示哪些域可以访问资源。

方便记忆:

1. 内容信息

  • Content-Type:我返回的是什么格式(如 HTML/JSON)
  • Content-Length:内容有多大

2. 服务器身份

  • Server:我是哪个服务器(如 nginx)

3. 状态维持

  • Set-Cookie:设置 cookie,下次你带回来

4. 缓存相关

  • ETag:这是资源的版本号
  • Last-Modified:资源上次修改时间
  • Cache-Control / Expires:多久前有效/过期时间

5. 跨域和重定向

  • Access-Control-Allow-Origin:谁能访问我(CORS)
  • Location:去这个新地址吧(重定向)

空行(Empty Line)在响应头和响应体之间,表示响应头的结束。

响应体是服务端实际传输的数据,可以是文本、HTML页面、图片、视频等,也可能为空。

HTTP有哪些请求方式?

  • GET:请求指定的资源。
  • POST:向指定资源提交数据进行处理请求(例如表单提交)。
  • PUT:更新指定资源。
  • DELETE:删除指定资源。
  • HEAD:获取GET请求相同响应的报文首部,不返回报文主体。
  • OPTIONS:查询服务器支持的请求方法。
  • PATCH:对资源进行部分更新

GET请求和POST请求的区别

  • 用途:GET请求通常用于获取数据,POST请求用于提交数据。
  • 数据传输:GET请求将参数附加在URL之后,POST请求将数据放在请求体中。
  • 安全性:GET请求由于参数暴露在URL中,安全性较低;POST请求参数不会暴露在URL中,相对更安全。
  • 数据大小:GET请求受到URL长度限制,数据量有限;POST请求理论上没有大小限制。
  • 幂等性:GET请求是幂等的,即多次执行相同的GET请求,资源的状态不会改变;POST请求不是幂等的,因为每次提交都可能改变资源状态。
  • 缓存:GET请求可以被缓存,POST请求默认不会被缓存。
0%