深入理解跳表及其在Redis中的应用

news2024/9/24 6:53:02

前言

跳表可以达到和红黑树一样的时间复杂度 O(logN),且实现简单,Redis 中的有序集合对象的底层数据结构就使用了跳表。其作者威廉·普评价:跳跃链表是在很多应用中有可能替代平衡树的一种数据结构。本篇文章将对跳表的实现及在Redis中的应用进行学习。**

一. 跳表的基础概念

跳表,即跳跃链表(Skip List),是基于并联的链表数据结构,操作效率可以达到O(logN),对并发友好,跳表的示意图如下所示。

跳表的特点,可以概括如下。

•跳表是多层(level)链表结构;

•跳表中的每一层都是一个有序链表,并且按照元素升序(默认)排列;

•跳表中的元素会在哪一层出现是随机决定的,但是只要元素出现在了第 k 层,那么 k 层以下的链表也会出现这个元素;

•跳表的底层的链表包含所有元素;

•跳表头节点和尾节点不存储元素,且头节点和尾节点的层数就是跳表的最大层数;

•跳表中的节点包含两个指针,一个指针指向同层链表的后一节点,一个指针指向下层链表的同元素节点。

以上图中的跳表为例,如果要查找元素 71,那么查找流程如下图所示。

从顶层链表的头节点开始查找,查找到元素71的节点时,一共遍历了4个节点,但是如果按照传统链表的方式(即从跳表的底层链表的头节点开始向后查找),那么就需要遍历7个节点,所以跳表以空间换时间,缩短了操作跳表所需要花费的时间。跳跃列表的算法有同平衡树一样的渐进的预期时间边界,并且更简单、更快速和使用更少的空间。这种数据结构是由William Pugh(音译为威廉·普)发明的,最早出现于他在1990年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。 谷歌上找到一篇作者关于跳表的论文,感兴趣强烈建议下载阅读:

https://epaperpress.com/sortsearch/download/skiplist.pdf

跳表在动态查找过程中使用了一种非严格的平衡机制来让插入和删除都更加便利和快捷,这种非严格平衡是基于概率的,而不是平衡树的严格平衡。说到非严格平衡,首先想到的是红黑树RbTree,它同样采用非严格平衡来避免像AVL那样调整树的结构,这里就不展开讲红黑树了,看来跳表也是类似的路子,但是是基于概率实现的。

二. 跳表的节点

已知跳表中的节点,需要有指向当前层链表后一节点的指针,和指向下层链表的同元素节点的指针,所以跳表中的节点,定义如下。

