数据结构:跳表

数据结构:跳表

    • 跳表
    • 实现
      • 类架构
      • 构造函数
      • 析构函数
      • 查找
      • 插入
      • 删除
    • 总代码


跳表

在传统的链表中,不论单链表还是双链表,查询时都要O(N)的时间复杂度,就算是一个有序链表,由于无法像数组一样定址,无法进行二分查找。

为此,有人提出了增加链表的指针域,使其可以进行二分查找:

在这里插入图片描述

假设对于一个有序链表,每相邻的两个节点,升高一层,增加一个指针,让指针指向下一个升高的节点,也就是下面的链表。

如果要查找21,那么从3出发,从高层往底层查找。先在第二层查找,21 > 7,所以目标节点在7后面,21 > 1221 > 19以此类推。直到查询到25,发现21 < 25,说明要查找的系欸但在[19, 25]之间,下降一层。随后去19的第一层的后一个节点查找,21 == 21找到目标元素。

这个查询过程中,跳过了6 9 17这几个只低层节点,快速的定位目标节点的范围,其实就是一个二分思想。

以此类推,还可以让每4个节点再升高一层:

在这里插入图片描述

甚至每8个节点再升高一层,也就是每到隔 2 n 2^{n} 2n的节点层数增加一层。这样就是一个完全的二分查找了,查询时间复杂度为 O ( l o g N ) O(logN) O(logN)

但是这样的结构有很大的问题,那就是不好删除节点。如果要插入或删除某个节点,此时后续每个节点的位置都变化了,那么节点对应的层数也要进行变化。而层数变化之后,又要维护错综复杂的指针关系,因此这种严格的二分结构没有被推广。

为了避免这种问题,设计者做了一个大胆的处理,不再严格规定每个位置的节点的层数,而是插入节点时随机出一个层数。这样插入与删除都不用管其它节点是多少层,自己的层数完全随机。

在这里插入图片描述

图中红色路径是查找21,绿色路径是在查找26。在查找过程中,会跳过很多节点,因此称为skipList 跳表

刚刚提到,跳表每个节点的层数都是随机的,这会不会太随便了,会不会导致效率很低?

其实跳表的随机是有固定的算法的,如下:

int getRandomLevel()
{
    int level = 1;

    while (rand() < RAND_MAX * p)
        level++;

    return level;
}

跳表的初始层数为1,随后进入一个循环判断,每次循环有p的概率增加一层,如果本次没有增加层,那么终止循环。

比如说第一次循环:

  1. p的概率变成2层,并进入下一轮循环判定
  2. 1 - p的概率不增加层,此时跳出循环,最终层数就是1

如果可以进入第二次循环:

  1. p的概率变成3层,并进入下一轮循环判定
  2. 1 - p的概率不增加层,此时跳出循环,最终层数就是2

以此类推。另外的,跳表还要给出一个最高层数的限制,如果层数到达最高层数,不再进行下一轮判定,这一步在上述代码中没有体现。

由此可得:

  1. 层数为1的概率为 1 − p 1 - p 1p
  2. 层数为2的概率为 p × ( 1 − p ) p\times(1-p) p×(1p),第一轮加层概率为 p,第二轮不加层概率为 (p - 1)
  3. 层数为3的概率为 p 2 × ( 1 − p ) p^{2}\times(1-p) p2×(1p),前两轮加层概率为 p 2 p^{2} p2,第三轮不加层概率为 (p - 1)
  4. 以此类推…
  5. 层数为n的概率为 p n − 1 × ( 1 − p ) p^{n-1}\times(1-p) pn1×(1p),前n-1轮加层概率为 p n − 1 p^{n-1} pn1,第n轮不加层概率为 (p - 1)

计算可得,跳表的平均层数为:

1 1 − p \frac{1}{1-p} 1p1

Redis中,p = 0.25,最大层数不超过32层,此时平均每个节点的层数是4/3层。


实现

类架构

  • 节点类
template <typename T>
struct SkipListNode
{
    T _val;
    vector<SkipListNode*> _nextNodes;

