数据结构—树

文章目录

  • 9.树
    • (1).树的基本概念
      • #1.基本定义
      • #2.树的广义表表示法
      • #3.基本术语
    • (2).树的存储结构
      • #1.标准形式(常用)
      • #2.逆存储形式
      • #3.孩子兄弟存储法
    • (3).并查集
      • #1.我们到底想解决什么问题
      • #2.并查集结点
      • #2.Find(查)
      • #3.Union(并)
      • #4.例子
    • (4).树的遍历
      • #1.前序遍历
      • #2.后序遍历
      • #3.遍历的非递归实现
        • i.非递归前序遍历
        • ii.非递归后序遍历
      • #4.层序遍历
    • (5).树的线性表示
      • #1.双亲表示法
      • #2.层号表示法
    • 小结

9.树

  接下来我们就要走出一对一的线性结构,来研究一下一对多的树形结构了,树的结构其实非常常见,比如我们在对文件分类的时候,同一个目录下有多个文件、目录,而子目录下还有更多文件,这就是一个比较典型的一对多关系。

(1).树的基本概念

#1.基本定义

  树是由 n ( n ≥ 0 ) n(n\ge0) n(n0)个结点组成的有限集T,它或为空树( n = 0 n=0 n=0);或为非空树。对于非空树T,它满足以下两个条件:

  • 1.有一个特定的结点,称之为根结点(root)
    1. 除根结点以外,其余的结点分成 m ( m > 0 ) m(m>0) m(m>0)个互不相交的有限集 T 0 , T 1 , T 2 , . . . , T m − 1 T_0, T_1, T_2,..., T_{m-1} T0,T1,T2,...,Tm1,其中每个集合都是一棵树,称为根结点的子树

  也就是说,树的定义本身是一个递归定义,例如下图:
p23

  其中A是根结点,B,C和D分别构成了三棵子树,你可以依次递归地判断下去,直到遇到一棵空树

#2.树的广义表表示法

  例如对于上面这棵树,我们可以表示为A(B(E, F), C, D(G(H))),字母后接的括号内包含该结点所有的子结点

