二叉树解体两种思路

  1. 是否可以通过遍历一遍二叉树得到答案?
    • 用一个traverse函数配合外部变量
    • 实现遍历的思维模式
  2. 是否可以定义一个递归函数,通过子树的答案推导出原问题的答案? 递归三部曲:
    • 函数定义,参数,返回值,充分利用返回值
    • 终止条件?
    • 单次递归条件?

快速排序:

  • 对于nums[lo…hi]
  • 找到分界点p
  • 通过交换元素使得nums[lo…p-1]都小于nums[p]
  • nums[p+1…hi]都大于nums[p]
  • 然后递归处理nums[lo…p-1]和nums[p+1…hi]
  • 最后整个数组就被排序了
void sort(int nums[], int st, int ed) {if (st >= ed) {return;}// ****** 前序位置 ******// 对 nums[lo..hi] 进行切分,将 nums[p] 排好序// 使得 nums[lo..p-1] <= nums[p] < nums[p+1..hi]int p = partition(nums, st, ed);// 左右子数组进行拆分sort(nums, st, p-1);sort(nums, p+1, ed);

先构造分界点,然后去左右子数组递归构造分界点,这就是二叉树的前序遍历!

归并排序:

  • 若要对nums[st…ed]排序
  • 先对nums[st…mid]排序
  • 再对nums[mid+1…ed]排序
  • 最后连接两个有序子数组
void sort(int[] nums, int st, int ed) {if (st == ed) {return;}int mid = (st + ed) / 2;sort(nums, st, mid);sort(nums, mid+1, ed);// ****** 后序位置 ******// 此时两部分子数组已经被排好序// 合并两个有序数组,使 nums[lo..hi] 有序merge(nums, st, mid, ed);
}

先对左右子数组进行排序,然后合并,这就是二叉树后续遍历框架!并且也是传说中的分治算法。

二叉树遍历方式

  • 递归遍历
  • 层序遍历

二叉树递归遍历(DFS)

递归遍历二叉树代码模板:

// 定义二叉树节点
class TreeNode {
public:int val;TreeNode* left;TreeNode* right;TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};// 二叉树递归遍历框架
void traverse(TreeNode* root) {if (root == nullptr) {return;}traverse(root->left);traverse(root->right);
}
  • traverse函数的遍历就是一直往左子节点走,直到遇到空指针走不了了,才尝试往右子节点走,如此循环,直到左右子树都走完了才返回上一层父节点。

  • 递归遍历节点的顺序仅取决于左右子节点的递归调用顺序,与其他代码无关。

理解前中后序遍历

递归遍历的顺序,即函数traverse访问节点的顺序是固定的,root指针在树上移动的顺序是固定的。

但是,我们在traverse函数不同位置写入代码处理逻辑,产生的效果是不同的,这就是前中后序遍历的结果不同,原因在于我们把代码写在了不同位置,产生了不同效果。

// 二叉树遍历框架
void traverse(TreeNode* root) {if (root == nullptr) {return;};// 前序位置traverse(root->left);// 中序位置traverse(root->right);// 后序遍历位置
}

强调:

  • 三种代码写入位置的关键区别在于执行时机不同
  • 真正的算法体不会叫我们简单计算前中后序遍历结果,而是要我们把正确的代码写入到正确的位置上
  • 所以必须准确理解三个位置的代码产生的不同效果

补充:

  • 二叉搜索树(BST)的中序遍历结果是有序的

层序遍历

在这里插入图片描述

写法一:

  • 简单写法,每次把对头元素拿出来,然后左右子节点加入队列
  • 缺点:无法知道节点在第几层,这是常见需求
  • 这个写法用得不多
void levelOrderTraverse(TreeNode* root) {if (root == nullptr) {return;}std::queue<TreeNode*> q;q.push(root);while (!q.empty()) {TreeNode* cur = q.front();q.pop();// 访问cur节点// 左右子树加入队列if (cur->left != nullptr) {q.push(cur->left);}if (cur->right != nullptr) {q.push(cur->right);}}
}

写法二:

void levelOrderTraverse(TreeNode* root) {if (root == nullptr) {return;}std::queue<TreeNode*> q;q.push(root);// 记录当前遍历到的层数(根节点视为第1层)int depth = 1;while (!q.empty()) {// 记录当前层的节点数量int sz = q.size();// 处理当前层的所有节点, sz 个// for (int i = 0; i < sz; i++) {while (sz-- > 0) {TreeNode* cur = q.front();q.pop();// 访问或者处理cur节点...cout << "depth = " << depth << ", val = " << cur->val << endl;// 左右子树加入队列if (cur->left != nullptr) {q.push(cur->left);}if (cur->right != nullptr) {q.push(cur->right);}		}depth++;}
}
  • 注意

写法三:

假设树枝有不同权重!

class State {
public:TreeNode* node;int depth;State(TreeNode* node, int depth) : node(node), depth(depth) {}
};void levelOrderTraverse(TreeNode* root) {if (root == nullptr) {return;}queue<State> q;// 根节点的路径权重和是 1q.push(State(root, 1));while (!q.empty()) {State cur = q.front();q.pop();// 访问 cur 节点,同时知道它的路径权重和cout << "depth = " << cur.depth << ", val = " << cur.node->val << endl;// 把 cur 的左右子节点加入队列if (cur.node->left != nullptr) {q.push(State(cur.node->left, cur.depth + 1));}if (cur.node->right != nullptr) {q.push(State(cur.node->right, cur.depth + 1));}}
}

深入理解前中后序遍历

  1. 前中后序遍历都是什么?
  2. 后序遍历有何特殊之处?
  3. 为什么多叉树没有中序遍历?

回顾二叉树递归遍历框架:

// 二叉树的遍历框架
void traverse(TreeNode* root) {if (root == nullptr) {return;}// 前序位置traverse(root->left);// 中序位置traverse(root->right);// 后序位置
}
  • 暂且不管前中后序的东西,单单看traverse函数
  • 实质上是个能够遍历二叉树节点的函数
  • 本质上和遍历数组,链表完全一样
// 迭代遍历数组
void traverse(vector<int>& arr) {for (int i = 0; i < arr.size(); i++) {}
}// 递归遍历数组
void traverse(vector<int>& arr, int i) {if (i == arr.size()) {return;}// 前序位置traverse(arr, i + 1);// 后序位置
}// 迭代遍历单链表
void traverse(ListNode* head) {for (ListNode* p = head; p != nullptr; p = p->next) {}
}// 递归遍历单链表
void traverse(ListNode* head) {if (head == nullptr) {return;}// 前序位置traverse(head->next);// 后序位置
}
  • 可见单链表和数组一般遍历方式是迭代,即for循环或者while循环的方式显示的迭代
  • 但是也是可以递归遍历的
  • 最关键的:只要是递归遍历,都有前序位置和后序位置,分别在递归之前和递归之后两个 位置
  • 前序位置:刚进入一个节点时
  • 后序位置:即将离开一个节点时

比如倒序打印一条单链表:

void traverse(ListNode* head) {if (head == nullptr) {return;}traverse(head->next);// 在后序位置 执行打印,就可以实现 先递归,每次递归过去想打印时都要先执行递归下一个节点// 直到最后一个节点,递归不了,就开始从最后一个节点开始向前打印cout << head->val << endl;
}
  • 前中后序绝不仅仅是三个顺序不同的list,而是递归遍历二叉树过程中,处理每个节点的三个特殊时间点。

  • 前序位置的代码:在刚刚进入一个二叉树节点时执行

  • 后序位置的代码:在即将离开一个二叉树节点时执行

  • 中序位置的代码:在一个二叉树节点的左子树都遍历完,即将开始遍历右子树的时候开始执行

  • 本文一直视“前中后序”为“位置”,代码执行位置,执行时机

  • 而不是我们常说的“前中后序”为“遍历”

两种解题思路:

  • 遍历一遍二叉树得出答案——>回溯算法思想
  • 分解问题,递归得到答案——>动态规划思想

个人函数命名习惯:

  1. 遍历二叉树解题时:
    • 函数签名一般使用void traverse(…),没有返回值
    • 靠外部变量来计算结果
    • 与此对应:回溯算法的函数签名一般也不需要返回值
  2. 分解问题思路解题:
    • 视具体函数,一般会有返回值
    • 返回值是子问题的计算结果
    • 与此对应:动态规划给出的函数签名是带有返回值的dp函数

二叉树深度

遍历的思路

  • 遍历一遍二叉树 + 外部变量记录每个节点所在深度
  • 取得最大值就是最大深度
class Solution {// 外部变量记录当前深度和最大深度int depth = 0;int res = 0;public:// 本题函数签名, 求取当前树root的最大深度int maxDepth(TreeNode* root) {return res;}// 遍历二叉树求最大深度, void类型签名, 不返回值void traverse(TreeNode* root) {if (root == nullptr) {return;}// 由于根节点算作深度为1, 所以在前序位置depth++depth++;// 在前序位置, 进入一个新节点时候, 若为叶子节点, 记录答案if (root->left == nullptr && root->right == nullptr) {res = std::max(res, deqth);}traverse(root->left);traverse(root->right);// 后序位置 是 离开当前节点的时机// 减少深度, 类似回溯, 需要把深度还回去depth--; }
}; 
  1. 为什么需要前序位置depth++, 后序位置depth–?
    • 把traverse视作一个在二叉树上游走的指针
    • 前序位置:刚进入一个节点时
    • 后序位置:即将离开一个节点
  2. 对于res的更新?
    • 其实放在前中后序位置都可以
    • 只要保证进入节点之后,离开节点之前

分解+递归的思路

class Solution {
public:// 函数签名:输入一个根节点root, 返回二叉树最大深度int maxDepth(TreeNode* root) {// 仅一个节点的深度视为1, 空节点就0if (root == nullptr) return 0;// 当前root的深度等于左右子树深度较大的一个+1// 分解问题:root的结果依赖 左右子树的结果int leftMax = maxDepth(root->left);int righMax = maxDepth(root->right);return 1 + std::max(leftMax, rightMax);}
}
  1. 这道题主要代码思路放在了前中后哪个位置?
    • 后序位置!
    • 后序位置执行深度加的操作!先遍历了左右子树,得到结果后,后序位置执行深度加的操作!
二叉树前序遍历

遍历思路

  • 前序位置:执行加vector操作,前序遍历
  • void签名函数,无返回值
  • 配合外部变量res
// 遍历思路计算前序遍历结果
class Solution {
public:// 外部变量存放前序遍历结果vector<int> res;// 前序遍历输出结果函数签名vector<int> preorderTraversal(TreeNode* root) {traverse(root);return res;}// 二叉树遍历函数, 前序位置执行关键操作void traverse(TreeNode* root) {if (root == nullptr) return;// 前序位置res.push_back(root->val);traverse(root->left);traverse(root->right);}
};
  • 这种遍历思路+外部变量的思路很好想
  • 考虑改成分解问题+递归的思路?来得到前序遍历的结果!

分解思路

  • 前序遍历的特点:根节点排在首位,接着是左子树遍历的结果,最后是右子树的遍历结果。
  • 这就实现了分解问题
    • 一个二叉树前序遍历结果 = 根节点 + 左子树前序遍历结果 + 右子树前序遍历结果
      在这里插入图片描述
class Solution {
public:vector<int> preorderTraversal(TreeNode* root) {vector<int> res;// 空节点返回if (root == nullptr) return res;// 前序遍历的结果 = [root-val , 左子树前序遍历结果 , 右子树前序遍历结果]	res.push_back(root->val);// vector<int> left = preorderTraversal(root->left);res.insert(res.end(), left.begin(), left.end());vector<int> right = preorderTraversal(root->right);res.insert(res.end(), left.begin(), left.end());	return res;}
};

遇到一道二叉树题目:

  1. 是否可以通过遍历一遍二叉树得到答案?
    • 用一个遍历函数traverse(),注意前中后序位置的利用
    • 配合外部变量
  2. 是否可以通过定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?
    • 写出递归函数定义
    • 充分利用函数返回值
  3. 无论哪种思路,最重要的是明白二叉树中每个节点做什么,在什么时机(前中后序)做?

后序位置的特殊之处

  • 前序位置本身没啥特殊之处,一般是习惯把前中后序位置不敏感的代码放在前序位置了。
  • 中序位置主要用在BST场景,完全可以把BST的中序遍历认为是遍历有序数组。

仔细观察前中后位置的代码:

  • 前序位置的代码只能从函数参数中获取父节点传递来的数据。
  • 中序位置的代码不仅有参数数据,还有左子树通过函数返回值传递回来的数据。
  • 后序位置的代码最强大,不仅获取参数数据,还可以同时获得左右子树通过函数返回值传递回来的数据。

所以,某些情况下把代码移动到后序位置效率最高;
有时,只有后序位置的代码能做。

举个例子感受它们能力差别

  1. 如果根节点视作第一层,打印每个节点所在层数
  2. 打印每个节点左右子树各有多少节点
// 二叉树遍历函数
void traverse(TreeNode* root, int level) {if (root == nullptr) return;// 前序位置printf("节点 %s 在第 %d 层", root.val, level);traverse(root->left, level + 1);traverse(root->right, level + 1);
}
// 这样调用
traverse(root, 1);在这里插入代码片
// 定义:输入一棵二叉树,返回这棵二叉树的节点总数
int count(TreeNode root) {if (root == null) {return 0;}int leftCount = count(root.left);int rightCount = count(root.right);// 后序位置printf("节点 %s 的左子树有 %d 个节点,右子树有 %d 个节点",root, leftCount, rightCount);return leftCount + rightCount + 1;
}
  • 一个节点在第几层,我们从根节点遍历过来的过程就能顺带记录,且递归函数的参数就能传递下去
  • 而以一个节点为根的整颗子树有多少节点,必须遍历完子树之后才能得到
  • 只有后序位置才能通过返回值获取子树的信息!
  • 一旦发现题目与子树有关,大概率要给函数设置合理的定义和返回值,在后序位置写关键代码,利用子树的信息了!

二叉树直径

  • 关键理解:直径长度 = 每个节点左右子树最大深度之和
  • 直径可以不经过根节点
  • 要求最长直径长度,那就是遍历整棵树每个节点,然后通过每个节点的左右子树的最大深度,求出每个节点的直径,最后把直径取最大值即可

思路一:

class Solution {
public:// 全局变量记录最大直径长度int maxDiameter = 0; // 解题主签名int diameterOfBinaryTree(TreeNode* root) {// 对每个节点计算直径,求最大直径traverse(root);return maxDiameter;}private:// 遍历二叉树void traverse(TreeNode* root) {if (root == nullptr) return;// 前序位置// 直径 = 左右子树各自最大深度之和,所以这里先算左右子树最大深度int leftMax = maxDepth(root->left);int rightMax = maxDepth(root->right);int myDiameter = leftMax + rightMax;// 更新全局最大直径		maxDiameter = max(maxDiameter, myDiameter);traverse(root->left);traverse(root->right);}// 计算二叉树的最大深度int maxDepth(TreeNode* root) {if (root == nullptr) return 0;// 分解问题+递归思路int leftMax = maxDepth(root->left);int rightMax = maxDepth(root->right);return 1 + max(leftMax, rightMax);}
};
  • 缺陷很明显的解法,最坏时间复杂度是 O(N^2)
  • 因为traverse遍历每个节点(计算直径)时,还会调用递归函数maxDepth,maxDepth同样遍历所有子树的所有节点
  • 究其原因在于,前序位置无法获取子树的信息,只能让每个节点调用maxDepth函数去计算子树的深度,属于想起来容易,实现起来容易,但是时间复杂度过高的解法!
  • 如何优化呢?maxDepth的后序位置是已经知道左右子树的最大深度的,所以将计算直径的逻辑同样放在后序位置,放在已知左右子树最大深度的位置就可以优化时间复杂度。
class Solution {
public:// 全局变量记录最大直径长度int maxDiameter = 0; // 解题主签名int diameterOfBinaryTree(TreeNode* root) {// 对每个节点计算直径,求最大直径maxDepth(root);return maxDiameter;}private:// 这样就不用traverse()单独计算直径了!// 去掉traverse()// 计算二叉树的最大深度int maxDepth(TreeNode* root) {if (root == nullptr) return 0;// 分解问题+递归思路int leftMax = maxDepth(root->left);int rightMax = maxDepth(root->right);// maxDepth的后序位置在此,此处已知左右子树各自的最大深度,这里更新直径就可以了!int myDiameter = leftMax + rightMax;maxDiameter = max(maxDiameter, myDiameter)return 1 + max(leftMax, rightMax);}
};
  • 时间复杂度只有 maxDepth 函数的 O(N) 了。
  • 遇到子树问题,首先想到的是给函数设置返回值,然后在后序位置做文章!

思考题:运用后序位置的题目使用的是遍历思路还是分解问题的思路?

我个人答案:是分解问题的思路,利用了子树返回的答案,作为当前节点的答案的一部分
看了答案:利用后序位置的题目,一般都使用分解问题的思路,因为当前 节点接收并且利用了子树返回返回的信息,意味着把原问题分解成了当前节点+左右子树的子问题。

所以,如果一开始写出递归套递归的解法,大概率要反思,重写成后序遍历优化!

以树的视角看动规/回溯/DFS算法的区别和联系

DFS和回溯算法非常相似,细节上有所区别:

  • DFS 做选择和撤销选择是在for循环外部
  • 回溯 做选择和撤销选择是在for循环内部

结合二叉树,动归/DFS/回溯可以看作二叉树问题扩展,只是关注点不同:

  • 动态规划算法属于分解问题、分治的思路,关注点在于整颗子树,处理子树的方式和利用子树返回的结果。
  • 回溯算法属于遍历的思路,关注点在于节点之间的树枝。
  • DFS算法属于遍历的思路,关注点在于单个节点处理。

例子一:分解问题的思想体现,动归
给一个二叉树,用分解问题的思路写一个count函数,计算共有多少节点

  • 后序位置,可以利用上左右子树的返回的结果!
  • 所以后序位置天然与分解的思路适配!
// 函数签名:输入一颗二叉树,返回二叉树节点总数
int count(TreeNode* root) {if (root == nullptr) return 0;// 分解思路:当前节点下的树的总节点数 == 左 + 右 + 1int leftCount = count(root->left);int rightCount = count(root->right);// 后序位置:已经得到了左右子树的结果!return leftCount + rightCount + 1;
}
  • 动态规划分解问题的思路
  • 着眼点永远是结构解法相同的整个子问题!
  • 类比到二叉树上就是递归处理子树
  • 注意后序位置才可以充分利用处理好的子树结果!

类比一个经典动归:
斐波那契

// f(n) 计算第 n 个斐波那契数
int fib(int n) {// base case if (n == 0 || n == 1) return n;// fib() 调用 本身是分解问题return fib(n - 1) + fib(n - 2);
}

例子二:回溯算法思想
(这里是java, 请豆包帮我换成cpp的)

void traverse(TreeNode root) {if (root == null) return;printf("从节点 %s 进入节点 %s", root, root.left);traverse(root.left);printf("从节点 %s 回到节点 %s", root.left, root);printf("从节点 %s 进入节点 %s", root, root.right);traverse(root.right);printf("从节点 %s 回到节点 %s", root.right, root);
}
  • 总之对于回溯来说,
    • 在对某个子路径调用递归函数traverse()遍历之前,就是即将进入时刻,属于该路径的前序
    • 在对某个子路径调用递归函数traverse()遍历之后,就是退出这个节点,回溯时刻!属于该路径后序么?(不确定这句话对不对,豆包帮我改!)

把二叉树进化成多叉树:(java 改成cpp!)

// 多叉树节点
class Node {int val;Node[] children;
}void traverse(Node root) {if (root == null) return;for (Node child : root.children) {printf("从节点 %s 进入节点 %s", root, child);traverse(child);printf("从节点 %s 回到节点 %s", child, root);}
}

从这个多叉树的遍历框架可以延伸出回溯算法框架套路:

void backtrack(...) {// base caseif (...)  return;for (int i = 0; i < ...; i++) {// 做选择...// 进入下一层决策树backtrack(...);// 撤销刚才的决策...}
}

回溯算法关注点是树的一条条树枝!
(改cpp)

// 回溯算法核心部分代码
void backtrack(int[] nums) {// 回溯算法框架for (int i = 0; i < nums.length; i++) {// 做选择used[i] = true;track.addLast(nums[i]);// 进入下一层回溯树backtrack(nums);// 取消选择track.removeLast();used[i] = false;}
}

在这里插入图片描述
例子三:DFS的思想体现,着眼于节点处理
一棵二叉树,请你写一个 traverse 函数,把这棵二叉树上的每个节点的值都加一。,代码如下:

void traverse(TreeNode* root) {if (root == nullptr) return;// 遍历过的每个节点的值加一root->val++;traverse(root->left);traverse(root->right);
}

看具体的 DFS 算法问题,比如
一文秒杀所有岛屿题目 中讲的前几道题,我们的关注点是 grid 数组的每个格子(节点),我们要对遍历过的格子进行一些处理,所以我说是用 DFS 算法解决这几道题的:

// DFS 算法核心逻辑
void dfs(int[][] grid, int i, int j) {int m = grid.length, n = grid[0].length;if (i < 0 || j < 0 || i >= m || j >= n) {return;}if (grid[i][j] == 0) {return;}// 遍历过的每个格子标记为 0grid[i][j] = 0;dfs(grid, i + 1, j);dfs(grid, i, j + 1);dfs(grid, i - 1, j);dfs(grid, i, j - 1);
}

总的来说:

  • 动态规划关注整颗子树
  • 回溯关注节点之间的树枝
  • DFS关注单个节点

理解为什么回溯算法和 DFS 算法代码中「做选择」和「撤销选择」的位置不同了,看下面两段代码:

// DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面
void dfs(Node* root) {if (!root) return;// 做选择printf("enter node %s\n", root->val.c_str());for (Node* child : root->children) {dfs(child);}// 撤销选择printf("leave node %s\n", root->val.c_str());
}// 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面
void backtrack(Node* root) {if (!root) return;for (Node* child : root->children) {// 做选择printf("I'm on the branch from %s to %s\n", root->val.c_str(), child->val.c_str());backtrack(child);// 撤销选择printf("I'll leave the branch from %s to %s\n", child->val.c_str(), root->val.c_str());}
}

回溯算法必须把「做选择」和「撤销选择」的逻辑放在 for 循环里面,否则怎么拿到「树枝」的两个端点?

  • 那dfs的重点呢?在于处理节点本身?不关注其他?(豆包回答我)

层序遍历:

// 输入一棵二叉树的根节点,层序遍历这棵二叉树
int levelTraverse(TreeNode* root) {if (root == nullptr) return;queue<TreeNode*> q;q.push(root);int depth = 0;// 从上到下遍历二叉树的每一层while (!q.empty()) {int sz = q.size();// 从左到右遍历每一层的每个节点for (int i = 0; i < sz; i++) {TreeNode* cur = q.front();q.pop();// 将下一层节点放入队列if (cur->left != nullptr) {q.push(cur->left);}if (cur->right != nullptr) {q.push(cur->right);}}depth++;}return depth;
}
  • while循环和for循环分管从上到下和从左到右的遍历!合力完成了层序遍历!
    在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/914999.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/914999.shtml
英文地址,请注明出处:http://en.pswp.cn/news/914999.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何区分Bug是前端问题还是后端问题?

在软件测试中,精准定位Bug的归属(前端 or 后端)是高效协作的关键。以下是系统化的排查方法,结合技术细节和实战技巧: 1. 核心判断逻辑 「数据 vs 展示」二分法: 后端问题:数据本身错误(API返回错误数据/逻辑错误/数据库问题) 前端问题:数据正确但展示异常(UI渲染错…

深入解析操作系统中的文件控制块(FCB):从原理到现代实现演进

一、FCB的核心概念与基础结构文件控制块&#xff08;File Control Block, FCB&#xff09;是操作系统管理文件的核心元数据结构&#xff0c;充当文件系统与物理存储间的桥梁。FCB本质上是文件在内存中的映射&#xff0c;包含文件的所有管理信息。传统FCB通常占据32-44字节空间&…

python原生处理properties文件

这个工具类使用 Python 的 configparser 模块操作 .properties 文件&#xff0c;核心是将 .properties 格式适配为 configparser 支持的 .ini 格式。 核心代码解释 1. 类初始化与配置解析 class Properties:def __init__(self, file_path: str, encoding: str utf-8):self.fil…

【java 安全】 IO流

前言 IO是指 Input/Output&#xff0c;即输入和输出。以内存为中心&#xff1a; Input指从外部读入数据到内存&#xff0c;例如把文件从磁盘读取到内存&#xff0c;从网络读取数据到内存等等。Output指把数据从内存输出到外部&#xff0c;例如把数据从内存写入到文件&#xff0…

音视频同步技术初剖析:原理、实现与FFmpeg分析

音视频同步的基本原理 音视频同步主要依靠以下几个关键点&#xff1a;时间戳机制&#xff1a; 在封装格式(如MP4)中&#xff0c;音频帧和视频帧都带有时间戳(PTS, Presentation Time Stamp)这些时间戳表示该帧应该在什么时间被呈现同步策略&#xff1a; 音频为主时钟&#xff1…

掌控网页的魔法之书:JavaScript DOM的奇幻之旅

掌控网页的魔法之书&#xff1a;JavaScript DOM的奇幻之旅 在网页开发的世界里&#xff0c;JavaScript就像一位魔法师&#xff0c;而DOM&#xff08;文档对象模型&#xff09;则是它的魔法之书。没有DOM&#xff0c;JavaScript就像失去了咒语的巫师&#xff0c;无法操控网页的元…

【C语言】深入理解柔性数组:特点、使用与优势分析

C语言学习 柔性数组 友情链接&#xff1a;C语言专栏 文章目录C语言学习前言&#xff1a;柔性数组一、柔性数组的特点二、柔性数组的使用三、柔性数组的优势总结附录上文链接专栏前言&#xff1a; 在有结构体和动态内存分配的知识后&#xff0c;今天咱们来说说柔性数组吧&…

RV126平台NFS网络启动终极复盘报告

1. 初始目标与环境目标: 将RV1126开发板的启动方式&#xff0c;由从eMMC内部存储挂载根文件系统&#xff08;rootfs&#xff09;&#xff0c;切换为通过网络挂载位于NFS服务器上的根文件系统。动机: 提升开发调试效率&#xff0c;实现代码修改后仅需重启即可验证&#xff0c;免…

一台显示器上如何快速切换两台电脑主机?

我注意到很多人会遇到一个常见的情况&#xff1a;他们有两台电脑&#xff0c;一台旧的用来处理基本的办公任务&#xff0c;另一台新的用来玩游戏。新手通常会用 DP端口连接第一台电脑的显示器&#xff0c;用 HDMI 连接第二台电脑。当他们想在两台电脑之间切换时&#xff0c;经常…

抗辐照与国产替代:ASM1042在卫星光纤放大器(EDFA)中的应用探索

摘要&#xff1a;本文以国科安芯推出的ASM1042芯片为例&#xff0c;通过分析ASM1042的抗辐照性能、高速数据传输能力、可靠性以及国产化优势&#xff0c;结合EDFA系统的需求特点&#xff0c;深入探讨了其在商业卫星光纤放大器&#xff08;EDFA&#xff09;项目中的应用潜力。AS…

鸿蒙ArkUI:声明式开发,高效构建全场景体验

目录 导言&#xff1a;开启鸿蒙应用开发的新范式 ArkUI框架概览 - 鸿蒙UI的灵魂 深入核心 - 声明式UI开发范式 命令式 vs 声明式&#xff1a;范式革命 ArkUI如何实现声明式&#xff1f; 创建内置组件 创建自定义组件 自定义组件的基本结构 ArkUI框架的核心特性与优势 …

数据查找 二叉查找树

查找一般分为有序查找和无序查找&#xff0c;这边在讲有序查找例二分查找二分查找就是在有序数组中&#xff0c;通过mid(lowhigh)/2来判定中间值&#xff0c;将中间值与待查找的值进行比较&#xff0c;如果待查找的值大于中间值&#xff0c;那么就将范围缩小&#xff0c;查找右…

几款开源的安全监控与防御工具分享

安全监控与防御工具概述 在现代网络安全架构中,合理选择和部署一系列的安全监控、检测、响应工具至关重要。下面我们将介绍一些常见的安全工具,包括 Elkeid、Wazuh、Caldera、ELK、Snort、Suricata、OpenHFW、OSSEC、GScan 和 Sysom,并详细介绍它们的下载链接、用处、使用方…

Elasticsearch:ES|QL 改进的时间线

作者&#xff1a;来自 Elastic Toms Mura 让我们回顾一下 ES|QL 的历史和它的改进。 更多阅读&#xff0c;Elasticsearch&#xff1a;ES|QL 查询展示。 Elasticsearch 配备了众多新功能&#xff0c;帮助你为自己的用例构建最佳搜索方案。查看我们的示例笔记本了解更多内容&…

Linux | Bash 子字符串提取

注&#xff1a;本文为 “ Bash 子字符串提取” 相关合辑。 英文引文&#xff0c;机翻未校。 如有内容异常&#xff0c;请看原文。 How to Extract Bash Substring? [5 methods] 如何提取 Bash 子字符串&#xff1f;[5 种方法] 2024-04-28 00:00:00 In Bash, a substring is…

Vue2 前端开发 - vue-quill-editor 富文本编辑器(编辑器基础案例、编辑器配置参数解读、编辑器事件)

一、vue-quill-editor 1、vue-quill-editor 概述vue-quill-editor 是一个基于 Quill 富文本编辑器的 Vue 组件vue-quill-editor 在 Vue 2 项目中可以很方便地集成与使用2、vue-quill-editor 安装 执行如下指令&#xff0c;安装 vue-quill-editor npm install vue-quill-editor …

断网情况下,网线直连 Windows 笔记本 和Ubuntu 服务器

在断网情况下&#xff0c;通过网线直连 Windows 笔记本 和 Ubuntu 服务器&#xff0c;并使用 VSCode 访问服务器及 Docker 容器 的步骤如下&#xff1a;1. 物理连接&#xff08;网线直连&#xff09; 1.1 使用网线连接 用 网线&#xff08;Cat5e 或更高&#xff09; 连接 Windo…

消息队列总结

为什么需要消息队列&#xff1f; 随着互联网快速发展&#xff0c;业务规模不断扩张&#xff0c;技术架构从单体演进到微服务&#xff0c;服务间调用复杂、流量激增。为了解耦服务、合理利用资源、缓冲流量高峰&#xff0c;「消息队列」应运而生&#xff0c;常用于异步处理、服务…

C#引用转换核心原理:类型视角切换

&#x1f50d; C#引用转换核心原理&#xff1a;类型视角切换 引用类型由内存指针和类型标记组成&#xff08;如图1&#xff09;。引用转换不改变内存地址&#xff0c;仅改变编译器识别对象的“视角”&#xff1a; B myVar1 new B(); // 实际B类型对象 A myVar2 (A)myV…

重要发布丨MaxKB V2正式发布,助力用户快速构建企业级智能体

2025年7月18日&#xff0c;MaxKB V2版本正式发布。MaxKB是一个强大易用的企业级智能体平台&#xff0c;致力于解决企业AI落地所面临的技术门槛高、部署成本高、迭代周期长等问题&#xff0c;让企业用户落地AI更简单。 秉承“开箱即用&#xff0c;伴随成长”的设计理念&#xff…