redis五大基本数据类型之(源码分析)

news2024/10/6 6:49:41

redis五大数据结构

  • String
  • Hash
  • set
  • List
  • Zset
  • 总结

String

String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M.
当我们在redis客户端输入set hello word 的时候
在这里插入图片描述
其在redis当中的存储如下图所示:
在这里插入图片描述
下面我们来解释一下这个dictEntry、SDS、redisObjectredis当中代表的含义.下面我们解释一下:
首先我们解释一下这个dictEntry,我们都知道redis一个kv结构的nosql数据库。dictEntry就是这个哈希的节点里面包含了key和val。在我们上面的例子当中那么这个key就是这个hello 但是请注意这个val并不是这个world这一个字符串。
在redis当中有5大基本数据类型,redis给这5大基本数据类型抽象了一个数据结构叫做redisObject
而这个SDS指的是这个简单动态字符串,下面我们通过阅读源码来看一下这个SDS简单动态字符串是什么样子的

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

如下图所示:
在这里插入图片描述
为什么Redis需要重新设计一个SDS这样的结构出来了?而不直接使用C语义当中的字符串?
在这里
c语言不想c++,java一样有这个String类只能是靠自己的char[]来实现,字符串在 C 语言中的存储方式,想要获取 「Redis」的长度,需要从头开始遍历,直到遇到 ‘\0’ 为止。所以,Redis 没有直接使用 C 语言传统的字符串标识,而是自己构建了一种名为简单动态字符串SDS的抽象类型,并将 SDS 作为 Redis 的默认字符串。
下面说一下博客对Redis为什么需要设计一个SDS动态字符串的理解:

  • SDS 获取字符串长度的时间复杂度是 O(1),有时候我们需要使用strlen去获取字符串的长度,如果用c语言里面的字符串我们需要变量一遍才能将长度给求出来这样效率很低。
  • Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
  • Redis当中的字符串是二进制安全的不会因为\0而中断,并且Redis不仅可以保存文本数据,还可以保存二进制数据。

下面我们谈一下string底层的三种编码方式。
当我们在redis客户端使用这个set k1 v1 时在底层会发生什么了?首先会调用到这个函数当中来:

void setCommand(client *c) {
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_NO_FLAGS;

    if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {
        return;
    }

    c->argv[2] = tryObjectEncoding(c->argv[2]);//尝试对字符串进行编码决定采用哪种编码
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

我们发现他会尝试对他进行编码下面我们在命令行来验证:

在这里插入图片描述
字符串对象的内部编码(encoding)有 3 种 :

  • int :当存储的字符串是数字并将能用long型存储时采用int
  • embstr:嵌入型字符串当字符串长度小于44个字节时采用embstr
  • raw:当字符串长度大于44个字节时采用raw.

在这里插入图片描述
首先是这个int
当字符串键值的内容可以用一个64位有符号整形来表示时,Redis会将键值转化为long型来进行存储,此时即对应 OBJ_ENCODING_INT 编码类型。内部的内存结构表示如下:
在这里插入图片描述
对于redisObject源码如下所示:

struct redisObject {
    unsigned type:4;
    unsigned encoding:4;
    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
                            * LFU data (least significant 8 bits frequency
                            * and most significant 16 bits access time). */
    int refcount;
    void *ptr;
};

Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set字符串的键值在 0~10000 之间的话,则可以 直接指向共享对象 而不需要再建立新对象,此时键值不占空间!减少内存分配。
在这里插入图片描述
下面我们看一下redis当中的伪代码来看看到底是不是这样的:
在这里插入图片描述
其中这个OBJ_SHARED_INTEGERS这个宏的值是这个10000:
在这里插入图片描述
下面我们看看这个embstr这个嵌入式字符串,为什么我们叫他嵌入式字符串了
在这里插入图片描述
下面我们重点看一下这个createEmbeddedStringObject这个方法
在这里插入图片描述
我们重点关注这个红色标注的箭头这两段代码:

robj *createEmbeddedStringObject(const char *ptr, size_t len) {
    robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);
    struct sdshdr8 *sh = (void*)(o+1);

    o->type = OBJ_STRING;
    o->encoding = OBJ_ENCODING_EMBSTR;
    o->ptr = sh+1;
    o->refcount = 1;
    if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
        o->lru = (LFUGetTimeInMinutes()<<8) | LFU_INIT_VAL;
    } else {
        o->lru = LRU_CLOCK();
    }

    sh->len = len;
    sh->alloc = len;
    sh->flags = SDS_TYPE_8;
    if (ptr == SDS_NOINIT)
        sh->buf[len] = '\0';
    else if (ptr) {
        memcpy(sh->buf,ptr,len);
        sh->buf[len] = '\0';
    } else {
        memset(sh->buf,0,len+1);
    }
    return o;
}