    SkipListNode(T val, int level)
        : _val(val)
        , _nextNodes(level, nullptr)
    {}
};
  1. val:当前节点存储的值
  2. _nextNodes:动态数组,存储指向下一个节点的指针,每一层的下一个节点可能不同
  • 跳表类
template <typename T>
class SkipList
{
    using Node = SkipListNode<T>;

private:
    Node* _head;   // 头节点
    int _maxLevel; // 最大层高
    double _p;     // 概率
    int _curLevel; // 当前最大层高
    int _size;	   // 节点个数
};
  1. _head:指向头节点的指针
  2. _maxLevel:单个节点的最大层数
  3. _p:单轮循环,节点增加层的概率
  4. _curLevel:当前所有节点中的最大层高
  5. _size:跳表的元素个数

构造函数

构造函数如下:

SkipList(int maxLevel = 32, double p = 0.25)
    : _maxLevel(maxLevel)
    , _p(p)
    , _curLevel(0)
    , _size(0)
{
    srand((unsigned int)time(nullptr));
    _head = new Node(T(), _maxLevel);
}

在构造函数中,允许用户自定义概率和最大层数,默认情况maxLevel = 32p = 0.25。在初始化列表中完成成员变量的初始化。

由于后续要计算随机数,在构造函数中srand((unsigned int)time(nullptr))将时间作为随机数种子。

随后构造一个头节点,这个节点作为哨兵不存储实际数据。头节点的层数指定为最大层数。


析构函数

析构函数如下:

~SkipList()
{
    Node* del = _head;
    Node* tmp = nullptr;

    while (del)
    {
        tmp = del->_nextNodes[0];
        delete del;
        del = tmp;
    }
}

析构函数需要从头到尾进行释放节点,此时直接遍历链表,并且只遍历第一层_nextNodes[0]。因为所有节点都有第一层,所以遍历第一层一定可以释放掉所有节点。


查找

基于跳表的两大特性:有序、分层。其实查找过程中,每次行动只有以下两种选择:

  1. 在同层向右移动
  2. 向下一层移动

比如查找21的过程:

在这里插入图片描述

红色路径就是21的查找路径。

  1. 从头节点第4层出发,查看同层的下一个节点621 >= 6说明要找的节点在6之后,向右移动
  2. 6的第4层出发,查看同层的下一个节点NULL,说明当前层没有下一个节点了,只能向下移动
  3. 6的第3层出发,查看同层的下一个节点2521 < 25说明要找的节点在25之前,向下移动
  4. 6的第2层出发,查看同层的下一个节点921 >= 9说明要找的节点在9之后,向右移动
  5. 9的第2层出发,查看同层的下一个节点1721 >= 17说明要找的节点在17之后,向右移动
  6. 17的第2层出发,查看同层的下一个节点2521 < 25说明要找的节点在25之前,向下移动
  7. 17的第1层出发,查看同层的下一个节点1921 >= 19说明要找的节点在19之后,向右移动
  8. 19的第1层出发,查看同层的下一个节点2121 >= 21说明要找的节点在21之后,向右移动
  9. 21的第1层出发,查看同层的下一个节点2521 < 25说明要找的节点在25之前,向下移动
  10. 到达第0层,说明已经查找完毕,当前所在节点21 == 21,成功找到节点

假设要查找20,前7步相同,后两步:

  1. 19的第1层出发,查看同层的下一个节点2120 <=21说明要找的节点在21之前,向下移动
  2. 到达第0层,说明已经查找完毕,当前所在节点19 != 20,说明节点20不存在

可以总结出查找逻辑:

  1. 同层下一个节点为空指针,向下移动一层
  2. 目标值大于等于同层下一个节点的值,移动至同层的下一个节点
  3. 目标值小于同层下一个节点的值,向下移动一层
  4. 当到达第0层,判断节点值是否是目标值

代码:

