【LeetCode Cookbook(C++ 描述)】一刷二叉树之递归遍历(DFS)(上)

news2024/11/16 6:02:35

目录

  • 二叉树的实现
  • LeetCode #144:Binary Tree Preorder Traversal 二叉树的前序遍历
    • 递归解法
      • 「遍历」思路
      • 「分而治之」思路
      • 更多例子:求二叉树最大深度
    • 迭代解法
    • Morris 遍历
  • LeetCode #94:Binary Tree Inorder Traversal 二叉树的中序遍历
    • 迭代解法
    • Morris 遍历
  • LeetCode #145:Binary Tree Postorder Traversal 二叉树的后序遍历
    • 迭代解法
    • Morris 遍历

本系列文章仅是 GitHub 大神 @halfrost 的刷题笔记 《LeetCode Cookbook》的提纲以及示例、题集的 C++转化。原书请自行下载学习。
本篇文章涉及新手应该优先刷的几道经典二叉树递归遍历算法题。

二叉树的实现

最常见的二叉树就是类似链表的链式存储结构,每个二叉树节点有指向左右子节点的指针,这种方式比较简单直观。

struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

这样就可以构建一棵二叉树了:

TreeNode* root = new TreeNode(1);
root->left = new TreeNode(2);
root->right = new TreeNode(3);
root->left->left = new TreeNode(4);
root->right->left = new TreeNode(5);
root->right->right = new TreeNode(6);

//构建出来的二叉树是这样的:
//     1
//    / \
//   2   3
//  /   / \
// 4   5   6

所谓顺序(递归)遍历,即前、中、后序遍历,就是在二叉树遍历框架下的不同位置添加代码:

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

前序位置的代码会在进入节点时执行;中序位置的代码会在左子树遍历完成后、遍历右子树之前执行;后序位置的代码会在左右子树遍历完成后执行。

labuladong

❗️用哈希表表示二叉树比较常见:在一般的算法题中,我们可能会把问题抽象成二叉树结构,但我们并不需要真的用 TreeNode 创建一棵二叉树出来,而是直接用类似哈希表的结构来表示二叉树:

1 -> [2, 3]
2 -> [4]
3 -> [5, 6]

LeetCode #144:Binary Tree Preorder Traversal 二叉树的前序遍历

#144
给你二叉树的根节点 root ,返回它节点值的前序遍历。

二叉树算法有两大思维模式:

  • 遍历」:是否可以通过遍历一遍二叉树得到答案? 如果可以,用一个 traverse() 函数配合外部变量来实现。
  • 分而治之」:是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案? 如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。

无论何种思维模式,我们都需要思考——如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前 / 中 / 后序位置)做? 我们不需要考虑其他节点,递归函数会在所有节点上执行相同的操作。我们使用 traverse() 函数对二叉树进行遍历,实际上与对数组、链表等线性数据结构的遍历没有什么本质的区别,只要是递归形式的遍历,都可以有前序位置和后序位置,分别在递归之前和递归之后

所谓前序位置,就是刚进入一个节点(元素)的时候,后序位置就是即将离开一个节点(元素)的时候,那么进一步,把代码写在不同位置,代码执行的时机也不同

在这里插入图片描述

每个节点都有唯一属于自己的前中后序位置,从而我们可以知道,前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点。相应地,多叉树节点可能有很多子节点,会多次切换子树去遍历,对于每个节点没有唯一的中序位置。

因此,二叉树的所有问题,就是通过在前中后序位置注入巧妙的代码逻辑从而实现自己的目的,我们只需要单独思考每一个节点应该做什么,其他的则抛给二叉树遍历框架,递归会在所有节点上做相同的操作

递归解法

「遍历」思路

对于前序遍历,我们先找出重复的子问题:先取根节点,再遍历左子树,最后遍历右子树。因此,我们需要利用辅助函数 traverse() 来分别遍历左右子树,以及引入外部变量 res 用于存储遍历结果;同时,我们必须保证递归可以被终止,base case没有子节点可供遍历,也就是 root == nullptr

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        traverse(root);
        return res;
    }

private:
    vector<int> res;
    void traverse(TreeNode* root) {
        if (root == nullptr) return;
        //前序位置
        res.push_back(root->val);
        traverse(root->left);
        traverse(root->right);
    }
};