我们发现他开辟空间时,将这个redisObject和sds的空间一起开辟了减少内存碎片,其中这一句代码 o->ptr = sh+1;直接让redisObject当中的ptr指针指向了这个对象。用一张图来看应该是这样的:
在这里插入图片描述
总结一下其优点:

  • embstr编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次
  • 释放 embstr编码的字符串对象同样只需要调用一次内存释放函数;
  • 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。

但是嵌入式字符串也有缺点:

  • 如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的,redis没有为embstr编码的字符串对象编写任何相应的修改程序。当我们对embstr编码的字符串对象执行任何修改命令(例如append)时,程序会先将对象的编码从embstr转换成raw,然后再执行修改命令

在这里插入图片描述

最后看一下这个raw这种编码格式
当字符串的长度大于等于这个44个字节时采用这个raw这中编码格式

#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
    if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
        return createEmbeddedStringObject(ptr,len);
    else
        return createRawStringObject(ptr,len);
}

字符串的键值为长度大于44的超长字符串时,Redis 则会将键值的内部编码方式改为OBJ_ENCODING_RAW格式,这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存与其依赖的redisObject的内存不再连续了
在这里插入图片描述

Hash

Hash 是一个键值对(key - value)集合,其中 value 的形式入:value=[{field1,value1},…{fieldN,valueN}]。Hash 特别适合用于存储对象。
Hash 与 String 对象的区别如下图所示:
在这里插入图片描述
在Redis6.0 时候Hash是的底层物理编码是ziplist和hashtable来实现的。由于博主已经装了最新版本的redis,在这里博主直接使用docker安装redis6.0并使用config get hash*来进行查看
在这里插入图片描述
上面这个图是通过命令去获取这个redis当中的redis.conf的配置信息,下面解释一下这两个是什么意思:

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  • 如果上面的条件不满足那么就会采用这个hash作为Hash结构的底层数据结构。

下面我们将这两个数值修改的小一些来进行验证,使用config set 命令来设置。

在这里插入图片描述
然后我们插入元素:
在这里插入图片描述
然后我们在插入一个比较长的元素:
在这里插入图片描述
我们发现这个物理编码里面变成了这个hashtable.下面我们重点说一下这个ziplist(压缩链表)
ziplist为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组并且是一个经过特殊编码的双向链表,它不存储指向前一个链表节点prev和指向下一个链表节点的指针next而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面

如果用一张图来描述他的话应该是这样的:
在这里插入图片描述
下面我们看看这个entry长啥样:

typedef struct zlentry {
    unsigned int prevrawlensize; /*存储上一个节点长度的数值所需要的字节数*/
    unsigned int prevrawlen;     /* 上一个节点的长度 */
    unsigned int lensize;        /* 当前节点长度的数值所需要的字节数*/
    unsigned int len;            /* 当前节点的长度 */
    unsigned int headersize;     /* 当前节点的头部大小,值 = prevrawlensize + lensize. */
    unsigned char encoding;      /* 编码方式,ZIP_STR_* 或 ZIP_INT_* */
    unsigned char *p;            /* 指向节点内容的指针. */
} zlentry;
  • zlbytes:记录了压缩列表占用的内存字节数,在对压缩列表进行内存重分配,或者计算zlend的位置时使用。它本身占了4个字节。
  • zltail:记录了尾节点(entry)至起始节点(entry)的偏移量。通过这个偏移量,可以快速确定最后一个entry节点的地址。
  • zllen:记录了entry节点的数量。当zllen的值小于65535时,这个值就表示节点的数量。当zllen的值大于65535时,节点的真实数量需要遍历整个压缩列表才能得出。
  • entry:压缩列表中所包含的每个节点。每个节点的长度根据该节点的内容来决定。
  • zlend:特殊值0XFF,标记了压缩列表的末端。表示该压缩列表到此为止。