bool find(T val)
{
    int level = _curLevel - 1; // 获得当前的最高层
    Node* cur = _head; // 记录当前节点

    while (level >= 0) // 当 level 到达第0层,查找完毕
    {
        if (cur->_nextNodes[level] != nullptr && val >= cur->_nextNodes[level]->_val)
            cur = cur->_nextNodes[level]; // 向同层的下一个节点移动
        else
            level--; // 向下移动一层
    }

    return cur != nullptr && cur->_val == val;
}

首先要注意的是,_nextNodes是一个数组,_nextNodes[0]代表的是第一层,也就是说下标和层数之间有一个错位。所以最开始level = _curLevel - 1要进行减一操作。

while(level >= 0)同理,level = 0时,是在第一层而不是第零层,继续循环。

cur->_nextNodes[level]代表同层的下一个节点,因为每次比较,都要和同层的下一个节点比较。比较之前,先判断下一个节点是否为空,防止访问空指针。

最后跳出循环时,查看cur存储的值是否是目标值,如果是,那么该元素存在,反之则不存在。


插入

如图:

在这里插入图片描述

当插入一个新节点时,该节点的前后指针都要改变,也就是图中固定红色指针。那么在找到插入位置后,要把插入位置每一层的前一个节点记录下来

查找每一层的前一个节点:

vector<Node*> findPrev(T val)
{
    vector<Node*> prevNodes(_maxLevel, _head); // 前一个节点的数组

    int level = _curLevel - 1; // 从当前最高层出发
    Node* cur = _head;

    while (level >= 0)
    {
        if (cur->_nextNodes[level] && val > cur->_nextNodes[level]->_val)
        {
            cur = cur->_nextNodes[level]; // 向右移动
        }
        else
        {
            prevNodes[level] = cur; // 记录该层的前一个节点为cur
            level--; // 向下移动
        }
    }

    return prevNodes;
}

该函数接受一个val,返回val的每一层的前一个节点。 prevNodes(_maxLevel, _head)用于存储返回值,元素初始化为_head,即每个节点默认的前一个节点为_head,如果后续新节点的层数超出了当前最大层数,此时前一个节点为_head

level = _curLevel - 1随后从当前的最高层出发,查找插入位置。

在这里插入图片描述

查找每一层前一个节点时,只要层数下降,说明当前节点就是该层的前一个节点

比如插入15,从head出发,到达节点6的第四层,此时第四层下一个节点为NULL,向下移动,此时15在第四层的前一个节点为6

从节点6的第三层出发,第三层下一个节点为2525 > 15向下移动,此时15在第三层的前一个节点为6

后续同理,在第二层的前一个节点为9,第一层的前一个节点为12

在上述代码中,有一个小细节,那就是val > cur->_nextNodes[level]->_val),在find中,此处是大于等于,而这里改成了大于。因为要找前一个节点,在下一个节点值等于目标值的时候,要向下移动,而不是向后移动。

插入代码:

bool insert(const T& val)
{
    vector<Node*> prevNodes = findPrev(val);
    if (prevNodes[0]->_nextNodes[0] != nullptr
        && prevNodes[0]->_nextNodes[0]->_val == val)
        return false; // 已存在

    int level = getRandomLevel(); // 获取随机层数
    _curLevel = max(_curLevel, level); // 更新最大层数

    Node* node = new Node(val, level); // 创建新节点

    for (int i = 0; i < level; i++)
    {
        node->_nextNodes[i] = prevNodes[i]->_nextNodes[i];
        prevNodes[i]->_nextNodes[i] = node;
    }

    _size++;
    return true;
}

插入之前,先判断目标值是否已经存在,由于prevNodes 存储的是每一层的前一个节点,所以prevNodes[0]->_nextNodes[0]才是要插入的位置,此时如果prevNodes[0]->_nextNodes[0]->_val == val,说明要插入的值原先就存在,直接返回false

随后通过getRandomLevel函数获取一个随机层数。如果随机的层数超过了当前最大层,更新当前的最大层数。

然后通过val和随机层数level创建一个节点。从新节点的最高层往下,node->_nextNodes[i] = prevNodes[i]->_nextNodes[i]将新节点的下一个节点,指向原先前一个节点的下一个节点。prevNodes[i]->_nextNodes[i] = node将前一个节点的下一个节点,设为新节点。这和链表的插入是一样的,只是因为有一个分层的概念,所以这里比较抽象。

