数据结构—树

news2024/11/25 17:17:24

文章目录

  • 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.coloradmin.cn/o/1255422.html

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

相关文章

[Linux]进程等待

文章目录 3.进程等待3.1什么是进程等待3.2为什么要进程等待3.3如何进行进程等待?1.wait2.waitpid2.1函数的讲解2.2status的理解2.3代码理解 3.4学后而思1.直接用全局变量获取子进程退出码可以吗?如下2.进程具有独立性 退出码是子进程的数据 父进程是如何拿到退出码的3.对内存…

3DCAT为华东师大设计学院打造元宇宙数字虚拟学院

6月11日&#xff0c;华东师范大学设计学院在chi K11美术馆举办了一场别开生面的 2023 年本科毕业设计暨项目实践教学现场演示展。其中&#xff0c;元宇宙数字虚拟学院&#xff08;一期&#xff09;的现场发布会引起了现场震撼&#xff0c;吸引了众多观众的目光和参与。 该元宇宙…

CSS清除浮动的八种方法

我们为什么需要清除浮动&#xff0c;如果我们不清除浮动会发生什么呢&#xff1f; 基础样式&#xff0c;没清除浮动之前代码&#xff1a; 可复制也可以自己手动布局&#xff0c;后可尝试使用下面介绍的方法练习清除浮动 <!DOCTYPE html> <html lang"en">…

Python中的下划线使用教程:单下划线、双下划线和头尾双下划线详解

概要 Python是一种简单、易学、功能强大的编程语言&#xff0c;被广泛应用于各种领域。在Python中&#xff0c;下划线的使用有其特殊的含义和用途。本文将详细介绍Python中的单下划线、双下划线和头尾双下划线的使用教程&#xff0c;帮助读者更好地理解和应用这些特性。 一、单…

如何在Ubuntu系统上安装YApi

简单介绍 YApi是高效、易用、功能强大的api管理平台&#xff0c;旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护API&#xff0c;YApi还为用户提供了优秀的交互体验&#xff0c;开发人员只需利用平台提供的接口数据写入工具以及简单的…

会声会影2024中文旗舰版配置最低要求及会声会影2024旗舰版新增哪些新功能?

会声会影&#xff08;Corel VideoStudio&#xff09;2024旗舰版为加拿大Corel公司发布的一款功能丰富的视频编辑软件。会声会影2024简单易用&#xff0c;具有史无前例的强大功能&#xff0c;拖放式标题、转场、覆叠和滤镜&#xff0c;色彩分级、动态分屏视频和新增强的遮罩创建…

什么是客户自助服务?综合指南献上~

《哈佛商业评论》曾报道过&#xff0c;81%的消费者在找客服之前会自己先去找办法解决。 如今&#xff0c;客户希望得到更快的响应。他们不想排队去等信息。他们想要的只是一个更快、更可靠的自助服务解决方案。作为企业&#xff0c;应该注意到他们的期望。企业需要做的就是通过…

Vite -构建优化 - 分包策略 + 打包压缩

什么是分包策略 分包策略 就是把不会常规更新的文件&#xff0c;单独打包处理。问 &#xff1a;什么是不会常规更新的文件&#xff1f; 答 &#xff1a; 就是基本上不会改的文件&#xff0c;比如我们引入的第三方的依赖包&#xff0c;例如 lodash工具包&#xff0c;这些工具包…

Gee教程2.上下文Context