在这里插入图片描述
在这里需要注意的是:
前节点:(前节点占用的内存字节数)表示前1个zlentry的长度,privious_entry_length有两种取值情况:1字节或5字节。取值1字节时,表示上一个entry的长度小于254字节。虽然1字节的值能表示的数值范围是0到255,但是压缩列表中zlend的取值默认是255,因此,就默认用255表示整个压缩列表的结束,其他表示长度的地方就不能再用255这个值了。所以,当上一个entry长度小于254字节时,prev_len取值为1字节,否则,就取值为5字节。记录长度的好处:占用内存小,1或者5个字节。
总结:
在这里插入图片描述
在这里说明一下这个entry:
虽然定义了这个结构体,但是根本就没有使用zlentry结构来作为压缩列表中用来存储数据节点中的结构,因为,这个结构存小整数或短字符串实在是太浪费空间了。这个结构总共在32位机占用了28个字节(32位机),在64位机占用了32个字节。这不符合压缩列表的设计目的:提高内存的利用率。因此,在redis中,并没有定义结构体来进行操作,而是定义了一些宏,压缩列表的节点真正的结构如下图所示:
在这里插入图片描述
zplist这样设计的好处是什么了?链表在内存中,一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题。如果知道了当前的起始地址,因为entry是连续的,entry后一定是另一个entry,想知道下一个entry的地址,只要将当前的起始地址加上当前entry总长度。如果还想遍历下一个entry,只要继续同样的操作。

既然有了双向循环链表为啥还要有这个ziplist了?

  • 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失。ziplist 是一个特殊的双向链表没有维护双向指针:previous next;而是存储上一个 entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的“时间换空间”
  • 链表在内存中一般是不连续的,遍历相对比较慢而ziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行),但是ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),所以ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。
  • 头节点里有头节点里同时还有一个参数 len,和string类型提到的 SDS 类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是 O(1)

既然有优点那么有没有缺点了?
压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患
第一步:现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,如下图:
在这里插入图片描述
因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值,一切OK,O(∩_∩)O哈哈~
第二步:这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为entry1的前置节点,如下图:
在这里插入图片描述
这下就完蛋了压缩链表需要记录上一个节点的长度,现在头插了一个大于254的节点长度那么prelen就需要用5个字节来存储。
在这里插入图片描述
entry1节点原本的长度在250~253之间,因为刚才的扩展空间,此时entry1节点的长度就大于等于254,因此原本entry2节点保存entry1节点的 prevlen属性也必须从1字节扩展至5字节大小。entry1节点影响entry2节点,entry2节点影响entry3节点…一直持续到结尾。这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」。

在redis7当中ziplist已经被废用次采用这个listpack(紧凑链表)来进行这个存储。同样的天上飞的理念总要有落地的实现。
在这里插入图片描述
注意可能有老铁会说这不是还有ziplist的吗?这是redis为了兼容性将ziplist保留了并没有删除但是实际上底层仍然采用的是listpack.下面我们通过实验来看看到底是不是这样的
在这里插入图片描述
同样的当节点的个数或者key、val的长度不符合条件就会改为hashtable.
和ziplist 列表项类似,listpack 列表项也包含了元数据信息和数据本身。不过,为了避免ziplist引起的连锁更新问题,listpack 中的每个列表项,不再像ziplist列表项那样保存其前一个列表项的长度。而是保存当前节点的长度
在这里插入图片描述

  • 定义该元素的编码类型,会对不同长度的整数和字符串进行编码。
  • 实际存放的数据。
  • encoding + data 的总长度,len 代表当前节点的回朔起始地址长度的偏移量。

不难看出listpack 没有记录前一个节点长度,只记录当前节点的长度,从而避免了压缩列表的连锁更新问题。

set

Redis用intset或hashtable存储set。如果元素都是整数类型,就用intset存储。如果不是整数类型,就用hashtable(数组+链表的存来储结构)key就是元素的值,value为null
同样的我们可以来验证一下:
在这里插入图片描述
下面我们来验证一下:
在这里插入图片描述
下面我们看看我们加入一个不是数字的类型看看:
在这里插入图片描述
这个非常的简单就不再说明:

List

在Redis3.0之前,list采用的底层数据结构是ziplist压缩列表+linkedList双向链表
然后在高版本的Redis中底层数据结构是quicklist(替换了ziplist+linkedList),而quicklist也用到了ziplist结论:quicklist就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表
在这里插入图片描述
在这里插入图片描述
quicklist 实际上是 zipList 和 linkedList 的混合体,它将 linkedList按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
在这里插入图片描述
在redis7当中ziplist被这个listpack所代替只是将quickListNode当中的节点改为了这个listpack而不在使用ziplist,在这里就不在画图了。

Zset