其实就是:

node->next = prev->next;
prev->next = node;

这样就很好理解了。

完成前后节点的指针关系维护,插入就完成了,最后_size++表示总节点数增加。


删除

理解了插入,删除也很简单了,如图:

在这里插入图片描述

同理,删除节点,只需要注意维护每一层的前后指针关系即可。

删除代码:

bool erase(const T& val)
{
    vector<Node*> prevNodes = findPrev(val);
    if (prevNodes[0]->_nextNodes[0] == nullptr
        || prevNodes[0]->_nextNodes[0]->_val != val)
        return false; // 不存在

    Node* del = prevNodes[0]->_nextNodes[0]; // 记录被删除的节点

    for (int i = 0; i < del->_nextNodes.size(); i++)
        prevNodes[i]->_nextNodes[i] = del->_nextNodes[i]; // 维护指针关系

	// 更新最大层数

    delete del;
    _size--;
    return true;
}

首先判断被删除的节点是否存在,这个if判断条件和插入是刚好相反的。如果目标值不存在,就不删除,返回false

随后Node* del = prevNodes[0]->_nextNodes[0]记录要删除的节点。这里使用prevNodes[0]是因为不确定要删除的节点是多少层,但是它至少有1层,所以拿第一层的下一个节点一定可以找到要删除的节点。

随后一个for循环维护指针关系,相当于以下代码:

prev->next = del->next;

最后delete del删除节点,最后_size--表示目标节点少一个。

但是这还有一个问题,那就是如果删除节点之后,层高降低了:

在这里插入图片描述

此处6作为唯一一个四层的节点,删除后整个跳表的高度都降低为了3,所以在删除操作的最后,还少了一步检测当前的最大层高。也就是以上代码的// 更新最大层数位置。

更新层高:

if (del->_nextNodes.size() == _curLevel) // 被删除的节点,层高和当前的最大层高一致
{
    for (int i = 0; i < _maxLevel; i++)
    {
        if (_head->_nextNodes[i] == nullptr) // 当前层 _head 下一个节点为空
        {
            _curLevel = i; // 更新层高
            break;
        }
    }
}

更新层高很简单,让_head节点从最底层出发,向上检测,如果某一层_head的下一个节点为nullptr,说明没有更高层了,此时层高就是该层-1

首先通过if判断,被删除节点的层高是否是最大层高,如果被删除节点的层高不是最大层高,那么就算被删除了也不会影响最大层高。

随后进入for循环,从最底层开始向上检测,当第一次检测到_head的下一个节点为空,更新层高。_curLevel = i,此处注意是i而不是i - 1,因为_nextNodes[i]层的下一个节点为空,说明_nextNodes[i]的低一层为最高层,而由于下标与层高的错位关系,_nextNodes[i]其实是第i + 1层,它的低一层是i + 1 - 1 = i,所以_curLevel = i


总代码

  • SkipList.hpp
#pragma once
#include <iostream>
#include <vector>
#include <stdlib.h>
#include <time.h>

using namespace std;

template <typename T>
struct SkipListNode
{
    T _val;
    vector<SkipListNode*> _nextNodes;

    SkipListNode(T val, int level)
        : _val(val)
        , _nextNodes(level, nullptr)
    {}
};

template <typename T>
class SkipList
{
    using Node = SkipListNode<T>;
public:

    SkipList(int maxLevel = 32, double p = 0.25)
        : _maxLevel(maxLevel)
        , _p(p)
        , _curLevel(0)
        , _size(0)
    {
        srand((unsigned int)time(nullptr));
        _head = new Node(T(), _maxLevel);
    }

    ~SkipList()
    {
        Node* del = _head;
        Node* tmp = nullptr;

        while (del)
        {
            tmp = del->_nextNodes[0];
            delete del;
            del = tmp;
        }
    }