publicclassSkiplistNode{

    public int data;
    public SkiplistNode next;
    public SkiplistNode down;
    public int level;

    public SkiplistNode(int data, int level) {
        this.data = data;
        this.level = level;
    }

上述是跳表中的节点的最简单的定义方式,存储的元素 data 为整数,节点之间进行比较时直接比较元素 data 的大小。

三. 跳表的初始化

跳表初始化时,将每一层链表的头尾节点创建出来并使用集合将头尾节点进行存储,头尾节点的层数随机指定,且头尾节点的层数就代表当前跳表的层数。初始化后,跳表结构如下所示。

跳表初始化的相关代码如下所示。

public LinkedList<SkiplistNode> headNodes;
public LinkedList<SkiplistNode> tailNodes;

publicint curLevel;

public Random random;

publicSkiplist() {
    random = newRandom();

    //headNodes用于存储每一层的头节点
    headNodes = newLinkedList<>();
    //tailNodes用于存储每一层的尾节点
    tailNodes = newLinkedList<>();

    //初始化跳表时,跳表的层数随机指定
    curLevel = getRandomLevel();
    //指定了跳表的初始的随机层数后,就需要将每一层的头节点和尾节点创建出来并构建好关系SkiplistNodehead=newSkiplistNode(Integer.MIN_VALUE, 0);
    SkiplistNodetail=newSkiplistNode(Integer.MAX_VALUE, 0);
    for (inti=0; i <= curLevel; i++) {
        head.next = tail;
        headNodes.addFirst(head);
        tailNodes.addFirst(tail);

        SkiplistNodeheadNew=newSkiplistNode(Integer.MIN_VALUE, head.level + 1);
        SkiplistNodetailNew=newSkiplistNode(Integer.MAX_VALUE, tail.level + 1);
        headNew.down = head;
        tailNew.down = tail;

        head = headNew;
        tail = tailNew;
    }
}

四. 跳表的添加方法

每一个元素添加到跳表中时,首先需要随机指定这个元素在跳表中的层数,如果随机指定的层数大于了跳表的层数,则在将元素添加到跳表中之前,还需要扩大跳表的层数,而扩大跳表的层数就是将头尾节点的层数扩大。下面给出需要扩大跳表层数的一次添加的过程。

初始状态时,跳表的层数为 2,如下图所示。

现在要往跳表中添加元素 120,并且随机指定的层数为 3,大于了当前跳表的层数 2,此时需要先扩大跳表的层数,2如 下图所示。

现在要往跳表中添加元素 120,并且随机指定的层数为 3,大于了当前跳表的层数 2,此时需要先扩大跳表的层数,2如 下图所示。

将元素 120 插入到跳表中时,从顶层开始,逐层向下插入,如下图所示。

跳表的添加方法的代码如下所示。

publicvoidadd(int num) {
    //获取本次添加的值的层数intlevel= getRandomLevel();
    //如果本次添加的值的层数大于当前跳表的层数//则需要在添加当前值前先将跳表层数扩充if (level > curLevel) {
        expanLevel(level - curLevel);
    }

    //curNode表示num值在当前层对应的节点SkiplistNodecurNode=newSkiplistNode(num, level);
    //preNode表示curNode在当前层的前一个节点SkiplistNodepreNode= headNodes.get(curLevel - level);
    for (inti=0; i <= level; i++) {
        //从当前层的head节点开始向后遍历,直到找到一个preNode//使得preNode.data < num <= preNode.next.datawhile (preNode.next.data < num) {
            preNode = preNode.next;
        }

        //将curNode插入到preNode和preNode.next中间
        curNode.next = preNode.next;
        preNode.next = curNode;

        //如果当前并不是0层,则继续向下层添加节点if (curNode.level > 0) {
            SkiplistNodedownNode=newSkiplistNode(num, curNode.level - 1);
            //curNode指向下一层的节点
            curNode.down = downNode;
            //curNode向下移动一层
            curNode = downNode;
        }
        //preNode向下移动一层
        preNode = preNode.down;
    }
}

privatevoidexpanLevel(int expanCount) {
    SkiplistNodehead= headNodes.getFirst();
    SkiplistNodetail= tailNodes.getFirst();
    for (inti=0; i < expanCount; i++) {
        SkiplistNodeheadNew=newSkiplistNode(Integer.MIN_VALUE, head.level + 1);
        SkiplistNodetailNew=newSkiplistNode(Integer.MAX_VALUE, tail.level + 1);
        headNew.down = head;
        tailNew.down = tail;

        head = headNew;
        tail = tailNew;

        headNodes.addFirst(head);
        tailNodes.addFirst(tail);
    }
}

五. 跳表的搜索方法

在跳表中搜索一个元素时,需要从顶层开始,逐层向下搜索。搜索时遵循如下规则。

•目标值大于当前节点的后一节点值时,继续在本层链表上向后搜索;

•目标值大于当前节点值,小于当前节点的后一节点值时,向下移动一层,从下层链表的同节点位置向后搜索;

•目标值等于当前节点值,搜索结束。

•下图是一个搜索过程的示意图。

•跳表的搜索的代码如下所示。

publicbooleansearch(int target) {
    //从顶层开始寻找,curNode表示当前遍历到的节点SkiplistNodecurNode= headNodes.getFirst();
    while (curNode != null) {
        if (curNode.next.data == target) {
            //找到了目标值对应的节点,此时返回truereturntrue;
        } elseif (curNode.next.data > target) {
            //curNode的后一节点值大于target//说明目标节点在curNode和curNode.next之间//此时需要向下层寻找
            curNode = curNode.down;
        } else {
            //curNode的后一节点值小于target//说明目标节点在curNode的后一节点的后面//此时在本层继续向后寻找
            curNode = curNode.next;
        }
    }
    returnfalse;
}