在redis6当中,当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 ),或者有序集合中新添加元素的 member 的长度大于服务器属性server.zset_max_ziplist_value 的值(默认值为 64 )时,redis会使用跳跃表作为有序集合的底层实现,否则会使用ziplist作为有序集合的底层实现。
下面我们来进行这个实验:
在这里插入图片描述
下面我们插入几条数据来进行这个实验
在这里插入图片描述
这是这个redis7当中的配置,在redis6当中使用的是这个ziplist。在这里就不作验证了,老铁可以自行下来研究。
对于这个跳表有兴趣的可以参考一下我的博客在这里将这个链接给出:
跳表博客

总结

在redis6当中这个数据类型的编码主要是
在这里插入图片描述
而在redis7当中的物理编码对应是
在这里插入图片描述
对于数据结构的时间复杂度为:
在这里插入图片描述

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

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

相关文章

远程桌面连接是什么?远程桌面连接使用教程

有时候电脑出现各类网络连接、网址访问出错问题&#xff0c;自己无法解决的情况下&#xff0c;常常会求助其他擅长IT的同事或朋友&#xff0c;要么自己通过社交工具在线沟通、要么抱着电脑找人家解决。然而&#xff0c;通过远程桌面完全可以让朋友同事远程帮自己查看电脑问题&a…

永远加载不满的进度条

前言 各位开发大佬&#xff0c;平时肯定见到过这种进度条吧&#xff0c;一直在加载&#xff0c;但等了好久都是在99% 如下所示&#xff1a;有没有好奇这个玩意儿咋做的呢&#xff1f;细听分说 &#xff08;需要看使用&#xff1a;直接看实践即可&#xff09; fake-progress …

亚马逊云科技Serverless Data:数字经济下的创新动能

Serverless时代已经到来&#xff01;企业的技术架构&#xff0c;总是伴随着不断增长的数据与日趋复杂的业务持续演进。如何通过构建更易用的技术架构来聚焦在业务本身&#xff0c;而不必在底层基础设施的管理上投入过多的精力&#xff0c;是数据驱动型企业需要思考的重要议题。…

实操| 前端新人无敲代码开发APP

作为一种大型的基于GPT-3. 5结构的语言模型&#xff0c;ChatGPT由OpenAI训练&#xff0c;采用深度学习技术&#xff0c;通过大量的文本数据学习&#xff0c;可以生成类似于人类自然语言的文字。ChatGPT是一种非常强大的对话引擎&#xff0c;能进行对话、回答问题和完成任务。Ch…

数据库锁原理

数据库锁原理锁的定义InnoDB中的锁模式共享锁独占锁共享意向锁和独占意向锁LOCK_AUTO_INC自增锁INNODB_AUTOINC_LOCK_MODEInnoDB中的锁类型表锁行锁行子类型LOCK_REC_NOT_GAP精准行锁LOCK_GAP行GAP锁LOCK_ORDINARY行NEXT-KEY锁LOCK_INSERT_INTENTION插入意向锁锁的定义 为了体…

数据分析之Matplotlib 基础入门

目录 第一章 什么是Matplotlib 常见图表及其分类 Matplotlib 第一个绘图程序 第二章 Matplotlib 基础 Matplotlib 图表常用设置 颜色设置 线条样式和标记样式 画布设置 设置坐标轴标题 设置坐标轴刻度 设置坐标轴范围 设置网格线 设置文本标签和标题 添加图例 添…

Image Deconvolution with the Half-quadratic Splitting Method

Image Deconvolution with the Half-quadratic Splitting Method 在处理图像重建或者逆问题的时候&#xff0c;我们经常会看到一种称为 Half-quadratic Splitting&#xff08;HQS&#xff09;的方法&#xff0c;这是在优化领域里非常经典的一种方法&#xff0c;之前也断断续续…

【Cesium 编程第一篇】概述、环境搭建、界面介绍

年前年后一直在面试&#xff0c;发现一个奇怪的现象&#xff1a;很多互联网公司经受住三年的疫情冲击&#xff0c;反而在疫情放开的那一刻撑不住了&#xff0c;很多大厂都在批量的裁员&#xff1a;美国硅谷、北京字节、迪士尼中国等等。在北京的朋友也是年后到现在一直没有找到…

AI是一场革命,我真不是在跟风

AI是场革命&#xff0c;好像现在很多人都开始这么说&#xff0c;那么我说我不是在跟风&#xff0c;为什么&#xff1f;不好意思&#xff0c;又要翻翻旧贴 -> AI是一场革命&#xff0c;不要笑&#xff0c;我是认真的。2016年我就这样讲了&#xff0c;就如我常说的&#xff0c…

【《中国工业经济》论文复刻】“一带一路”倡议与中国企业升级