    bool find(T val)
    {
        int level = _curLevel - 1;
        Node* cur = _head;

        while (level >= 0)
        {
            if (cur->_nextNodes[level] && val >= cur->_nextNodes[level]->_val)
                cur = cur->_nextNodes[level];
            else
                level--;
        }

        return cur != nullptr && cur->_val == val;
    }

    vector<Node*> findPrev(T val)
    {
        vector<Node*> prevNodes(_maxLevel, _head);

        int level = _curLevel - 1;
        Node* cur = _head;

        while (level >= 0)
        {
            if (cur->_nextNodes[level] && val > cur->_nextNodes[level]->_val)
            {
                cur = cur->_nextNodes[level];
            }
            else
            {
                prevNodes[level] = cur;
                level--;
            }
        }

        return prevNodes;
    }

    int getRandomLevel()
    {
        int level = 1;

        while (rand() < RAND_MAX * _p && level < _maxLevel)
            level++;

        return level;
    }

    bool insert(const T& val)
    {
        vector<Node*> prevNodes = findPrev(val);
        if (prevNodes[0]->_nextNodes[0] != nullptr
            && prevNodes[0]->_nextNodes[0]->_val == val)
            return false; // 已存在

        int level = getRandomLevel();
        _curLevel = max(_curLevel, level);

        Node* node = new Node(val, level);

        for (int i = 0; i < level; i++)
        {
            node->_nextNodes[i] = prevNodes[i]->_nextNodes[i];
            prevNodes[i]->_nextNodes[i] = node;
        }

        _size++;
        return true;
    }

    bool erase(const T& val)
    {
        vector<Node*> prevNodes = findPrev(val);
        if (prevNodes[0]->_nextNodes[0] == nullptr
            || prevNodes[0]->_nextNodes[0]->_val != val)
            return false; // 不存在

        Node* del = prevNodes[0]->_nextNodes[0];

        for (int i = 0; i < del->_nextNodes.size(); i++)
            prevNodes[i]->_nextNodes[i] = del->_nextNodes[i];

        if (del->_nextNodes.size() == _curLevel)
        {
            for (int i = 0; i < _maxLevel; i++)
            {
                if (_head->_nextNodes[i] == nullptr)
                {
                    _curLevel = i;
                    break;
                }
            }
        }

        delete del;
        _size--;
        return true;
    }

    void print()
    {
        vector<vector<T>> show(_curLevel, vector<T>(_size, 0));
        Node* cur = _head->_nextNodes[0];
            
        for (int i = 0; i < _size; i++)
        {
            for (int j = 0; j < cur->_nextNodes.size(); j++)
                show[j][i] = cur->_val;

            cur = cur->_nextNodes[0];
        }

        for (int i = _curLevel - 1; i >= 0; i--)
        {
            for (int j = 0; j < _size; j++)
            {
                if (show[i][j] != T())
                    printf("%2d", show[i][j]);
                else
                    printf("  ");
            }
            printf("\n");
        }
    }

private:
    Node* _head;      // 头节点
    int _maxLevel; // 最大层高
    double _p;        // 概率
    int _curLevel; // 当前最大层高
    int _size;
};
  • test.cpp
#include <iostream>
#include "SkipList.hpp"

using namespace std;

