跳表详解和实现|深挖Redis底层数据结构

文章目录

  • 跳表
    • 前言
    • 项目代码仓库
    • 认识跳表
    • 跳表的实现思路
    • 跳表性能分析
    • 对比平衡树(avl和红黑树)和哈希表
    • 使用手册
      • 成员变量
      • 成员函数
        • 构造
        • 析构
        • 迭代器
        • `size`
        • `clear`
        • `empty`
        • `operator=`
        • `find`
        • `insert`
        • `erase`
    • 跳表细节实现
      • 节点定义
      • 跳表结构定义
      • 构造、析构、拷贝构造和赋值重载
      • `size()`
      • 查找接口
      • `insert`接口
      • `erase`接口
      • 迭代器设计

跳表

前言

​ 博主在这边博客,会带着大家实现跳表。我是按照stl容器的格式和风格来进行设计的,实现的是一个类模版,支持迭代器遍历等stl风格的功能,支持跳表的增删查功能。

这里分享我的一些博客专栏,都是干货满满的。

  • 手撕数据结构专栏
  • 高质量博客专栏

项目代码仓库

  • CPlusPlus-review-main/tree/master/skip_list

认识跳表

​ 增加了向前指针的链表叫作跳表。跳表全称叫做跳跃表,简称跳表。跳表是一个随机化的数据结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。

​ 跳表是一个随机化的数据结构,可以被看做二叉树的一个变种,它在性能上和红黑树,AVL树不相上下,但是跳表的原理非常简单,目前在Redis和LeveIDB中都有用到。

​ 它采用随机技术决定链表中哪些节点应增加向前指针以及在该节点中应增加多少个指针。跳表结构的头节点需有足够的指针域,以满足可能构造最大级数的需要,而尾节点不需要指针域。

​ 采用这种随机技术,跳表中的搜索、插入、删除操作的时间均为O(logn),然而,最坏情况下时间复杂性却变成O(n)。相比之下,在一个有序数组或链表中进行插入/删除操作的时间为O(n),最坏情况下为O(n)。

​ 跳表的结构如图所示。

跳表的实现思路

  1. 假如我们每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点,如下图所示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由 于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概 只有原来的一半。
  2. 以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。如下图c,这样搜索效率就进一步提高了。
  3. 跳表正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方 式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似 二分查找,使得查找的时间复杂度可以降低到O(log n)。但是这个结构在插入删除数据的时 候有很大的问题,插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格 的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也 包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。
  4. 跳表的设计为了避免这种问题,做了一个大胆的处理,不再严格要求对应比例关系,而是 插入一个节点的时候随机出一个层数。这样每次插入和删除都不需要考虑其他节点的层数, 这样就好处理多了。

跳表性能分析

上面我们说到,skiplist插入一个节点时随机出一个层数,听起来怎么这么随意,如何保证搜索时的效率呢?

这里首先要细节分析的是这个随机层数是怎么来的。一般跳表会设计一个最大层数maxLevel的限制,其次会设置一个多增加一层的概率p。那么计算这个随机层数的伪代码如下:

randomLevel()
	|v| := 1
	-- random() that returns a random value in [0,1)
	while random() < p and |v| < MaxLevel do
		|v| := |v| + 1
	return |v|

根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。定量的分析如下可以算出定量的式子。

根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。定量的分析如下:

  • 节点层数至少为1。而大于1的节点层数,满足一个概率分布。
  • 节点层数恰好等于1的概率为1-p。
  • 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。
  • 节点层数大于等于3的概率为p2,而节点层数恰好等于3的概率为p2(1-p)。
  • 节点层数大于等于4的概率为p3,而节点层数恰好等于4的概率为p3(1-p)。
  • ……

因此,一个节点的平均层数(也即包含的平均指针数目),计算如下:
1 × ( 1 − p ) + 2 p ( 1 − p ) + 3 p 2 ( 1 − p ) + 4 p 3 ( 1 − p ) + … = ( 1 − p ) ∑ k = 1 + ∞ k p k − 1 = ( 1 − p ) ⋅ 1 ( 1 − p ) 2 = 1 1 − p 1 \times(1-p)+2 p(1-p)+3 p^{2}(1-p)+4 p^{3}(1-p)+\ldots=(1-p) \sum_{k=1}^{+\infty} k p^{k-1}=(1-p) \cdot \frac{1}{(1-p)^{2}}=\frac{1}{1-p} 1×(1p)+2p(1p)+3p2(1p)+4p3(1p)+=(1p)k=1+kpk1=(1p)(1p)21=1p1