六. 跳表的删除方法

当在跳表中需要删除某一个元素时,则需要将这个元素在所有层的节点都删除,具体的删除规则如下所示。

•首先按照跳表的搜索的方式,搜索待删除节点,如果能够搜索到,此时搜索到的待删除节点位于该节点层数的最高层;

•从待删除节点的最高层往下,将每一层的待删除节点都删除掉,删除方式就是让待删除节点的前一节点直接指向待删除节点的后一节点。

•下图是一个删除过程的示意图。

•跳表的删除的代码如下所示。

publicbooleanerase(int num) {
    //删除节点的遍历过程与寻找节点的遍历过程是相同的//不过在删除节点时如果找到目标节点,则需要执行节点删除的操作SkiplistNodecurNode= headNodes.getFirst();
    while (curNode != null) {
        if (curNode.next.data == num) {
            //preDeleteNode表示待删除节点的前一节点SkiplistNodepreDeleteNode= curNode;
            while (true) {
                //删除当前层的待删除节点,就是让待删除节点的前一节点指向待删除节点的后一节点
                preDeleteNode.next = curNode.next.next;
                //当前层删除完后,需要继续删除下一层的待删除节点//这里让preDeleteNode向下移动一层//向下移动一层后,preDeleteNode就不一定是待删除节点的前一节点了
                preDeleteNode = preDeleteNode.down;

                //如果preDeleteNode为null,说明已经将底层的待删除节点删除了//此时就结束删除流程,并返回trueif (preDeleteNode == null) {
                    returntrue;
                }

                //preDeleteNode向下移动一层后,需要继续从当前位置向后遍历//直到找到一个preDeleteNode,使得preDeleteNode.next的值等于目标值//此时preDeleteNode就又变成了待删除节点的前一节点while (preDeleteNode.next.data != num) {
                    preDeleteNode = preDeleteNode.next;
                }
            }
        } elseif (curNode.next.data > num) {
            curNode = curNode.down;
        } else {
            curNode = curNode.next;
        }
    }
    returnfalse;
}

七. 跳表完整代码

跳表完整代码如下所示。