int main()
{
    SkipList<int> sk;

    sk.insert(1);
    sk.insert(2);
    sk.insert(3);
    sk.insert(4);
    cout << "测试重复插入:" << endl;
    cout << "第一次:" << sk.insert(5) << endl;
    cout << "第二次:" << sk.insert(5) << endl;
    sk.insert(6);
    sk.insert(7);
    sk.insert(8);
    sk.insert(9);
    cout << "-------------------" << endl;
    cout << "插入1-9:" << endl;
    sk.print();
    cout << "-------------------" << endl;
    cout << "查询1:" << sk.find(1) << endl;
    cout << "查询2:" << sk.find(2) << endl;
    cout << "查询8:" << sk.find(8) << endl;
    cout << "查询9:" << sk.find(9) << endl;
    cout << "查询12:" << sk.find(12) << endl;
    cout << "查询256:" << sk.find(256) << endl;
    cout << "查询-5:" << sk.find(-5) << endl;
    cout << "-------------------" << endl;
    cout << "删除7:" << sk.erase(7) << endl;
    cout << "重复删除7:" << sk.erase(7) << endl;
    sk.print();
    cout << "-------------------" << endl;
    cout << "删除3:" << sk.erase(7) << endl;
    sk.erase(3);
    sk.print();
    cout << "-------------------" << endl;

    return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

学习最新vue20.17.0-事件处理

vue中文官网事件处理 | Vue.js (vuejs.org) 我在官网基础上,添加些代码,方便初学者学习,能够快速理解官网内容,掌握自己所需要的知识,以便节省宝贵的时间。 事件处理 监听事件 我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript…

Anaconda3与PyCharm安装配置

参考文章 Anaconda3与PyCharm安装配置保姆教程 参照上面文章&#xff0c;安装好Anaconda3和PyCharm环境 下面重点记录下环境配置 1&#xff0c;在window系统菜单中选择Anaconda Prompt&#xff0c;而不是Anaconda Powershell Prompt 2, 打开Anaconda Prompt&#xff0c;输…

[网络基础]——什么是IP路由,路由优先级,度量值详解

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;网络通信基础TCP/IP专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年10月14日15点23分 路由器扮演着至关重要的角色&#xff0c;它不仅负责将数据包从源地址转发到目的地址&#xff0c;还…

wsl1升级到wsl2步骤

1、进入到windows功能界面&#xff08;winr&#xff1a;输入cmd&#xff0c;到界面里面输出control&#xff09; 这几个选项勾选上&#xff0c;然后自动重启电脑 2、下载WSL2内核安装包 前往此链接&#xff0c;然后点击下图的下载链接&#xff0c;下载这个更新包后用管理员权…

美畅物联丨剖析 GB/T 28181 与 GB 35114:视频汇聚领域的关键协议

我们在使用畅联云平台进行视频汇聚时&#xff0c;经常会用的GB/T 28181协议&#xff0c;前面我们写了关于GB/T 28181的相关介绍&#xff0c;​ 详见《畅联云平台&#xff5c;关于GB28181你了解多少&#xff1f;》。 ​最近也有朋友向我们咨询GB 35114协议与GB/T 28181有什么不同…

详细分析Redisson分布式锁中的renewExpiration()方法

目录 一、Redisson分布式锁的续期 整体分析 具体步骤和逻辑分析 为什么需要递归调用&#xff1f; 定时任务的生命周期&#xff1f; 一、Redisson分布式锁的续期 Redisson是一个基于Redis的Java分布式锁实现。它允许多个进程或线程之间安全地共享资源。为了实现这一点&…

闯关leetcode——118. Pascal‘s Triangle

大纲 题目地址内容 解题代码地址 题目 地址 https://leetcode.com/problems/pascals-triangle/description/ 内容 Given an integer numRows, return the first numRows of Pascal’s triangle. In Pascal’s triangle, each number is the sum of the two numbers direct…

2.Java--入门程序

一、开发Java程序 步骤&#xff1a; 1.编写代码 其中第一行的HelloWorld叫类名&#xff0c;下面的框架叫main()方法&#xff0c; 类名要和文件名一致&#xff0c; 2.编译代码 用Javac进行编译&#xff0c;将编写的代码保存之后&#xff0c;打开WindowsR输入cmd 用cd文件夹…

SPP与SPPF的区别?Anchor based和Anchor free的区别?

SPP与SPPF的区别&#xff1f; spp是何凯明提出来的&#xff0c;名为空间金子塔&#xff0c;有效避免了对图像区域的裁剪、缩放操作导致的图像失真等问题。 解决了卷积神经网络对图相关重复特征提取的问题&#xff0c;大大提高了产生候选框的速度&#xff0c;且节省了计算成本。…

razor TagHelper 汇总、HtmlHelper 汇总

Tag Helper Tag Helpers 的范围由 addTagHelper 和 removeTagHelper 进行控制&#xff0c;并且 “!” 为退出字符。 addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers // 手动高亮 asp-for 》》 Label <label asp-for"userName"></label>》》生…

九大排序之选择排序和归并排序

1.前言 每一次从待排序的数据元素中选出最小&#xff08;或最大&#xff09;的一个元素&#xff0c;存放在序列的起始位置&#xff0c;直到全部待排序的数据元素排完 。 本章重点&#xff1a; 堆排序和选择排序和归并排序 2.选择排序 基本思路 left和right记录区间的左端和右…

Opencv库的安装与vs项目配置(vs成功配置opencv库)

目录 一、下载安装opencv 1、下载 2、减压安装 3、环境变量配置 二、vs项目配置opencv 1、创建vs项目 2、配置opencv库 3、测试 其中&#xff1a;二、2、配置opencv库是最复杂的&#xff0c;有空需要搞清楚vs中配置不同地方的区别。 以下所有测试是opencv官方4.6.0 w…

差分的数学定义——由泰勒展开式推导

差分是数值分析中的概念&#xff0c;用于近似连续函数的导数。差分可以通过多种方式定义&#xff0c;一阶差分常见的有前向差分、后向差分和中心差分&#xff0c;二阶差分常用的是中心差分法。 一阶差分 1. 前向差分 (Forward Difference) 对于一个函数 f ( x ) f(x) f(x)&…

机器学习数据标准化与归一化:提升模型精度的关键

&#x1f4d8;数据标准化与归一化&#xff1a;提升模型精度的关键 机器学习中的数据处理环节至关重要&#xff0c;其中&#xff0c;数据标准化与归一化是提高模型性能的关键步骤之一。数据的特征尺度往往不一致&#xff0c;直接影响模型的训练效果&#xff0c;因此对数据进行处…

大数据开发基础实训室设备

大数据实验实训一体机 大数据实验教学一体机是一种专为大数据教育设计的软硬件融合产品&#xff0c;其基于华为机架服务器进行了调优设计&#xff0c;从而提供了卓越的性能和稳定性。这一产品将企业级虚拟化管理系统与实验实训教学信息化平台内置于一体&#xff0c;通过软硬件…

【超详细】TCP协议

TCP(Transmission Control Protocol 传输控制协议) 传输层协议有连接可靠传输面向字节流 为什么TCP是传输控制协议呢&#xff1f; 我们以前所看到的write接口&#xff0c;都是把用户级缓冲区的数据拷贝到发送缓冲区中&#xff0c;然后数据就由TCP自主决定了&#xff0c;所以…

番茄工作法计时器:高效时间管理利器

《番茄工作法计时器&#xff1a;高效时间管理利器》 在快节奏的现代生活中&#xff0c;高效管理时间成为每个人的迫切需求。今天&#xff0c;我们为你推荐一款强大的番茄工作法计时器。 这款计时器设计简洁&#xff0c;操作便捷&#xff0c;仅有两个按钮 —— 工作 25 分钟和休…

【未公开0day】RaidenMAILD CVE-2024-32399 路径穿越漏洞【附poc下载】

免责声明&#xff1a;本文仅用于技术学习和讨论。请勿使用本文所提供的内容及相关技术从事非法活动&#xff0c;若利用本文提供的内容或工具造成任何直接或间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果均与文章作者及本账号无关。 fofa语…

【C++】创建TCP服务端

实现了一个基本的 TCP 服务器&#xff0c;可以接受多个客户端连接&#xff0c;然后持续接收客户端发送的信息&#xff0c; 最后将接收到的信息再发送回客户端 。 源码 头文件&#xff08;TCPServerTest.h&#xff09; #include <iostream> #include <winsock2.h&g…

Idea序列图插件-SequenceDiagram Core

&#x1f496;简介 SequenceDiagram Core 是一个 IntelliJ IDEA 插件&#xff0c;它允许开发者直接在 IDE中创建和编辑序列图&#xff08;Sequence Diagrams&#xff09;。序列图是 UML&#xff08;统一建模语言&#xff09;中的一种图表类型&#xff0c;用于描述对象之间如何…