#3.基本术语

  • 结点:树中的每个独立单元
  • 结点的度(次数):一个结点的子树的个数称为结点的度(类比图中结点的出度/入度)
  • 叶子结点:度为0的结点称为叶子结点,他没有子树
  • 分支结点:度不为0的结点
  • m次完全树:假设树T是一棵m次数,如果T中非叶子结点的次数都为m,则称树T为一棵m次完全树
  • 双亲结点和子结点:结点的子树的根结点称为该结点的子结点,而该结点也称为子树根结点的双亲结点(父结点)
  • 兄弟结点:如果结点k有两个或两个以上子结点,则结点k的所有子结点互为兄弟结点
  • 路径:对于树种的任意两个不同的结点 k i k_i ki k j k_j kj,如果 k i k_i ki出发能够“自上而下地”通过树种的结点到达结点 k j k_j kj,则称 k i k_i ki k j k_j kj存在一条路径
  • 路径长度:路径中所包含的边的数量称为路径长度(路径的长度等于这条路径上的结点个数-1
  • 结点的祖先:如果从结点 k i 0 k_{i_0} ki0到结点 k i n k_{i_n} kin有路径 ( k i 0 , k i 1 , . . . , k i n ) (k_{i_0}, k_{i_1}, ..., k_{i_n}) (ki0,ki1,...,kin),则称 k i 0 , k i 1 , . . . , k i n − 1 k_{i_0}, k_{i_1},..., k_{i_{n-1}} ki0,ki1,...,kin1都是结点 k i n k_{i_n} kin的祖先
  • 结点的后代:结点 k i 1 , k i 2 , . . . , k i n k_{i_1},k_{i_2},...,k_{i_n} ki1,ki2,...,kin都是结点 k i 0 k_{i_0} ki0的后代
  • 结点的层次:将根的层次定义为0,其余结点所在的层次都等于其父结点的层次+1(一般也有将根结点层次定义为1的,空树的层次则为0,这里采取ECNU数据结构课中给出的定义
  • 高度(深度):树中层次最大的结点的层次称为树的深度
    在这里插入图片描述

  我们再拿这张图举个例子, k 0 k_0 k0的层次是0, k 1 , k 4 , k 5 k_1, k_4, k_5 k1,k4,k5的层次是1,以此类推,并且该树的深度为3

  • 有序树:如果给定的m次树,结点的各子树从左到右是有次序(不可交换)的,那么称该树为有序树
    p24

  例如在这里我们就使用了一棵有序树来表示 ( A + B ) × 5 ÷ ( 2 × ( C − D ) ) (A+B)\times5\div(2\times(C-D)) (A+B)×5÷(2×(CD))的值,有趣的是,如果你已经提前了解了二叉树的前、中、后序遍历之后,你就会发现,表达式树的前、中、后序遍历得到的序列正是前缀、中缀、后缀表达式,比如我们用后序遍历可以得到: A B + 5 ∗ 2 C D − ∗ / AB+5*2CD-*/ AB+52CD/,是不是还挺神奇的,之后我们会再讲到树的遍历

  • 森林 m ( m ≥ 0 ) m(m\ge0) m(m0)棵互不相交的树的集合

(2).树的存储结构

  说了这么多,接下来我们应该看看怎么把一棵树存储起来了,不过因为树是非线性结构,我们必须采取一些措施把它映射到一个线性的结构上去,否则我们就没有办法存储了。

#1.标准形式(常用)

  我们采取如下的结构体来存储树中的结点:

struct TreeNode
{int val;TreeNode* child[MAXN];
};

  很简单的结构,首先存储自己结点的值,然后存储所有子结点的指针,以便于找到所有的子结点,它的存储结构图如下:
p25

  还是比较好理解的对吧,这里向上的小箭头代表空指针,意味着存储空间的此位不存在子结点,不过这样的结构很有可能造成比较严重的空间浪费,因此在C++中我们可以采取vector来替代原生数组:

struct TreeNode
{int val;vector<TreeNode*> child;
};

  同理,能够更好利用零散空间的链式存储也比较适合这种场景,我们可以把存储子结点的存储结构换成链表,例如:

struct TreeNode
{int val;list<TreeNode*> child;
};

  带有vector的在C语言中不好实现,但是使用链表的实现起来就没有那么复杂了,不过这二者对于我们来说不会有很大的差异,因为后续的操作多数都是要对整个包含子结点的存储结构进行遍历的,因此我们可能不太需要随机访问

  这种存储结构对于双亲找子的过程非常方便,我们只要知道一个结点,就可以很轻松地找到它的所有子结点,但是如果我们知道了一个结点,希望找到它的双亲结点,在这种存储方式之下就不是很容易了,这个问题其实看起来就和链表的某个结点希望找到其上一个结点一样,如果我们不额外耗费一点空间,那肯定是需要耗费更多的时间来完成这件事的

#2.逆存储形式

  我们把每个结点都存自己的子结点的过程逆转一下,每个结点都存自己的双亲结点,结构体变成这样:

struct TreeNode
{int val;TreeNode* parent;
};

  这样就可以非常容易地找到自己的双亲结点了,它的存储形式是这样:
p26

  但是这样一来,想找到自己的子结点又变得困难了起来,所以怎么办呢?这还不好办,就像链表为了快速找到前后结点一样,我们设置一个双向的结点就可以了:

struct TreeNode
{int val;TreeNode* parent;vector<TreeNode*> child;
};

  这就完美了,虽然每个结点占用的空间可能比较大,但是至少我们在时间上有了很大的优化,无论是找子结点还是找双亲结点都可以直接通过指针跳转了

#3.孩子兄弟存储法

  这回我们就不存全部的结点了,我们每个结构体中只存储两个结点的指针:自己的第一个子结点以及自己右边的第一个兄弟结点,结构体定义如下:

struct TreeNode
{int val;TreeNode* firstChild;TreeNode* nextSibling;
};

p27

  这种存储方式的好处是,我们可以很好地解决上面几种存储方法空间消耗很大的问题,同时我们把多叉树通过这种方式转换成了二叉树,我们对于二叉树的一些操作对于这种存储方式的多叉树也是可以使用的

(3).并查集

#1.我们到底想解决什么问题

  我们怎么表示一个集合呢?其实用什么都行,比如我上学期C++课上写过一个用数组实现的集合:

class integerSet
{
private:int* arr;size_t _size;size_t _capacity;
public:static constexpr size_t npos = -1;struct bad_integerSet : public std::exception{bad_integerSet() = default;const char* what() const throw (){return "bad_integerSet";}};
public:integerSet() = delete;integerSet(size_t size): arr(new int[size+1] {0}), _size(0), _capacity(size) {}integerSet(const integerSet& s1);integerSet(integerSet&& s1) noexcept;~integerSet();size_t find(int elem) const;void insert(int elem);integerSet setunion(const integerSet& s1) const;integerSet setdifference(const integerSet& s1) const;integerSet setintsection(const integerSet& s1) const;integerSet setsymmetricdifference(const integerSet& s1) const;integerSet& operator=(const integerSet& s1);int& operator[](size_t _index);bool operator==(const integerSet& s1) const;size_t size() const;size_t capacity() const;bool isSubset(const integerSet& s1) const;bool isMember(int m) const;bool isEmpty() const;void clear();void erase(int elem);
};

  它的实现你倒是确实可以试一试,不是很困难,不过如果你的集合无序,有一些操作做起来会比较困难,所以C++的STL中提供的集合是基于红黑树(set)/哈希表(unordered_set) 实现的,但是这样就有点麻烦了

  我们接下来要考虑的问题,是比较简单的,假设说我们的一个数组中存在很多个集合,并且这些集合之间不存在交集(也就是说这些集合一起构成了这个数组的划分),我们如何在更快的时间内,表示出其中每一个元素的集合所属关系呢?要明确的一点是:在我们当前研究的集合中,元素a在集合A中,元素b在集合B中,如果元素b也在集合A中,那么集合A和集合B是一个集合,也就是说,一旦在两个集合中出现同一个元素,那么这两个集合就可以直接合并,那我们是不是可以用树来表达这个结构呢?

  例如对于三个集合:{0, 6, 7, 8}, {1, 4, 9}, {2, 3, 5},假设我们把第一个元素作为根,那么就有:
p28

  哇哦,我们用双亲存储法构建这棵树,就可以把一个集合描述出来,那么如果 S 1 S_1 S1中的结点4,正好也属于 S 0 S_0 S0呢?就比如下面这样:
p29

  这种情况下出现了多对一的结构,那么理论上讲我们用就不能描述了,可能需要用到的结构,但是我们好像忘了点什么事情:

在我们当前研究的集合中,元素a在集合A中,元素b在集合B中,如果元素b也在集合A中,那么集合A和集合B是一个集合

  既然4是 S 0 S_0 S0 S 1 S_1 S1共有的元素,那么集合 S 0 S_0 S0和集合 S 1 S_1 S1应当是一个集合,所以我们直接把 S 1 S_1 S1的根合并到 S 0 S_0 S0去,形成一个更大的集合,这样就可以把这些数据全都加到同一个集合里面去了:
p30

  在这种集合中,我们怎么判定两个元素是否属于同一个集合呢?这个也简单,因为每个结点都可以找到自己的双亲结点,所以我们只要一直判断到根结点,看看两个元素是不是能够找到同一个根结点即可

  所以综上,我们的集合只需要两个操作Union(并)Find(查) 两个操作即可,我们研究的这一类集合,也就叫做并查集

#2.并查集结点

  并查集的每个结点要怎么存呢?其实比较简单,对于一个数组,下标是它的值,数组内对应的数字是它的双亲结点,而对于根结点,可以存一个负数(表示整个根结点下有多少个结点),也可以存自己的下标(这意味着如果找到下标和存储值相同的,就已经找到根了)

  所以在这里我们采用前者,前者对于一些问题的解决来说非常方便,比如找出朋友圈中人数最多的朋友圈的数量,需要注意的是,如果我们采取前者,在初始化的时候就需要我们把数组每一个元素对应的值都改成-1,这样每一个元素都是自己的根,对于后续我们的操作会非常方便

#2.Find(查)

  先说查是因为并操作也需要先依赖于查操作,所以我们可以给出如下代码:

int find(vector<int>& dsu, int x)
{if (dsu[x] < 0) {return x;}return find(dsu, dsu[x]);
}

  非常简单的函数,如果找到一个值小于0的,就直接返回,如果不是,则继续向着根找,直到找到一个小于0的就结束了,这样是不是很简单呢?判断两个元素是不是在一个集合里的操作就是这么简单

#3.Union(并)

  然后就是并,在这里我们采取一个简单策略,如果x和y需要合并,那就直接把y对应的整棵树合并到x上面去,代码如下:

int Union(vector<int>& dsu, int x, int y)
{int fx{ find(dsu, x) }, fy{ find(dsu, y) };if (fx != fy) {dsu[fx] += dsu[fy];dsu[fy] = fx; }return fx;
}

  我们首先找到两个结点对应的根,如果两个对应相同的根,就不需要合并,但如果是不同的根,我们直接把y对应的整棵树都合并进x对应的树,所以在这里只要让fy指向fx,把fx的值加上fy的值即可

#4.例子

  就是这样,并查集的代码加起来20行不到,就可以实现我们需要的功能了,所以来看看这个例子:
p31

  这里我直接给代码了:

#include <iostream>
#include <vector>
using namespace std;int find(vector<int>& circle, int x)
{if (circle[x] < 0) {return x;}else {return circle[x] = find(circle, circle[x]);}
}int Union(vector<int>& circle, int rootX, int rootY)
{int fx{ find(circle, rootX) }, fy{ find(circle, rootY) };if (fx != fy) {circle[fx] += circle[fy];circle[fy] = fx; }return fx;
}int main()
{int N{ 0 }, M{ 0 };cin >> N >> M;vector<int> circle(N + 3, -1);for (int i = 0; i < M; i++) {int cnt{ 0 }, root{ 0 }, in{ 0 };cin >> cnt;for (int j = 0; j < cnt; j++) {cin >> in;if (j == 0) {root = in;}else if (in != root) {int rt{ find(circle, in) };if (root != rt) {root = Union(circle, rt, root);}}}}int min_val{ 0 };for (auto& i : circle) {if (i < min_val) min_val = i;}cout << -min_val << endl;return 0;
}

  return circle[x] = find(circle, circle[x]);这里利用了一个压缩存储的特性,如果一个元素找到的不是整棵树的根,那就把它合并到根上去,这样可以让一棵树变得更 “扁”,从而提高并查集的查找效率。

(4).树的遍历

  我们约定,遍历树的时候总是从左到右,例如:
p32

  其中,橙色箭头是向前的过程,蓝色箭头是回溯的过程,我们一直向左找到最左,如果没有子结点了就开始回溯,就这样一直搜索,直到最终回到根结点上,这样的过程实际上是深度优先搜索(Depth-First Search) 的搜索模式,接下来我们就来看看怎么实现对于树的遍历

  你有没有觉得,这个问题用递归好像很好解决呢?

#1.前序遍历

  首先是前序遍历,又称先序遍历,即每次遇到一个结点,首先把结点本身的值打印出来,然后再打印子结点的值,所以我们可以很轻松地写出递归版本的前序遍历代码:

void PreOrderTraversal(const TreeNode* root)
{if (!root) {cout << root->val << " ";for (const auto child : root) {PreOrderTraversal(child);}}
}

  这就结束了?这就结束了,因为树的定义本身是递归的,所以每一个子结点都可以对应一颗子树,而子树的遍历流程跟双亲结点一模一样,所以我们可以用一个非常非常简单的递归函数完成前序遍历的流程

#2.后序遍历

  后序遍历就是在所有的子结点打印完再打印本身的值,所以体现到代码上就是:

void PreOrderTraversal(const TreeNode* root)
{if (!root) {for (const auto child : root) {PreOrderTraversal(child);}cout << root->val << " ";}
}

  这俩真是一个比一个简单,后序遍历只是打印顺序有差异,所以我们只需要把cout打印放到递归的后面,就实现了!这多简单啊,对吧?

#3.遍历的非递归实现

  那么有一些很讨厌的题目,要求你用非递归的方式去实现一遍前序和后序遍历,这当然是可以做的,这就需要我们用一个栈去手动模拟递归遍历树的流程

i.非递归前序遍历
void NoRecursionPreOrderTraversal(const TreeNode* root)
{stack<const TreeNode*> st;st.push(root);while (!st.empty()) {const TreeNode* ptr{ st.top() };st.pop();cout << ptr->val << " ";for (int j = ptr->child.size() - 1; j >= 0; j--) {st.push(ptr->child[j]); // 我们需要倒过来入栈,才能保证后面遍历顺序正确}}
}

  那么对于下面这棵树,我们就很容易得到它的正确遍历结果了:0 1 4 5 6 2 3 7 8 9,你可以自己用这段代码试试看
p33

ii.非递归后序遍历
void NoRecursionPostOrderTraversal(const TreeNode* root, int _Msize)
{stack<const TreeNode*> st;vector<int> mark(_Msize * 2, 0);st.push(root);mark[0] = 0;while (!st.empty()){const TreeNode* ptr{ st.top() };if (mark[st.size() - 1] == 0 && !ptr->child.empty()){mark[st.size() - 1] = 1;for (int j = ptr->child.size() - 1; j >= 0; j--){if (!ptr->child.empty()){st.push(ptr->child[j]);mark[st.size() - 1] = 0;}}}if (st.top()->child.empty() || mark[st.size() - 1] == 1) {cout << st.top()->val << " ";st.pop();}}
}

  后序遍历则要复杂的多,参数部分我们还要传入树的结点个数,以便mark数组正常工作,这里我不做过多解释,这一部分可以选择性学习。

#4.层序遍历

  层序遍历就是如同下图的树,我们从根结点开始一层一层打印出来,比如这里的层序遍历结果应该就是0 1 2 3 4 5 6 7 8 9
p33

  所以怎么写呢,我们发现这个东西有点像广度优先搜索(Breath-First Search),所以我们需要采用队列来实现层序遍历:

void LevelOrderTraversal(const TreeNode* root)
{queue<const TreeNode*> qt;qt.push(root);while (!qt.empty()) {const TreeNode* ptr{ qt.front() };qt.pop();for (const auto c : ptr->child) {qt.push(c);}cout << ptr->val << " ";}
}

  非常简单,我们每次取出队首元素之后,把这个结点的所有子结点全都入队,然后把它打印出来即可。

(5).树的线性表示

#1.双亲表示法

  双亲表示法是比较简单的表示,比如我们有 n n n个结点,我们依次给出这 n n n个结点的双亲结点的编号(如果是根节点,双亲结点的编号为-1),然后我们需要还原出这棵树来,怎么做呢?

如果你比较厉害,实际上双亲表示法已经足够你做完树的各种操作了,在这里我们只是简单完成一下从双亲表示法到标准存储的转换,理论上讲,我们可以在 O ( n ) O(n) O(n)的时间复杂度内完成这个过程,想法很简单:我们首先新建这 n n n个结点并把它们存储起来,然后去遍历存储了双亲编号的数组,然后把它们添加到对应结点的child数组里面去即可,所以我们可以很快写出下面的代码:

TreeNode* buildTree(const vector<int>& parents)
{size_t n{ parents.size() };vector<TreeNode*> nodes;TreeNode* root{ nullptr };for (int i = 0; i < n; i++) {nodes.push_back(new TreeNode{ i, vector<TreeNode*>{} });}for (int i = 0; i < n; i++) {if (parents[i] != -1) {nodes[parents[i]]->child.push_back(nodes[i]);}else root = nodes[i];}return root;
}

  所以就是这样,我们把某个子结点直接链接到对应的双亲结点上去即可。

#2.层号表示法

  我们根据树的前序遍历顺序,依次给出结点的值以及其所在的层号,然后据此建立一棵树,比如(0,A) (1,B) (2,D) (2,E) (1,C)是一棵二叉树,还原的方法其实并不困难,如果层号比上一个结点层号大一个,则说明这个结点是上一个结点的子结点,如果比上一个结点小,则说明回到了已记录结点的某一层的另一棵子树上,所以我们可以写出以下代码:

struct TreeNode
{TreeNode* parent;char c;vector<TreeNode*> child;
};struct LevelTreeNode
{int level;char c;
};void buildTree(vector<TreeNode*>& nodes, const vector<LevelTreeNode*>& lnodes)
{int current_level{ 1 }; // 用current_level记录当前层号nodes[0]->c = lnodes[0]->c; // 前序遍历的第一个结点一定是根TreeNode* last_root{ nodes[0] }; // 记录上一层的根结点for (int i = 1; i < lnodes.size(); i++) {if (lnodes[i]->level != current_level) {  // 仅在不同层的时候需要特殊处理if (lnodes[i]->level > current_level) { // 如果层号变大,说明所有后续结点都是子结点last_root = *(last_root->child.begin() + last_root->child.size() - 1); // 找到上一个根结点的双亲结点最后的子结点(其实就是当前遍历到结点对应的根)current_level = lnodes[i]->level; // 层号变更}else {int dif{ current_level - lnodes[i]->level }; // 如果层号变小,则开始回溯while (dif != 0) { // 回溯到同一层last_root = last_root->parent;dif--;} current_level = lnodes[i]->level;}}// 标准建树操作nodes[i]->parent = last_root;nodes[i]->c = lnodes[i]->c;last_root->child.push_back(nodes[i]);}
}

  nodes是对应所有结点的vector,这里只是用来保存各个结点的信息,而lnodes则是一个包含层号的结点vector,它内部还包含了前序遍历的顺序

  在这里,我们采取了一个辅助手段——保存结点的双亲结点,我们前面已经很轻松地解决了双亲结点建树的问题,所以只要我们能够根据层号和前序遍历顺序还原出双亲的对应关系,我们就可以很轻松地把这个问题解决了。

小结

  这一篇中我们比较详细地介绍了关于树的一些内容,不过树的内容还远不止这些,未来的两篇分别是二叉树树的应用,敬请期待。

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

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

相关文章

winform联合halcon读取图像出现问题

1.在Form1.cs和Form.Designer.cs中添加using HalconDotNet&#xff1b; 2. 3.添加Halcon导入.cs的程序 4.注释掉导出文件的主函数&#xff0c;不然会报错。 .

C#常见的设计模式-行为型模式

前言 行为型模式是面向对象设计中的一类设计模式&#xff0c;它关注对象之间的通信和相互作用&#xff0c;以实现特定的行为或功能。在C#中&#xff0c;有许多常见的行为型模式&#xff0c;下面将对其中10种行为型模式进行介绍&#xff0c;并给出相应的代码示例。 目录 前言1.…

ky10 server sp3 解决/boot/grub2/grub.cfg 找不到

现象 /boot/grub2 目录下不存在grub.cfg 配置文件 解决 执行下面脚本即可 yum install -y grub2 grub2-mkconfig -o /boot/grub2/grub.cfg 执行完成第一条命令 执行完成第二条命令 查看效果 已经生成这个文件了

Java抽象类和接口(2)

&#x1f435;本篇文章继续对接口相关知识进行讲解 一、排序 1.1 给一个对象数组排序&#xff1a; class Student {public String name;public int age;public Student(String name, int age) {this.name name;this.age age;}public String toString() {return "name:…

BetaFlight模块设计之三十七:SBUS

BetaFlight模块设计之三十七&#xff1a;SBUS 1. 源由2. sbus启动&动态任务3. 主要函数3.1 sbus初始化3.2 sbusFrameStatus更新3.3 rxFrameTimeUs3.4 sbusDataReceive接收数据 4. 辅助函数4.1 sbusChannelsDecode 5. 参考资料 1. 源由 接着BetaFlight模块设计之三十六&…

Leetcode—266.回文排列【简单】Plus

2023每日刷题&#xff08;四十&#xff09; Leetcode—266.回文排列 C语言实现代码 char chara[26] {0};int calculate(char *arr) {int nums 0;for(int i 0; i < 26; i) {nums arr[i];}return nums; }bool canPermutePalindrome(char* s) {int len strlen(s);for(in…

零基础在ubuntu上搭建rtmp服务器-srs

搭建服务器 搭建 SRS&#xff08;Simple-RTMP-Server&#xff09;服务器需要一些步骤&#xff0c;以下是一个简单的步骤指南。请注意&#xff0c;SRS 的配置可能会有所不同&#xff0c;具体取决于你的需求和环境。在开始之前&#xff0c;请确保你的 Ubuntu 系统已经连接到互联…

高效记账,轻松管理,批量记账与柱形图分析助你掌控收支明细

你是否曾经因为繁琐的记账过程而感到烦恼&#xff1f;是否曾经因为无法全面掌握个人或企业的收支情况而感到困惑&#xff1f;现在&#xff0c;我们为你带来了一种全新的高效记账方式&#xff0c;让你从此告别繁琐&#xff0c;轻松掌控收支明细。 首先第一步我们要打开晨曦记账…

商城免费搭建之java商城 鸿鹄云商 B2B2C产品概述

【B2B2C平台】&#xff0c;以传统电商行业为基石&#xff0c;鸿鹄云商支持“商家入驻平台自营”多运营模式&#xff0c;积极打造“全新市场&#xff0c;全新 模式”企业级B2B2C电商平台&#xff0c;致力干助力各行/互联网创业腾飞并获取更多的收益。从消费者出发&#xff0c;助…

pytest系列——pytest-xdist插件之多进程运行测试用例|| pytest-parallel插件之多线程运行测试用例

pytest之多进程运行测试用例(pytest-xdist) 前言 平常我们功能测试用例非常多时&#xff0c;比如有1千条用例&#xff0c;假设每个用例执行需要1分钟&#xff0c;如果单个测试人员执行需要1000分钟才能跑完当项目非常紧急时&#xff0c;会需要协调多个测试资源来把任务分成两…

警惕!AI正在“吞食”你的数据

视觉中国供图 □ 科普时报记者 陈 杰 AI大模型的热度&#xff0c;已然开始从产业向日常生活渗透&#xff0c;并引起不小的舆论旋涡。近日&#xff0c;网友指出国内某智能办软件有拿用户数据“投喂”AI之嫌&#xff0c;引发口水的同时&#xff0c;再度把公众对AI的关注转移到数…

使用paddledetection的记录

首先在这里使用的是是paddle--detection2.7的版本。 成功进行训练 目录&#xff1a; 目录 数据集准备 配置文件的修改 使用的是BML的平台工具&#xff1a; !python -m pip install paddlepaddle-gpu2.5 -i https://mirror.baidu.com/pypi/simple --user %cd /home/aistudio…

Rust语言入门教程(七) - 所有权系统

所有权系统是Rust敢于声称自己为一门内存安全语言的底气来源&#xff0c;也是让Rust成为一门与众不同的语言的所在之处。也正是因为这个特别的所有权系统&#xff0c;才使得编译器能够提前暴露代码中的错误&#xff0c;并给出我们必要且精准的错误提示。 所有权系统的三个规则…

Anaconda安装教程(超详细版)

目录 一、Anaconda简介 二、运行环境 三、安装Anaconda 四、手动配置环境变量&#xff08;重点&#xff09; 五、测试Anaconda环境是否配置成功 一、Anaconda简介 Anaconda&#xff0c;一个开源的Python发行版本&#xff0c;可用于管理Python及其相关包&#xff0c;包含了…

慕尼黑电子展采访全程 | Samtec管理层对话电子发烧友:虎家卓越服务

【摘要/前言】 今年的慕尼黑上海电子展上&#xff0c;Samtec大放异彩&#xff0c;特装展台一亮相就获得了大家的广泛关注&#xff0c;展台观众络绎不绝。 作为深耕连接器行业数十年的知名厂商以及Electronica的常客&#xff0c;Samtec毫无疑问地获得了大量媒体朋友的关注和报…

【数据结构】二叉树之链式结构

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《Linux》《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 一、前置说明二、二叉树的遍历2.1 前序遍历2.2 中序遍历2.3 后序遍历2.4 层序遍历 三、…

如何在本地安装部署WinSCP,并实现公网远程本地服务器

可视化文件编辑与SSH传输神器WinSCP如何公网远程本地服务器 文章目录 可视化文件编辑与SSH传输神器WinSCP如何公网远程本地服务器1. 简介2. 软件下载安装&#xff1a;3. SSH链接服务器4. WinSCP使用公网TCP地址链接本地服务器5. WinSCP使用固定公网TCP地址访问服务器 1. 简介 …

送PDF书 | 豆瓣9.2分,超250万Python新手的选择!蟒蛇书入门到实践

在此疾速成长的科技元年&#xff0c;编程就像是许多人通往无限可能世界的门票。而在编程语言的明星阵容中&#xff0c;Python就像是那位独领风 骚的超级巨星&#xff0c; 以其简洁易懂的语法和强大的功能&#xff0c;脱颖而出&#xff0c;成为全球最炙手可热的编程语言之一。 …

Gitea和Jenkins安装

Gitea Gitea&#xff1a;https://dl.gitea.com/gitea/1.21.0/ Jenkins&#xff1a;https://www.jenkins.io/download/ 数据库配置 可以参考官方文档-https://docs.gitea.cn/1.20/installation/database-prep&#xff0c;这里以MySQL作为讲解 MySQL 在数据库实例上&#xf…

智能物流时代:快递物流信息订阅与推送API自动推送物流变更信息

引言 在当今数字化和智能化的时代&#xff0c;物流行业也在迅速演变&#xff0c;通过技术创新提高效率、提升服务质量。其中&#xff0c;快递物流信息订阅与推送API的自动推送功能成为推动物流领域发展的重要驱动力。本文将深入探讨这一趋势&#xff0c;并分析快递物流信息订阅…