publicclassSkiplist {

    public LinkedList<SkiplistNode> headNodes;
    public LinkedList<SkiplistNode> tailNodes;

    publicint curLevel;

    public Random random;

    publicSkiplist() {
        random = newRandom();

        //headNodes用于存储每一层的头节点
        headNodes = newLinkedList<>();
        //tailNodes用于存储每一层的尾节点
        tailNodes = newLinkedList<>();

        //初始化跳表时,跳表的层数随机指定
        curLevel = getRandomLevel();
        //指定了跳表的初始的随机层数后,就需要将每一层的头节点和尾节点创建出来并构建好关系SkiplistNodehead=newSkiplistNode(Integer.MIN_VALUE, 0);
        SkiplistNodetail=newSkiplistNode(Integer.MAX_VALUE, 0);
        for (inti=0; i <= curLevel; i++) {
            head.next = tail;
            headNodes.addFirst(head);
            tailNodes.addFirst(tail);

            SkiplistNodeheadNew=newSkiplistNode(Integer.MIN_VALUE, head.level + 1);
            SkiplistNodetailNew=newSkiplistNode(Integer.MAX_VALUE, tail.level + 1);
            headNew.down = head;
            tailNew.down = tail;

            head = headNew;
            tail = tailNew;
        }
    }

    publicbooleansearch(int target) {
        //从顶层开始寻找,curNode表示当前遍历到的节点SkiplistNodecurNode= headNodes.getFirst();
        while (curNode != null) {
            if (curNode.next.data == target) {
                //找到了目标值对应的节点,此时返回truereturntrue;
            } elseif (curNode.next.data > target) {
                //curNode的后一节点值大于target//说明目标节点在curNode和curNode.next之间//此时需要向下层寻找
                curNode = curNode.down;
            } else {
                //curNode的后一节点值小于target//说明目标节点在curNode的后一节点的后面//此时在本层继续向后寻找
                curNode = curNode.next;
            }
        }
        returnfalse;
    }

    publicvoidadd(int num) {
        //获取本次添加的值的层数intlevel= getRandomLevel();
        //如果本次添加的值的层数大于当前跳表的层数//则需要在添加当前值前先将跳表层数扩充if (level > curLevel) {
            expanLevel(level - curLevel);
        }

        //curNode表示num值在当前层对应的节点SkiplistNodecurNode=newSkiplistNode(num, level);
        //preNode表示curNode在当前层的前一个节点SkiplistNodepreNode= headNodes.get(curLevel - level);
        for (inti=0; i <= level; i++) {
            //从当前层的head节点开始向后遍历,直到找到一个preNode//使得preNode.data < num <= preNode.next.datawhile (preNode.next.data < num) {
                preNode = preNode.next;
            }

            //将curNode插入到preNode和preNode.next中间
            curNode.next = preNode.next;
            preNode.next = curNode;

            //如果当前并不是0层,则继续向下层添加节点if (curNode.level > 0) {
                SkiplistNodedownNode=newSkiplistNode(num, curNode.level - 1);
                //curNode指向下一层的节点
                curNode.down = downNode;
                //curNode向下移动一层
                curNode = downNode;
            }
            //preNode向下移动一层
            preNode = preNode.down;
        }
    }

    publicbooleanerase(int num) {
        //删除节点的遍历过程与寻找节点的遍历过程是相同的//不过在删除节点时如果找到目标节点,则需要执行节点删除的操作SkiplistNodecurNode= headNodes.getFirst();
        while (curNode != null) {
            if (curNode.next.data == num) {
                //preDeleteNode表示待删除节点的前一节点SkiplistNodepreDeleteNode= curNode;
                while (true) {
                    //删除当前层的待删除节点,就是让待删除节点的前一节点指向待删除节点的后一节点
                    preDeleteNode.next = curNode.next.next;
                    //当前层删除完后,需要继续删除下一层的待删除节点//这里让preDeleteNode向下移动一层//向下移动一层后,preDeleteNode就不一定是待删除节点的前一节点了
                    preDeleteNode = preDeleteNode.down;

                    //如果preDeleteNode为null,说明已经将底层的待删除节点删除了//此时就结束删除流程,并返回trueif (preDeleteNode == null) {
                        returntrue;
                    }

                    //preDeleteNode向下移动一层后,需要继续从当前位置向后遍历//直到找到一个preDeleteNode,使得preDeleteNode.next的值等于目标值//此时preDeleteNode就又变成了待删除节点的前一节点while (preDeleteNode.next.data != num) {
                        preDeleteNode = preDeleteNode.next;
                    }
                }
            } elseif (curNode.next.data > num) {
                curNode = curNode.down;
            } else {
                curNode = curNode.next;
            }
        }
        returnfalse;
    }

    privatevoidexpanLevel(int expanCount) {
        SkiplistNodehead= headNodes.getFirst();
        SkiplistNodetail= tailNodes.getFirst();
        for (inti=0; i < expanCount; i++) {
            SkiplistNodeheadNew=newSkiplistNode(Integer.MIN_VALUE, head.level + 1);
            SkiplistNodetailNew=newSkiplistNode(Integer.MAX_VALUE, tail.level + 1);
            headNew.down = head;
            tailNew.down = tail;

            head = headNew;
            tail = tailNew;

            headNodes.addFirst(head);
            tailNodes.addFirst(tail);
        }
    }

    privateintgetRandomLevel() {
        intlevel=0;
        while (random.nextInt(2) > 1) {
            level++;
        }
        return level;
    }

}

八. 跳表在Redis中的应用

ZSet结构同时包含一个字典和一个跳跃表,跳跃表按score从小到大保存所有集合元素。字典保存着从member到score的映射。这两种结构通过指针共享相同元素的member和score,不会浪费额外内存。

typedef structzset {
    dict*dict;
    zskiplist *zsl;
} zset;