每个节点均被遍历了一次,该算法的时间复杂度为   O ( n ) \ O(n)  O(n) ,额外维护了一个数组,空间复杂度为   O ( n ) \ O(n)  O(n)

「分而治之」思路

如果我们不使用额外的函数方法以及任何外部变量,单纯地递归调用 preorderTraversal() 来完成二叉树的前序遍历,那么可以分解问题为:一棵二叉树的前序遍历结果 = 根节点 + 左子树的前序遍历结果 + 右子树的前序遍历结果

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(), right.begin(), right.end());

        return res;
    }
};

而中序、后序遍历的递归解法原理一致,只需更改主要代码逻辑所处的位置即可。

更多例子:求二叉树最大深度

#104
给定一个二叉树 root ,返回其最大深度。

二叉树的最大深度是指从根节点到最远叶子节点的最长路径上的节点数。

我们采用「遍历」二叉树的思路,显然需要遍历一遍二叉树,用一个外部变量记录每个节点所在的深度,取最大值就可以得到最大深度。由于前序位置是进入一个节点的时候,后序位置是离开一个节点的时候,depth 记录当前递归到的节点深度,我们把 traverse 理解为在二叉树上游走的一个指针,所以需要在前序位置增加 depth ,在后序位置减小 depth

至于对 res 的更新,放到前中后序位置均可,只要保证在进入节点之后、离开节点之前(即 depth 自增之后、自减之前)。

class Solution {
public:
    int maxDepth(TreeNode* root) {
        traverse(root);
        return res;
    }

private:
    int depth = 0, res = 0;  //分别记录遍历到的节点的深度、最大深度
    void traverse(TreeNode* root) {
        if (root == nullptr) return;
        //前序位置
        depth++;
        if (root->left == nullptr && root->right == nullptr) res = max(res, depth);   //到达叶子节点,更新最大深度

        traverse(root->left);
        traverse(root->right);
        //后序位置,回溯,深度减少
        depth--;
    }
};

一棵二叉树的最大深度可以通过子树的最大深度推导出来,这就是「分而治之」思路计算答案的思路——首先利用递归函数的定义算出左右子树的最大深度,然后推出原树的最大深度,主要逻辑则集中放在后序位置

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == nullptr) return 0;
        //利用定义,计算左右子树的最大深度
        int leftMax = maxDepth(root->left);
        int rightMax = maxDepth(root->right);
        //整棵树的最大深度等于左右子树的最大深度取最大值,再加上根节点本身
        return max(leftMax, rightMax) + 1;
    }
};

迭代解法

这种解法与递归方法是等价的,递归隐式地维护了一个栈,而迭代则显式地将这个栈模拟出来。具体思路如下:

  • 初始化一个栈,定义一个二叉树节点 node ,初始化为根节点 root
  • 当栈或 node 节点不为空时:
    • 访问左子树:将 node 的值加入到结果集 res 中,并将 node 压入栈 stk 中,然后 node 指向其左子节点。这个过程一直持续到 node 为空,即没有左子节点为止。
    • 转向右子树:从栈中弹出一个节点(最近被遍历完左子树的节点),将 node 设置为栈顶元素(即最近被弹出的节点)的右子节点,开始遍历右子树。
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        if (root == nullptr) return res;

        stack<TreeNode*> stk;
        TreeNode* node = root;
        while (!stk.empty() || node != nullptr) {
            while (node != nullptr) {
                res.emplace_back(node->val);
                stk.emplace(node);
                node = node->left;
            }
            node = stk.top();
            stk.pop();
            node = node->right;
        }
        return res;
    }
};

该算法时间复杂度与空间复杂度均为   O ( n ) \ O(n)  O(n) 。在遍历过程中,这一解法始终先访问当前节点的值,然后遍历左子树,最后通过栈的帮助下转而遍历右子树。这种方式避免了递归带来的栈溢出风险,特别是在处理非常深的树的时候。