在Redis的跳表实现中,这两个参数的取值如下。(Redis是nosql数据库,不是常规关系型的,而是kv类型的数据库,一般用来做缓存)

p = 1/4
maxLevel = 32

跳表的平均时间复杂度为O(logN),这个推导的过程较为复杂,需要有一定的数学功底。

对比平衡树(avl和红黑树)和哈希表

跳表相比平衡搜索树(AVL树和红黑树)对比,都可以做到遍历数据有序,时间复杂度也差不多。

跳表的优势是:

  1. 跳表实现简单,容易控制。平衡树增删查改遍历都更复杂。
  2. 跳表的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子/颜色等消耗。跳表中p=1/2时,每个节点所包含的平均指针数目为2;跳表中p=1/4时,每个节点所包 含的平均指针数目为1.33。

跳表相比哈希表而言,就没有那么大的优势了。相比而言

  1. 哈希表冲突不严重的时候,哈希表平均时间复杂度是O(1),比跳表快。
  2. 哈希表空间消耗略多一点。
  3. 但跳表遍历数据有序
  4. 跳表空间消耗略小一点,哈希表存在链接指针和表空间消耗。
  5. 哈希表扩容有性能损耗
  6. 哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力。

Redis为什么用skiplist而不用平衡树?

在前面我们对于skiplist和平衡树、哈希表的比较中,其实已经不难看出Redis里使用skiplist而不用平衡树的原因了。现在我们看看,对于这个问题,Redis的作者 @antirez 是怎么说的:

There are a few reasons: (cited from https://news.ycombinator.com/item?id=1171423)

  1. They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees.
  2. A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees.
  3. They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.

使用手册


template <class key_type, class value_type> class skip_list;

跳表中的元素不允许重复。


成员变量

成员变量名成员变量定义note
key_type第一个模版参数key-value数据中的key
value_type第二个模版参数key-value数据中的value
iterator一个单向迭代器
const_iteratorconst单向迭代器

成员函数

构造
skip_list();
template <class InputIterator>
skip_list(InputIterator first, InputIterator last);
skip_list(const std::initializer_list<std::pair<key_type, value_type>> &init_list);
skip_list(const skip_list<key_type, value_type> &lst)

三种构造方式:

  • 无参数构造
  • 迭代器区间构造
  • 初始化列表构造
  • 拷贝构造
析构
~skip_list();

析构时释放所有节点。

迭代器
iterator begin(); // 返回第一个元素的迭代器
iterator end();		// 返回最后一个元素的下一个位置的迭代器
const_iterator begin() const // 返回第一个元素的const迭代器
const_iterator end() const	 // 返回最后一个元素的下一个位置的const迭代器
size
size_t size() const;

返回跳表元素个数。

clear
void clear();

清除跳表中所有元素。

empty
bool empty() const;

判断跳表是否为空,如果为空,返回true,否则返回false

operator=
skip_list<key_type, value_type> &operator=(const skip_list<key_type, value_type> &lst);

赋值重载,把另一个跳表的值复制到当前跳表中。

find
std::pair<value_type, bool> find(key_type target);

在跳表中查找键为target的元素,查找的平均效率为O(logn)

返回找到的键为target的元素对应的值和表示查找是否成功的bool值所构成的pair

insert
bool insert(std::pair<key_type, value_type> kv);

插入一个kv结构,返回值表示是否插入成功。

erase
bool erase(key_type key);

删除键为key的节点,返回值表示是否删除成功。

跳表细节实现

这一部分是我对跳表的实现细节。

节点定义

template <class key_type, class value_type>
struct __skip_list_node
{
public:
    std::pair<key_type, value_type> __kv;
    std::vector<__skip_list_node *> __next_vector;

public:
    __skip_list_node(const std::pair<key_type, value_type> &kv, int level)
        : __kv(kv), __next_vector(level, nullptr) {}
};

两个字段,一个就是数据,是kv结构,用pair来存,第二个就是指针的数组,用vector来存放。

这个节点具体有几层,通过上层封装来决定。

跳表结构定义

template <class key_type, class value_type>
class skip_list
{
private:
    typedef __skip_list_node<key_type, value_type> Node;
    Node *__head;
    size_t __max_level = 32; // 就按照redis的给吧,给个32,概率给个0.25
    double __p = 0.25;
    size_t __size = 0;
public:
  	// ...
}

__max_level__p分别代表跳表的最大层数和增加一层的概率,这里的设计我是按照redis来做的。

__size用来控制跳表元素的个数,在inserterase中去设置就行了。

然后要设置头节点__head

构造、析构、拷贝构造和赋值重载

构造提供了三种形式:

  • 无参的default构造
  • 迭代器区间构造
  • 初始化列表构造
skip_list() { __empty_initialize(); }
template <class InputIterator>
skip_list(InputIterator first, InputIterator last)
{
    __empty_initialize();
    __iterator_initialize(first, last);
}
skip_list(const std::initializer_list<std::pair<key_type, value_type>> &init_list)
{
    __empty_initialize();
    __iterator_initialize(init_list.begin(), init_list.end());
}

这三个函数涉及到__empty_initialize()__iterator_initialize()这两个函数,作用分别为,空初始化(初始化头节点)和用迭代器区间的初始化。

实现如下所示,实现很简单。

void __empty_initialize() { __head = new Node({-1, -1}, 1); }
template <class iterator_type>
void __iterator_initialize(iterator_type first, iterator_type last)
{
    while (first != last)
        insert(*first++);
}

析构函数如下所示,要调用clear()接口,清除所有的节点,最后再释放头节点即可。

~skip_list()
{
    clear();
    delete __head;
}

clear()的作用是释放头节点。

void clear()
{
    Node *cur = __head->__next_vector[0]; // 指向第一个节点
    while (cur)
    {
        Node *node_to_free = cur;
        cur = cur->__next_vector[0];
        delete node_to_free;
    }
    // 这几步记得弄一下
    __size = 0;
    __head->__next_vector.resize(1);
    __head->__next_vector[0] = nullptr;
}

遍历跳表的时候只需要遍历最下面一层即可,最下面一层上遍历才能保证遍历到所有的节点。

拷贝构造和赋值重载都是复用前面的__empty_initialize()__iterator_initialize()就行了。

skip_list(const skip_list<key_type, value_type> &lst)
{
    __empty_initialize();
    __iterator_initialize(lst.begin(), lst.end());
}
skip_list<key_type, value_type> &operator=(const skip_list<key_type, value_type> &lst)
{
    clear();
    __iterator_initialize(lst.begin(), lst.end());
    return *this;
}

size()

size_t size() const { return __size; }

查找接口

查找接口就是两个原则:

  • 从左上角开始查找,如果本层的next存在,而且target还比这个next的值还大,继续向右走
  • 如果本层的next不存在,或者target比较小,那么要向下跳了
std::pair<value_type, bool> find(key_type target)
{
    Node *cur = __head;
    int level = __head->__next_vector.size() - 1; // 这个就是最高层数
    /* 这个level不要用 size_t 因为有可能减到 -1 去 */
    while (level >= 0)
    {
        // 下一个位置存在 && target比下一个位置要大,继续跳,向右走
        if (cur->__next_vector[level] && cur->__next_vector[level]->__kv.first < target)
            cur = cur->__next_vector[level]; // 向右走
        // 下一个位置不存在 || target比下一个位置要小,不跳了,向下走
        else if (cur->__next_vector[level] == nullptr || cur->__next_vector[level]->__kv.first > target)
            --level; // 向下走
        else
            return {cur->__next_vector[level]->__kv.second, true};
    }
    return {value_type(), false};
}

insert接口

insert接口的关键,就是找到prev_vector指针向量,这个非常重要,我封装到了__find_prev_vector接口里面。

这个接口:寻找待插入数据的前驱指针,这个这个向量里面的指针不一定是同一个Node里面的指针,有可能是不同Node里面的,如图所示。

如果找到prev_vector指针数组后,还有三个步骤:

  • 确认插入的值在跳表中还没有
  • 生成随机层数
  • 更改链接关系
bool insert(std::pair<key_type, value_type> kv)
{
    __size++; // 计数器++
    /*
        关键是:要找到插入位置的每一层的前面的一系列指针
        但是注意,这一系列指针不一定在同一个vector里面
        因为层数是不知道的,所以这个prev_vector的高度一定先初始化为最高(当前最高,不是只maxLevel)
    */
    std::vector<Node *> prev_vector = __find_prev_vector(kv.first);
    if (prev_vector[0]->__next_vector[0] && prev_vector[0]->__next_vector[0]->__kv.first == kv.first)
        return false; // 说明已经有这个key了
    int n = __random_level();
    Node *newnode = new Node(kv, n);
    // 如果 n 比 head 高,head层数不够怎么办
    if (n > __head->__next_vector.size())
    {
        __head->__next_vector.resize(n, nullptr); // 如果n比head高才升高
        prev_vector.resize(n, __head);
    }
    // 连接前后节点
    for (size_t i = 0; i < n; i++)
    {
        newnode->__next_vector[i] = prev_vector[i]->__next_vector[i];
        prev_vector[i]->__next_vector[i] = newnode;
    }
    return true;
}

__find_prev_vector如下所示。

std::vector<Node *> __find_prev_vector(key_type key)
{
    Node *cur = __head;
    int level = __head->__next_vector.size() - 1;
    std::vector<Node *> prev_vector(level + 1, __head);
    while (level >= 0)
    {
        if (cur->__next_vector[level] && cur->__next_vector[level]->__kv.first < key)
            cur = cur->__next_vector[level];
        else if (cur->__next_vector[level] == nullptr || cur->__next_vector[level]->__kv.first >= key)
        {
            // 不仅要向下走,还要更新prev_vector
            prev_vector[level] = cur;
            --level;
        }
    }
    return prev_vector;
}

erase接口

同样也要调用__find_prev_vector来找到prev_vector

找到之后,检查元素在不在,然后更新链接关系就可以了

bool erase(key_type key)
{
    // 同样也是找到每一层的前一个
    // 删掉之后链接起来就可以了,不用考虑别人
    std::vector<Node *> prev_vector = __find_prev_vector(key);
    // 看最下面的这一层就行了,如果最下面的prev_vector指向空,说明找到最右边也没找到
    // 如果最下面这一层的下一个的val不是对应的值,也不对了,说明找不到了
    // 而且最下面这一层的下一个的val一定是大于num的,如果找不到的话
    if (prev_vector[0]->__next_vector[0] == nullptr || prev_vector[0]->__next_vector[0]->__kv.first != key)
        return false;
    Node *del = prev_vector[0]->__next_vector[0]; // 最下面这一层的下一个
    for (size_t i = 0; i < del->__next_vector.size(); i++)
        prev_vector[i]->__next_vector[i] = del->__next_vector[i];
    // 这样就可以了
    delete del;
    // 检查一下是否删除的是最高的节点
    /* 如果头节点的最高层指向的是空,那么这一层就可以删掉了 */
    while (__head->__next_vector.size() > 1 && __head->__next_vector[__head->__next_vector.size() - 1] == nullptr)
        __head->__next_vector.pop_back();
    --__size;
    return true;
}

迭代器设计

迭代器设计是按照stl的风格来进行设计的,思路也是比较简单的。

template <class key_type, class value_type, class pair_type> /*有可能是const的也有可能不是const的*/
struct __skip_list_iterator
{
public:
    typedef __skip_list_node<key_type, value_type> Node;
    typedef __skip_list_iterator<key_type, value_type, pair_type> self;
    Node *__node;
    __skip_list_iterator(Node *node)
        : __node(node) {}
    // key肯定是不能改的,一定要const
    // value可以改, 假设是int, 那么这个value_type可能是int, 也可能是const int
public:
    bool operator!=(const self &it) const { return __node != it.__node; }
    bool operator==(const self &it) const { return __node == it.__node; }

public:
    pair_type &operator*() { return __node->__kv; }
    pair_type *operator->() { return &(operator*()); }
    // 只有单向迭代器
    self operator++()
    {
        // 注意,这个是前置++
        // ++ 返回的还是一个迭代器
        __node = __node->__next_vector[0];
        return *this;
    }
    self operator++(int)
    {
        self tmp(*this);
        __node = __node->__next_vector[0];
        return tmp;
    }
};
template <class key_type, class value_type>
class skip_list
{
public:
    typedef __skip_list_iterator<key_type, value_type, std::pair<key_type, value_type>> iterator; // key要带上const,因为key是不能改的!
    typedef __skip_list_iterator<key_type, value_type, const std::pair<key_type, value_type>> const_iterator;
    iterator begin() { return iterator(__head->__next_vector[0]); }
    iterator end() { return iterator(nullptr); }
    const_iterator begin() const { return const_iterator(__head->__next_vector[0]); }
    const_iterator end() const { return const_iterator(nullptr); }
public:
  	// ...
};

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

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

相关文章

hummingbird,一个便于将模型部署到边缘设备的Python库!

前言 随着人工智能和机器学习的快速发展&#xff0c;将训练好的模型部署到生产环境中成为了一个重要的任务。而边缘计算设备&#xff0c;如智能手机、嵌入式系统和物联网设备&#xff0c;也需要能够运行机器学习模型以进行实时推理。Python Hummingbird 是一个强大的工具&…

Linux Terminator工具: 保存窗口布局 执行默认启动指令

How do I get Terminator to start up with my custom layout? - Ask Ubuntu

新春快乐(烟花、春联)【附源码】

新春快乐 一&#xff1a; C语言 -- 烟花二&#xff1a;Python -- 春联三&#xff1a;Python -- 烟花四&#xff1a;HTML -- 烟花 一&#xff1a; C语言 – 烟花 运行效果&#xff1a; #include <graphics.h> #include <math.h> #include <time.h> #include…

JSP原理简述

JSP动态网页技术&#xff0c;可以定义html&#xff0c;css&#xff0c;js等静态内容&#xff0c;还可以定义java代码等动态内容。 注意导入坐标时&#xff0c;JSP的scope标签是provided&#xff0c;和servlet一样&#xff0c;否则会报错。 JSP本质上就是一个Servlet&#xff0c…

C++11:移动构造函数【写法+调用时机】【C++返回vector为什么不报错】

文章目录 what is 移动构造函数&#xff1f;移动构造函数的实现的例子when 移动构造函数&#xff1f;在C98之前&#xff0c;没有移动构造函数&#xff0c;是怎么做的呢&#xff1f;后记 what is 移动构造函数&#xff1f; 构造函数string(string&& str)类似于复制构造…

单片机项目调试中的技巧和常见问题解决

单片机是嵌入式系统中的重要组成部分&#xff0c;在各种电子设备中发挥着重要的作用。在单片机项目开发过程中&#xff0c;调试是至关重要的一环&#xff0c;同时也会遇到一些常见问题。本文将介绍一些单片机项目调试的技巧以及常见问题的解决方法&#xff0c;希望能够对单片机…

leetcode——滑动窗口题目汇总

本章总结一下滑动窗口的解题思路&#xff1a; 在字符串中使用双指针 left 和 right 围成的一个左闭右开的区域作为一个窗口。不断将 right 向右滑动&#xff0c;直到窗口中的字符串符合条件。此时将 left 向右滑动&#xff0c;直到窗口中的字符串不符合条件&#xff0c;期间需…

H12-821_73

73.某台路由器Router LSA如图所示&#xff0c;下列说法中错误的是&#xff1f; A.本路由器的Router ID为10.0.12.1 B.本路由器为DR C.本路由器已建立邻接关系 D.本路由器支持外部路由引入 答案&#xff1a;B 注释&#xff1a; LSA中的链路信息Link ID&#xff0c;Data&#xf…

MFC实现遍历系统进程

今天我们来枚举系统中的进程和结束系统中进程。 认识几个API 1&#xff09;CreateToolhelp32Snapshot 用于创建系统快照 HANDLE WINAPI CreateToolhelp32Snapshot( __in DWORD dwFlags, //指定快照中包含的系统内容__in DWORD th32P…

C++初阶:适合新手的手撕vector(模拟实现vector)

上次讲了常用的接口&#xff1a;C初阶&#xff1a;容器&#xff08;Containers&#xff09;vector常用接口详解 今天就来进行模拟实现啦 文章目录 1.基本结构与文件规划2.空参构造函数&#xff08;constructor)4.基本函数&#xff08;size(),capacity(),resize(),reserve())4.增…

基于JavaWeb的网上订餐项目

点击以下链接获取源码&#xff1a; https://download.csdn.net/download/qq_64505944/88825723?spm1001.2014.3001.5503 Java项目-16 浏览商品&#xff0c;会员登录&#xff0c;添加购物车&#xff0c;进行配送等功能 文件代码功能介绍 1.Src下的java文件存放的我们后端的…

Python算法题集_两两交换链表中的节点

Python算法题集_两两交换链表中的节点 题24&#xff1a;两两交换链表中的节点1. 示例说明2. 题目解析- 题意分解- 优化思路- 测量工具 3. 代码展开1) 标准求解【四节点法】2) 改进版一【列表操作】3) 改进版二【三指针法】4) 改进版三【递归大法】 4. 最优算法 本文为Python算法…

Python 小白的 Leetcode Daily Challenge 刷题计划 - 20240209(除夕)

368. Largest Divisible Subset 难度&#xff1a;Medium 动态规划 方案还原 Yesterdays Daily Challenge can be reduced to the problem of shortest path in an unweighted graph while todays daily challenge can be reduced to the problem of longest path in an unwe…

ubuntu20.04 安装mysql(8.x)

安装mysql命令 sudo apt-get install mysql-server安装完毕后&#xff0c;立即初始化密码 sudo mysql -u root # 初次进入终端无需密码ALTER USER rootlocalhost IDENTIFIED WITH caching_sha2_password BY yourpasswd; # 设置本地root密码设置mysql远程登录 设置远程登录账…

【java苍穹外卖项目实战一】苍穹外卖项目介绍

文章目录 1、项目介绍1、项目概述2、 产品原型3、技术选型 1、项目介绍 在开发苍穹外卖这个项目之前&#xff0c;我们需要全方位的来介绍一下当前我们学习的这个项目。接下来&#xff0c;我们将从项目简介、产品原型、技术选型三个方面来介绍苍穹外卖这个项目。 1、项目概述 …

4核8G服务器配置性能怎么样?12M带宽配置服务器能干什么?

腾讯云轻量4核8G12M轻量应用服务器支持多少人同时在线&#xff1f;通用型-4核8G-180G-2000G&#xff0c;2000GB月流量&#xff0c;系统盘为180GB SSD盘&#xff0c;12M公网带宽&#xff0c;下载速度峰值为1536KB/s&#xff0c;即1.5M/秒&#xff0c;假设网站内页平均大小为60KB…

STM32 定时器

目录 TIM 定时器定时中断 定时器外部时钟 PWM驱动LED呼吸灯&#xff08;OC&#xff09; PWM控制舵机 PWMA驱动直流电机 输入捕获模式测频率&#xff08;IC&#xff09; 输入捕获模式测占空比 编码器接口测速(编码器接口) TIM 通用定时器 高级定时器 定时器定时中断 Ti…

CentOS7集群配置免密登录

准备工作 提前开启三台虚拟机hadoop102、hadoop103,hadoop104,关于三台虚拟机的安装可以参考&#xff1a;https://mp.csdn.net/mp_blog/creation/editor/136010108 配置免密登录 一、分别修改三台机器的hosts,配置主机映射关系 vim /etc/hosts 文件中输入以下内容&#xf…

【C语言期末】商品管理系统

本文资源&#xff1a;https://download.csdn.net/download/weixin_47040861/88820155 1.题目要求 商品管理系统 商品信息包括&#xff1a;包括编号、类别、名称、价格、折扣比例、生产时间 、存货数量等要求&#xff1a;1、信息首先保存在文件中&#xff0c;然后打开文件进行…

AcWing 1240 完全二叉树的权值(双指针)

[题目概述] 给定一棵包含 N 个节点的完全二叉树&#xff0c;树上每个节点都有一个权值&#xff0c;按从上到下、从左到右的顺序依次是 A 1 , A 2 , ⋅ ⋅ ⋅ A N A_1,A_2,⋅⋅⋅A_N A1​,A2​,⋅⋅⋅AN​&#xff0c;如下图所示&#xff1a; 现在小明要把相同深度的节点的权值…
最新文章