ZSet中的字典和跳表布局:

1.ZSet中跳表的实现细节

随机层数的实现原理:

跳表是一个概率型的数据结构,元素的插入层数是随机指定的。Willam Pugh在论文中描述了它的计算过程如下:指定节点最大层数 MaxLevel,指定概率 p, 默认层数 lvl 为1;

生成一个0~1的随机数r,若r<p,且lvl<MaxLevel ,则lvl ++;

重复第 2 步,直至生成的r >p 为止,此时的 lvl 就是要插入的层数。

论文中生成随机层数的伪码:

在Redis中对跳表的实现基本上也是遵循这个思想的,只不过有微小差异,看下Redis关于跳表层数的随机源码src/z_set.c:

/* Returns a random level for the new skiplist node we are going to create.
 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
 * (both inclusive), with a powerlaw-alike distribution where higher
 * levels are less likely to be returned. */intzslRandomLevel(void){
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

其中两个宏的定义在redis.h中:

#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */#define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */

可以看到while中的:

(random()&0xFFFF) < (ZSKIPLIST_P*0xFFFF)

第一眼看到这个公式,因为涉及位运算有些诧异,需要研究一下Antirez为什么使用位运算来这么写?

最开始的猜测是random()返回的是浮点数[0-1],于是乎在线找了个浮点数转二进制的工具,输入0.5看了下结果:

可以看到0.5的32bit转换16进制结果为0x3f000000,如果与0xFFFF做与运算结果还是0,不符合预期。

实际应用时对于随机层数的实现并不统一,重要的是随机数的生成,在LevelDB中对跳表层数的生成代码是这样的:

template <typename Key, typename Value>
int SkipList<Key, Value>::randomLevel() {

  staticconstunsignedint kBranching = 4;
  int height = 1;
  while (height < kMaxLevel && ((::Next(rnd_) % kBranching) == 0)) {
    height++;
  }
  assert(height > 0);
  assert(height <= kMaxLevel);
  return height;
}

uint32_tNext( uint32_t& seed){
  seed = seed & 0x7fffffffu;

  if (seed == 0 || seed == 2147483647L) { 
    seed = 1;
  }
  staticconstuint32_t M = 2147483647L;
  staticconstuint64_t A = 16807;
  uint64_t product = seed * A;
  seed = static_cast<uint32_t>((product >> 31) + (product & M));
  if (seed > M) {
    seed -= M;
  }
  return seed;
}

可以看到leveldb使用随机数与kBranching取模,如果值为0就增加一层,这样虽然没有使用浮点数,但是也实现了概率平衡。

2.跳表结点的平均层数

我们很容易看出,产生越高的节点层数出现概率越低,无论如何层数总是满足幂次定律越大的数出现的概率越小。

如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。
幂次定律的表现是少数几个事件的发生频率占了整个发生频率的大部分, 而其余的大多数事件只占整个发生频率的一个小部分。

幂次定律应用到跳表的随机层数来说就是大部分的节点层数都是黄色部分,只有少数是绿色部分,并且概率很低。

定量的分析如下:

•节点层数至少为1,大于1的节点层数满足一个概率分布。

•节点层数恰好等于1的概率为p^0(1-p)

•节点层数恰好等于2的概率为p^1(1-p)

•节点层数恰好等于3的概率为p^2(1-p)

•节点层数恰好等于4的概率为p^3(1-p)

依次递推节点层数恰好等于K的概率为p^(k-1)(1-p)

因此如果我们要求节点的平均层数,那么也就转换成了求概率分布的期望问题了:

表中P为概率,V为对应取值,给出了所有取值和概率的可能,因此就可以求这个概率分布的期望了。方括号里面的式子其实就是高一年级学的等比数列,常用技巧错位相减求和,从中可以看到结点层数的期望值与1-p成反比。对于Redis而言,当p=0.25时结点层数的期望是1.33。

总结

跳表的时间复杂度与AVL树和红黑树相同,可以达到O(logN),但是AVL树要维持高度的平衡,红黑树要维持高度的近似平衡,这都会导致插入或者删除节点时的一些时间开销,所以跳表相较于AVL树和红黑树来说,省去了维持高度的平衡的时间开销,但是相应的也付出了更多的空间来存储多个层的节点,所以跳表是用空间换时间的数据结构。以Redis中底层的数据结构zset作为典型应用来展开,进一步看到跳跃链表的实际应用。

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

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

相关文章

蓝桥杯:染色时间

蓝桥杯&#xff1a;染色时间https://www.lanqiao.cn/problems/2386/learning/?contest_id80 问题描述 输入格式 输出格式 样例输入输出 样例输入 样例输出 评测用例规模与约定 解题思路&#xff1a;优先队列 AC代码(Java)&#xff1a; 问题描述 小蓝有一个 n 行 m 列…

华为OD机试题,用 Java 解【任务混部】问题

最近更新的博客 华为OD机试题,用 Java 解【停车场车辆统计】问题华为OD机试题,用 Java 解【字符串变换最小字符串】问题华为OD机试题,用 Java 解【计算最大乘积】问题华为OD机试题,用 Java 解【DNA 序列】问题华为OD机试 - 组成最大数(Java) | 机试题算法思路 【2023】使…

本地docker部署mysql,IDEA直连实战

1、安装mysql镜像 前文中我们安装了docker和redis镜像&#xff0c;并在idea中成功连接&#xff0c;现在安装mysql镜像 docker pull mysql &#xff0c;默认最新版本 ps:可以参考https://www.runoob.com/docker/docker-install-mysql.html 2、启动mysql 打开powershell&…

快速掌握 Flutter 图片开发核心技能

大家好&#xff0c;我是 17。 在 Flutter 中使用图片是最基础能力之一。17 做了精心准备&#xff0c;满满的都是干货&#xff01;本文介绍如何在 Flutter 中使用图片&#xff0c;尽量详细&#xff0c;示例完整&#xff0c;包会&#xff01; 使用网络图片 使用网络图片超级简…

【035】基于java的进销库存管理系统(Vue+Springboot+Mysql)前后端分离项目,附万字课设论文

1.3 系统实现的功能 本次设计任务是要设计一个超市进销存系统&#xff0c;通过这个系统能够满足超市进销存系统的管理及员工的超市进销存管理功能。系统的主要功能包括&#xff1a;首页、个人中心、员工管理、客户管理、供应商管理、承运商管理、仓库信息管理、商品类别管理、 …

【知识图谱】架构-特点-缺点简介

架构物联网、云计算、人工智能等新一代信息技术的迅猛发展&#xff0c;带来了制造业的新一轮突破&#xff0c;推动着制造系统向智能化方向发展&#xff0c;驱动着未来制造模式的创新。其中数据和知识是实现制造业与新一代信息技术融合的基础&#xff0c;是实现智能制造的保障。…

PyQt5(二) python程序打包成.exe文件

目录一、安装 **pyinstaller**二、pyinstaller 打包2.1 pyinstaller 打包机制参考链接前言我们在 pycharm 上写的程序在发送到一台没有安装 python 解释器的机器上是不能运行的&#xff0c;甚至还要安装程序中所使用的第三方包&#xff0c;这样极其不方便。 但是 PC 是可以直接…

【C++】哈希——unordered系列容器|哈希冲突|闭散列|开散列

文章目录一、unordered系列关联式容器二、哈希概念三、哈希冲突四、哈希函数五、解决哈希冲突1.闭散列——开放定址法2.代码实现3.开散列——开链法4.代码实现六、结语一、unordered系列关联式容器 在C98中&#xff0c;STL提供了底层为红黑树结构的一系列关联式容器&#xff0c…

MySQL 横表和竖表相互转换

一 竖表转横表 1. 首先创建竖表 create table student ( id varchar(32) primary key, name varchar (50) not null, subject varchar(50) not null, result int); 2. 插入数据 insert into student (id, name, subject, result) values (0001, 小明, 语文, 83); insert into…

RK系列(RK3568) 收音机tef6686芯片驱动,i2c驱动

SOC:RK3568模块&#xff1a;tef6686系统&#xff1a;Android121.首先目前tef6686只有单片机才有驱动&#xff0c;Linux要集成只需要控制模块内部的i2c地址的顺序从github下载tef6686 Andruino的代码 https://github.com/tehniq3/TEF6686解压进入TEF6686-master\TEF6686_1602i2c…

华为OD机试用Python实现 -【任务混部】(2023-Q1 新题)

华为OD机试题 华为OD机试300题大纲任务混部题目输入输出示例一输入输出说明示例二输入输出说明备注Code代码编写思路华为OD机试300题大纲 参加华为od机试,一定要注意不要完全背诵代码,需要理解之后模仿写出,通过率才会高。 华为 OD 清单查看地址:blog.csdn.net/hihell/ca…

Google Guice 5:AOP

1. AOP 1.1 实际开发中面临的问题 在实际开发中&#xff0c;经常需要打印一个方法的执行时间&#xff0c;以确定是否存在慢操作 最简单的方法&#xff0c;直接修改已有的方法&#xff0c;在finnally语句中打印耗时 Override public Optional<Table> getTable(String da…

中级嵌入式系统设计师2014下半年下午试题与答案解析

中级嵌入式系统设计师2014下半年下午试题与答案解析 试题一 阅读下列说明和图,回答下列问题。 [说明] ATM自动取款机系统是一个由终端机、ATM系统、数据库组成的应用系统,具有提取现金、查询账户余额、修改密码及转账等功能。ATM自动取款机系统用例图如图1所示。

win11开始菜单增强工具:StartAllBack

StartAllBack是一款Windows11开始菜单增强工具&#xff0c;在任务栏上为Windows 11恢复经典样式的Windows 7主题风格开始菜单&#xff0c;主要功能包括&#xff1a;恢复和改进开始菜单样式、个性化任务栏、资源管理器等功能。软件功能恢复和改进任务栏在任务图标上显示标签调整…

【Spring】通过JdbcTemplate实现CRUD操作

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 通过JdbcTemplate实现 增删查改一、添加相关依…

python-下载某短视频平台视频(高清无水印)

python-下载某短视频平台音视频&#xff08;高清无水印&#xff09;前言1、获取视频 url2、发送请求3、数据解析4、本地保存5、完整代码前言 1、Cookie中文名称为小型文本文件&#xff0c;指某些网站为了辨别用户身份而储存在用户本地终端&#xff08;Client Side&#xff09;…

RTMP的工作原理及优缺点

一.什么是RTMP&#xff1f;RTMP&#xff08;Real-Time Messaging Protocol&#xff0c;实时消息传输协议&#xff09;是一种用于低延迟、实时音视频和数据传输的双向互联网通信协议&#xff0c;由Macromedia&#xff08;后被Adobe收购&#xff09;开发。RTMP的工作原理是&#…

Windows 11 网卡MAC地址 | 机器地址 | 网络地址 为 0 | 00-00-00-00-00-00?手动修复……

一位同事反映&#xff0c;他的电脑今天上班开机无法上网&#xff0c;上周末还正常&#xff0c;请我帮忙检修。该同事的电脑安装的是Windows 11&#xff0c;检查网络连接的详细信息&#xff0c;发现IP地址、网关、DNS参数都正常&#xff0c;但物理地址为00-00-00-00-00-00。另外…

力扣每日一题(2023年2月)

2023年2月期每日一题第一天 &#xff08;2325. 解密消息&#xff09;第十六天&#xff08;2341. 数组能形成多少数对&#xff09;第十七天 &#xff08;1139. 最大的以 1 为边界的正方形&#xff09;第十八天 &#xff08;1237. 找出给定方程的正整数解&#xff09;第十九天 &a…

再度盈利,搜狐稳了?

2016年在宣布要用3年时间回归互联网舞台中心之后&#xff0c;很长一段时间内张朝阳积极活跃在各种社交媒体上&#xff0c;完全是一派“积极出山”的姿态。而后畅游从美股退市&#xff0c;搜狗“卖身”腾讯&#xff0c;一系列的收缩动作又似乎是在逐渐远离喧嚣。而在最近三年&am…