不过,我们可以简化一下代码逻辑,每次递归都是先将根节点放入栈,然后右子树,最后左子树

  • 初始化维护一个栈,将根节点入栈。
  • 当栈不为空时:
    • 弹出栈顶元素 node ,将节点值加入结果数组中。
    • node 的右子树不为空,右子树入栈。
    • node 的左子树不为空,左子树入栈。
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        if (root == nullptr) return res;
        
        stack<TreeNode*> stk;
        stk.push(root);
        
        while (!stk.empty()) {
            TreeNode* node = stk.top();
            stk.pop();
            //访问节点
            res.push_back(node->val); 
            //由于栈是后进先出的,所以需要先压入右子树,再压入左子树
            if (node->right != nullptr) stk.push(node->right);
            if (node->left != nullptr) stk.push(node->left);
        }
        
        return res;
    }
};

优化后的代码时间复杂度、空间复杂度与上一迭代解法一致。

Morris 遍历

有一种巧妙的方法可以在线性时间内,只占用常数空间来实现前序遍历。其核心思想是利用树的大量空闲指针,实现空间开销的极限缩减。实现思路如下:

  1. 新建临时节点,令该节点为 root
  2. 若当前节点的左子节点为空,将当前节点加入答案,并遍历当前节点的右子节点。
  3. 若当前节点的左子节点不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点:
    • 若前驱节点的右子节点为空,则将前驱节点的右子节点设置为当前节点,将当前节点加入答案,并将前驱节点的右子节点更新为当前节点,当前节点更新为当前节点的左子节点。
    • 若前驱节点的右子节点为当前节点,将它的右子节点重新设为空。当前节点更新为当前节点的右子节点。
  1. 重复步骤 2 和步骤 3,直到遍历结束。
class Solution {
public:
    vector<int> preorderTraversal(TreeNode *root) {
        vector<int> res;
        if (root == nullptr) return res;

        TreeNode *p1 = root, *p2 = nullptr;  // p1是当前遍历到的节点,p2 是 p1 的左子节点或者用于Morris遍历的临时右子节点
        while (p1 != nullptr) {
            p2 = p1->left;
            if (p2 != nullptr) { 
            	while (p2->right != nullptr && p2->right != p1) p2 = p2->right;  //找到 p1 左子树中的最右节点
			//如果最右节点的右指针为空,则进行 Morris 遍历的特定操作
                if (p2->right == nullptr) {
                    res.emplace_back(p1->val);
                    p2->right = p1;
                    p1 = p1->left;
                    continue;
                } else p2->right = nullptr;  //如果最右节点的右指针已经指向 p1,说明之前已经访问过p1的左子树了,此时需要恢复树的原状,即将最右节点的右指针置为空
            } else res.emplace_back(p1->val);  //如果 p1 没有左子节点,则直接访问 p1,并转向 p1 的右子树
			//转向 p1 的右子树
            p1 = p1->right;
        }
        return res;
    }
};

LeetCode #94:Binary Tree Inorder Traversal 二叉树的中序遍历

#94
递归解法只需要更改核心代码位置即可,不再赘述。

迭代解法

在前序遍历中,遍历节点的顺序和处理节点的顺序是一致的;而在中序遍历中,访问节点的顺序和处理节点的顺序是不一致的,并且,处理节点是在遍历完左子树之后。总而言之,这一算法应该从根节点开始,一层层地遍历,从左子树最左子节点开始处理节点。

具体思路如下:

  • 初始化一个空栈。
  • 根节点或栈不为空时,从根节点开始:
    • 若当前节点有左子树,一直遍历左子树,每次将当前节点压入栈中。
    • 若当前节点无左子树,从栈中弹出该节点,尝试访问该节点的右子树。
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        if (root == nullptr) return res;

        stack<TreeNode*> stk;
        while (stk.size() > 0 || root != nullptr) {   //当栈不为空或当前节点不为空时,继续遍历
        	//如果当前节点不为空,则尽可能地向左子树深入
            if (root != nullptr) {
                stk.push(root);
                root = root->left;
            }
            //如果当前节点为空,说明已经到达最左端的节点,需要回溯
            else {
                TreeNode* cur = stk.top();
                stk.pop();
                res.push_back(cur->val);
                root = cur->right;
            }
        }

        return res;
    }
};

Morris 遍历