先来看看Gin框架的简单例子 func main() {engine : gin.Default()engine.GET("/", func(c *gin.Context) {c.String(http.StatusOK, "hello World!")})//监听并启动服务&#xff0c;默认 http://localhost:8080/engine.Run() }//我们自己写的 func main()…

PRD学习

产品经理零基础入门&#xff08;五&#xff09;产品需求文档PRD&#xff08;全16集&#xff09;_哔哩哔哩_bilibili 1. PRD的2种表现形式 ① RP格式 &#xff08;1&#xff09;全局说明 ② 文档格式 2. 交互说明撰写 ① 维度 ② 步骤 ③ 规则 &#xff08;1&#xff09;单位…

通达信视频教程的下载地址

百度网盘 请输入提取码百度网盘为您提供文件的网络备份、同步和分享服务。空间大、速度快、安全稳固&#xff0c;支持教育网加速&#xff0c;支持手机端。注册使用百度网盘即可享受免费存储空间https://pan.baidu.com/s/12yNV62ROERRzmyqm9u22aQ?pwdgmdx

Leetcode 1727. 具有重排的最大子矩阵

题目要求&#xff1a; 给定一个大小为 m x n 的二进制矩阵&#xff0c;并且允许您以任意顺序重新排列矩阵的列。 对列进行最佳重新排序后&#xff0c;返回矩阵中每个元素都为 1 的最大子矩阵的面积。 输入&#xff1a;矩阵 [[0,0,1],[1,1,1],[1,0,1]] 输出&#xff1a;4 说明…

2017年五一杯数学建模B题自媒体时代的消息传播问题解题全过程文档及程序

2017年五一杯数学建模 B题 自媒体时代的消息传播问题 原题再现 电视剧《人民的名义》中人物侯亮平说&#xff1a;“现在是自媒体时代&#xff0c;任何突发性事件几分钟就传播到全世界。”相对于传统媒体&#xff0c;以互联网技术为基础的自媒体以其信息传播的即时性、交往方式…

x大网校登录接口js逆向分析

网站&#xff1a; import base64 # 解码 result base64.b64decode(aHR0cHM6Ly91c2VyLndhbmd4aWFvLmNuL2xvZ2lu.encode(utf-8)) websiteresult.decode(utf-8) # print(result) print(website)思路&#xff1a; 模拟登录&#xff0c;得到token值&#xff0c;才能对内部数据进行…

【YOLOv5入门】目标检测

【大家好&#xff0c;我是爱干饭的猿&#xff0c;本文重点介绍YOLOv5入门-目标检测的任务、性能指标、yolo算法基本思想、yolov5网络架构图。 后续会继续分享其他重要知识点总结&#xff0c;如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff0c;关注一下吧】 上一篇…

利用 LD_PRELOAD 环境变量

文章目录 原理LD_PRELOAD介绍如何上传.so文件 例题 [虎符CTF 2022]ezphp 原理 LD_PRELOAD介绍 LD_PRELOAD是Linux系统的一个环境变量&#xff0c;它可以影响程序的运行时的链接&#xff08;Runtime linker&#xff09;&#xff0c;它允许你定义在程序运行前优先加载的动态链接…

系列十九、Spring实例化bean的方式

一、概述 所谓实例化bean&#xff0c;大白话讲就是Spring如何把这一个个的普通的Java对象创建为Spring bean的。 二、方式 Spring中实例化bean常用的有以下四种&#xff0c;即&#xff1a; ① 构造器方式&#xff1b; ② 静态工厂方式&#xff1b; ③ 实例工厂方式&#xff1b;…

c语言实现10进制转16进制

代码如下&#xff1a; #define _CRT_SECURE_NO_WARNINGS #include <stdio.h>int dectohex(int b, char array[]) {char a[17] { "0123456789ABCDEF" };int c[30] { 0 }, i 0, base 16, j 0;while (b){c[i] b % base;b b / base;}j i;for (i--; i >…

steam/csgo搬砖项目真能月入过万吗?到底真的假的

steam/csgo搬砖第三课之如何出售 steam搬砖核心原理是什么&#xff1f;为什么会有差价产生&#xff1f;buff不是更低价吗&#xff1f;很多小白会有这些疑问&#xff01; steam搬砖指的是通过买卖csgo游戏装备赚钱的。 玩过游戏的应该就很清楚&#xff0c;像绝地求生&#xff…

数据结构——带头循环双向链表(List)

1、带头双向循环链表介绍 在上一篇博客中我们提到了链表有三个特性&#xff0c;可以组合成为8种不同类型的链表。单链表是其中比较重要的一种&#xff0c;那么这次我们选择和带头双向循环链表会会面&#xff0c;这样我们就见识过了所有三种特性的呈现。 带头双向循环链表&#…