数据和变量描述 本部分介绍文章研究所使用的数据和关键变量。 数据来源&#xff1a;自主整理 时间范围&#xff1a;2012-2017年 变量说明&#xff1a; 相关变量见下表。 一. 摘要 近年来&#xff0c;中国应该如何实现产业升级受到学界的广泛关注&#xff0c;产业升级归根…

Widows下安装Nginx并设置开机自启

1 下载Nginx 下载地址&#xff1a;http://nginx.org/en/download.html 2 启动Nginx nginx的启动方式有两种&#xff1a;一种是直接点击nginx.exe启动&#xff0c;另一种是通过命令行启动 2.1 直接启动 找到nginx目录&#xff0c;双击nginx.exe 即可启动 2.2 命令行启动…

不卷不成魔,新时代的IT人员更需要卷,不卷不成活

简介 从2022年开始至今&#xff0c;IT界发生了很多巨大的变革带来了许多巨大的变化。 这些变革、这些变化导致了有人欢喜有人悲、有人迷茫有人焦虑。1年半来&#xff0c;迷茫、焦虑、精神内耗了也都差不多了&#xff0c;大家都已经认识到了现实&#xff0c;作为凡人的我们所能…

Moviepy模块之多图拼接为一个动图

文章目录前言项目场景项目素材1.jpg2.jpg3.jpg项目代码1. 引入库2. 读取存储图片的文件夹3. 获取文件夹中所有的.jpg结尾的图片文件名4. 按照文件名排序5. 读取所有图片并拼接成动图6. 保存动图问题描述原因分析解决方案最终效果前言 大家好&#xff0c;我是空空star&#xff0…

《花雕学AI》16:BingGPT桌面端的另外一个惊喜—完美整合了新Bing的AI作画功能

你是否曾经想过&#xff0c;如果你能用语言描述你想要的画面&#xff0c;就能让AI为你生成一幅美丽的图画&#xff0c;那该有多好&#xff1f;你是否曾经想过&#xff0c;如果你能在桌面端直接与新Bing进行智能、流畅、有趣的对话&#xff0c;而不需要打开浏览器或安装插件&…

好看的html登录界面,

界面效果&#xff1a; 代码&#xff1a; <!DOCTYPE html> <html><head><title>Login Page</title><style>body {background-color: #f2f2f2;font-family: Arial, sans-serif;}form {background-color: #fff;border-radius: 5px;box-shado…

【LeetCode: 剑指 Offer II 085. 生成匹配的括号 | 递归 | 回溯】

&#x1f34e;作者简介&#xff1a;硕风和炜&#xff0c;CSDN-Java领域新星创作者&#x1f3c6;&#xff0c;保研|国家奖学金|高中学习JAVA|大学完善JAVA开发技术栈|面试刷题|面经八股文|经验分享|好用的网站工具分享&#x1f48e;&#x1f48e;&#x1f48e; &#x1f34e;座右…

文本翻译免费软件-word免费翻译软件

好用的翻译文件软件应该具备以下几个方面的特点&#xff1a;支持多种文件格式&#xff0c;翻译结果准确可靠&#xff0c;界面操作简便易用&#xff0c;价格实惠&#xff0c;用户体验舒适。以下是几个好用的翻译文件软件&#xff1a; 1.147cgpt翻译软件 翻译软件特点&#xff1…

Diffusion Models 简单代码示例

一、关于Diffusion 模型的简单介绍 首先diffusion模型和VAE、Flow、Gan等模型类似&#xff0c;均属于生成模型&#xff0c;可以和GCN、CNN等其他深度学习网络相结合&#xff0c;完成特定的生成任务&#xff0c;如下图&#xff1a; 基于 GAN 生成模型&#xff0c;基于 VAE 的生成…

初识二叉树

树的概念与结构 树是一种非线性的数据结构&#xff0c;它是由n个有限结点组成一个具有层次关系的集合&#xff0c;把它叫做树是因为它看起来像一颗倒挂的树&#xff0c;也就是说它的根朝上&#xff0c;而叶朝下的。 子树之间不能有交集&#xff0c;否则就不是树型结构 比如下面…

3dmax间隔选择与循环选择你会用吗?真传干货来了!

文章目录一、循环选择1、AltL&#xff08;线性循环&#xff09;2、AltR&#xff08;环向循环&#xff09;3、在线选择上altR和altL的差别二、 间隔选1、选一隔一2、选二隔一3、选二隔二4、隔三选一5、面的间隔选择总结6、线的间隔选文章原出处&#xff1a; https://blog.csdn.n…