Morris 遍历算法本身就是为中序遍历的非递归实现而设计,但也可以添加额外的逻辑实现前序、后序遍历。其主要利用了叶子节点的空指针来建立临时线索Thread ),以便在遍历过程中无需使用栈或递归即可恢复节点的中序前驱,在不使用栈或递归的情况下实现二叉树的遍历。线索化将这些空指针指向中序遍历中的前驱节点或后继节点。线索提供了在遍历过程中前进或回溯的路径信息。

具体来说,在 Morris 中序遍历中,对于当前遍历到的节点:

  • 如果存在左子节点,则找到该左子树中的最右节点(即中序遍历的前驱节点)。
  • 如果该最右节点的右指针为空,则将其右指针指向当前节点,这样就建立了一条从中序前驱节点到当前节点的线索
  • 当再次访问到这个最右节点时(通过其右指针,即线索),此时已经遍历完了当前节点的左子树,于是将这条线索删除(即将最右节点的右指针置为空),并继续遍历过程。

通过这种方式,Morris 遍历能够在不使用额外空间的情况下,通过线索在树中前进和回溯,从而实现中序遍历(以及前序和后序遍历的变体)。线索的创建和删除是 Morris 遍历的核心操作,它们允许算法在遍历过程中动态地构建和销毁临时路径,从而避免了使用显式的栈或递归栈。

Morris 遍历是一种非破坏性的遍历方法,它只会在遍历过程中暂时改变树的结构,在遍历完成后,通过删除所有创建的线索,从而恢复二叉树的原始结构。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        TreeNode* curr = root;
        while (curr != nullptr) {
            //如果当前节点的左子树为空,则直接访问当前节点并移至右子树
            if (curr->left == nullptr) {
                res.push_back(curr->val);
                curr = curr->right;
            } else {
                //否则,找到当前节点左子树中的最右节点(即中序前驱)
                TreeNode* predecessor = curr->left;
                while (predecessor->right != nullptr && predecessor->right != curr) predecessor = predecessor->right;
                //如果前驱节点的右指针为空,则建立线索
                if (predecessor->right == nullptr) {
                    predecessor->right = curr;
                    curr = curr->left;
                } else {
                    //如果前驱节点的右指针已指向当前节点,说明左子树已遍历完
                    //恢复线索,并访问当前节点,移至右子树
                    predecessor->right = nullptr;
                    res.push_back(curr->val);
                    curr = curr->right;
                }
            }
        }
        
        return res;
    }
};

LeetCode #145:Binary Tree Postorder Traversal 二叉树的后序遍历

#145
递归解法只需要更改核心代码位置即可,不再赘述。

迭代解法

后序遍历的顺序为:左子树 -> 右子树 -> 根节点,根节点需要被处理 3 次。具体思路如下:

  • 初始化一个空栈。
  • 当根节点或者栈不为空时,从根节点开始:
    • 每次将当前节点压入栈中,若当前节点存在左子树,就遍历左子树,否则就遍历右子树。
    • 若当前节点左子树、右子树均不存在,则从栈中弹出该节点。如果当前节点是上一个节点(即弹出该节点后的栈顶元素)的左节点,尝试访问上个节点的右子树;如果不是,那当前栈的栈顶元素继续弹出。

这一算法并不是标准的后序遍历迭代方法,并没有显式地保证在访问根节点之前,其左右子树都已经被访问过,而是通过栈的状态和遍历的顺序来隐含地实现这一点的。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> stk;
        while (!stk.empty() || root != nullptr) {
        	//尽可能地向左子树深入,将沿途的节点压入栈中
            while (root != nullptr) {
                stk.push(root);
                if (root->left != nullptr) root = root->left;  //如果存在左子树,则继续向左
                else root = root->right;  //如果没有左子树,但可能有右子树,尝试转向右子树
            }

            root = stk.top();
            stk.pop();
            res.push_back(root->val);
			//检查栈是否为空,以及栈顶元素的左子节点是否是刚刚弹出的节点
            if (!stk.empty() && stk.top()->left == root) root = stk.top()->right;
            //如果不是,或者栈为空,说明没有更多的子树需要处理
            //将 root 设为 nullptr,以便外层循环可以检查栈是否还有未处理的节点
            else root = nullptr;
        }

        return res;
    }
};

要严格地实现二叉树的后序遍历,我们需要一个额外的指针 prev 来跟踪最近访问的节点,以便判断何时可以安全地访问并处理当前节点

在这个实现中,当遍历到最左端的节点时,该节点会被压入栈中,然后转向处理其右子节点(如果存在):

  • 如果右子节点不存在,或者右子节点就是最近访问过的节点 prev ,那么当前节点(栈顶元素)就可以被安全地处理(即将其值添加到结果向量中),并将其标记为已访问(通过更新 prev )。
  • 如果右子节点存在且未被访问过,则当前节点会再次被压入栈中,以便稍后再次访问其右子节点。
  • 这个过程会一直重复,直到遍历完树中的所有节点。最终,res 数组将包含树的后序遍历结果。
class Solution {
public:
    vector<int> postorderTraversal(TreeNode *root) {
        vector<int> res;
        if (root == nullptr) return res;

        stack<TreeNode *> stk;
        TreeNode *prev = nullptr;
        while (root != nullptr || !stk.empty()) {
        	//尽可能地向左子树深入,并将沿途的节点压入栈中
            while (root != nullptr) {
                stk.emplace(root);
                root = root->left;
            }
            //当无法再向左深入时,弹出栈顶元素,并检查其右子树
            root = stk.top();
            stk.pop();
            // 如果当前节点的右子树为空,或者右子树已经被访问过(即 prev 指向右子节点),则可以安全地访问当前节点的值,并将其添加到结果数组中
            if (root->right == nullptr || root->right == prev) {
                res.emplace_back(root->val);
                prev = root;
                root = nullptr;
                //如果右子树存在且未被访问,则将当前节点重新压入栈中并转向右子节点进行遍历
            } else {
                stk.emplace(root);
                root = root->right;
            }
        }
        return res;
    }
};

Morris 遍历

  1. 新建临时节点,令该节点为 root
  2. 如果当前节点的左子节点为空,则遍历当前节点的右子节点。
  3. 如果当前节点的左子节点不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点:
    • 如果前驱节点的右子节点为空,将前驱节点的右子节点设置为当前节点,当前节点更新为当前节点的左子节点。
    • 如果前驱节点的右子节点为当前节点,将它的右子节点重新设置为空,倒序输出从当前节点的左子节点到该前驱节点这条路径上的所有节点;当前节点更新为当前节点的右子节点。
  1. 重复步骤 2 和步骤 3,直到遍历结束。
class Solution {
public:
    vector<int> postorderTraversal(TreeNode *root) {
        vector<int> res;
        if (root == nullptr) return res;

        TreeNode *p1 = root, *p2 = nullptr;  // p1 用于遍历树,p2 用于寻找 p1 的左子树中的最右节点
        while (p1 != nullptr) {
            p2 = p1->left;
            if (p2 != nullptr) {
            	//寻找 p1 在左子树中的后继节点(即左子树的最右节点)
                while (p2->right != nullptr && p2->right != p1) p2 = p2->right;
                    
                if (p2->right == nullptr) {  //如果 p2 的最右节点为空,说明 p1 是第一次访问
                	//将 p1 的左子树的最右节点指向 p1,建立临时线索
                    p2->right = p1;
                    p1 = p1->left;
                    continue;
                } else {   //如果 p2 的最右节点已经指向 p1,说明 p1 的左子树和右子树都已经被访问过
                	//恢复树的结构,移除临时线索
                    p2->right = nullptr;
                    //添加 p1 的左子树到结果中
                    addPath(res, p1->left);
                }
            }
            //如果没有左子树或左子树已经处理完,转向右子树
            p1 = p1->right;
        }
        //循环结束后,根节点及其右子树还未被处理
        //通过 addPath() 添加根节点及其右子树
        addPath(res, root);
        return res;
    }

private:
	void addPath(vector<int> &vec, TreeNode *node) {
		//辅助函数,用于将一条路径(从右向左)添加到结果 vector 中
        int count = 0;  //记录添加的节点数
        while (node != nullptr) {
            ++count;
            vec.emplace_back(node->val);  //从右向左添加节点值
            node = node->right;  //遍历右子树
        }
        //反转刚才添加的节点,以恢复后序遍历的顺序
        reverse(vec.end() - count, vec.end());
    }
};

呜啊?

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

亲民且友好的Top期刊,最快46天录用!

本期小编解析一本超亲民超友好的Top期刊&#xff0c;发文量大&#xff0c;编辑处理速度极快&#xff08;近期案例46天录用&#xff09;&#xff0c;毕业有高分区发文要求的小伙伴&#xff0c;赶紧码住这一本神刊&#xff01; 期刊简介 Knowledge-Based Systems (KBS) 出版社 …

AI智能测评应用平台项目分享

大家好&#xff0c;我是程序媛雪儿&#xff0c;今天咱们聊个我新学的项目&#xff0c;AI智能评测应用平台系统。 咱们先了解一下这个系统是干嘛的。 一、业务分析 大致业务流程是应用制作者在创建应用页面填写应用信息&#xff0c;依次添加题目和评分规则生成测评应用&#xff…

信息学奥赛初赛天天练-56-CSP-J2019完善程序2-双关键字排序、计数排序、前缀和、前缀自增、后缀自增、数组下标自增

PDF文档公众号回复关键字:20240805 1 完善程序 (单选题 &#xff0c;每小题3分&#xff0c;共30分) 计数排序 计数排序是一个广泛使用的排序方法。下面的程序使用双关键字计数排序&#xff0c;将 n 对 10000以内的整数&#xff0c;从小到大排序。 例如有三对整数 (3,4)、(2,…

标准IO及相关练习

标准IO 能够将指定的数据写入指定的文件中的操作&#xff08;通过文件指针去访问指定的文件&#xff1a;FILE*&#xff09;&#xff0c;标注IO只提供写入或者读取操作&#xff0c;不提供删除文件中的内容&#xff0c;想要删除文件&#xff0c;则需要自己写逻辑来实现。 文件指…

【LeetCode Cookbook(C++ 描述)】一刷哈希表(Hash Table)(下)

目录 LeetCode #349&#xff1a;Intersection of Two Arrays 两个数组的交集LeetCode #383&#xff1a;Ransom Note 赎金信LeetCode #454&#xff1a;4Sum II - 四数相加 II 本系列文章仅是 GitHub 大神 halfrost 的刷题笔记 《LeetCode Cookbook》的提纲以及示例、题集的 C转化…

办公楼子母钟系统,不止显示时间,还可以做临时告示牌

在现代办公环境中&#xff0c;时间管理对于提高工作效率至关重要。传统的时钟往往只能提供最基本的时间显示功能&#xff0c;而在快节奏的办公楼里&#xff0c;一个既能准确显示时间又能发布紧急通知的系统显得尤为必要。本文将介绍办公楼子母钟系统的独特优势及其在不同场景中…

TOOL使用

一、代码生成器 1.页面代码生成&#xff08;前端&#xff09; 生成后会在前端&#xff08;pc&#xff09;代码中看得到代码&#xff0c;可在此做二次开发&#xff1a; 代码生成器中新建不同模块&#xff0c;对应着modules文件夹下文件夹—>生成代码时&#xff0c;选择对应…

50etf期权怎么可以买跌做空吗?

50ETF期权可以做买方也可以做卖方&#xff0c;并且50ETF期权还能够买涨买跌双向交易&#xff0c;50ETF期权可以看涨期权和看跌期权&#xff0c;所以50ETF期权是可以买跌做空的&#xff0c;并且50ETF期权是很适合进行做空操作的&#xff0c;下文为大家介绍50etf期权怎么可以买跌…

如何将.bin文件,hex方式查看里面数据。以自己需要的任何长度来分割

liunux环境编译命令&#xff1a; gcc test.cpp -o testtest.cpp 文件&#xff1a; instring 为需要被分割的文件&#xff1a; outstring 为分割后的文件&#xff1a; #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.…

NSF共享目录未授权访问

NSF共享目录未授权访问 Network File System(NFS)&#xff0c;是由SUN公司研制的UNIX表示层协议(pressentation layer protocol)&#xff0c;能使使用者访问网络上别处的文件就像在使用自己的计算机一样。服务器在启用nfs服务以后&#xff0c;由于fs服务未限制对外访问&#x…

同步时钟系统,防水效果出色,无惧户外雨天环境

在我们的日常生活和工作中&#xff0c;时钟是不可或缺的存在。然而&#xff0c;传统时钟在使用过程中逐渐暴露出一些问题。 传统时钟通常依靠机械结构或简单的电子元件来保持时间的准确性&#xff0c;这使得它们容易受到外界因素的干扰。例如&#xff0c;温度的变化可能导致机芯…

NAS性能巅峰的第二选择 | 极空间Z423标准版开箱体验,强大的不仅仅只是配置

NAS性能巅峰的第二选择 | 极空间Z423标准版开箱体验&#xff0c;强大的不仅仅只是配置 哈喽小伙伴们好&#xff0c;我是Stark-C~ 在去年10月份新品发布会的时候&#xff0c;极空间推出了堪称私有云性能巅峰的『极空间Z423』。作为当前民用级NAS配置的天花板&#xff0c;极空间…

安防视频监控EasyCVR视频汇聚平台设备发送了GPS位置,但是订阅轨迹为空是什么原因?

安防视频监控EasyCVR视频汇聚平台兼容性强、支持灵活拓展&#xff0c;平台可提供视频远程监控、录像、存储与回放、视频转码、视频快照、告警、云台控制、语音对讲、GIS地图、轨迹跟踪、平台级联等视频能力。 用户描述&#xff0c;设备在电子地图中可以查看到定位信息&#xff…

简单操作,轻松创作专业级电子书

在当今数字化时代&#xff0c;电子书的崛起为创作者们带来了前所未有的便利和创作自由。相较于传统印刷书籍&#xff0c;现代电子书的制作过程不再是一项复杂而耗时的任务&#xff0c;而是通过简单操作就能轻松实现的专业级创作。 但是你知道该如何创作专业级的电子书吗&#x…

STM32的FATFS文件系统移植

准备工作 本移植基于STM32HAL库&#xff0c;在开始移植之前准备好一个Cubemx、一个待移植的FATFS源码和一张文件格式为FAT32且大小小于等于32G的SD卡。本项目用的是正点原子的F103开发板。 源码下载&#xff1a;FatFs - Generic FAT Filesystem Module 移植开始 cubemx代码 此…

图片无损压缩工具都有哪些?试试这3款巨好用的压缩软件!支持在线使用

3个图片无损压缩工具推荐&#xff0c;在线免费使用 请你一定要收藏好&#xff01; 1、转转大师 推荐指数&#xff1a;⭐⭐⭐⭐⭐ 直达链接>>pdftoword.55.la 转转大师是一个专业文档转换工具&#xff0c;支持在线编辑&#xff0c;也是微软office官方中国合作伙伴&…

07.FreeRTOS列表与列表项

文章目录 07. FreeRTOS列表与列表项1. 列表和列表项的简介2. 列表相关API函数3. 代码验证 07. FreeRTOS列表与列表项 1. 列表和列表项的简介 列表的定义&#xff1a; typedef struct xLIST {listFIRST_LIST_INTEGRITY_CHECK_VALUE /* 校验值 */volatile UBaseType_t uxN…

吴恩达机器学习-可选的实验室-正则化成本和梯度

目标 在本实验中&#xff0c;你将: 用正则化项扩展前面的线性和逻辑代价函数。重新运行前面添加正则化项的过拟合示例。 import numpy as np %matplotlib widget import matplotlib.pyplot as plt from plt_overfit import overfit_example, output from lab_utils_common i…

关于vs2022项目占用空间太大的问题

之前在分享vs2022项目&#xff08;估计其它vs版本也差不多&#xff09;的时候发现项目占用空间比较大&#xff0c;即使压缩也不利于上传网盘&#xff0c;于是看了一下目录&#xff0c;发现有个隐藏的.vs目录&#xff0c;里面有个和项目同名的文件夹&#xff0c;占用着很大的空间…

⌈ 传知代码 ⌋ MSA+抑郁症模型总结(三)

&#x1f49b;前情提要&#x1f49b; 本文是传知代码平台中的相关前沿知识与技术的分享~ 接下来我们即将进入一个全新的空间&#xff0c;对技术有一个全新的视角~ 本文所涉及所有资源均在传知代码平台可获取 以下的内容一定会让你对AI 赋能时代有一个颠覆性的认识哦&#x…