您现在的位置是:首页» 资讯» 数据结构考研排序,数据结构和数据结构算法一样吗

数据结构考研排序,数据结构和数据结构算法一样吗

2024-03-02 14:12:01
本内容由小编为大家分享关于研究生招生简章、考研分数线、考研调剂、考研真题资料等信息。数据结构 考研,数据结构考研真题提到 Redis,我想大家并不陌生,基本上每个项目中都会有它的身影出现。作为一款性能卓越的中间件,其功能强大,在系统中经常扮

本内容由小编为大家分享关于研究生招生简章、考研分数线、考研调剂、考研真题资料等信息。

数据结构 考研,数据结构考研真题

提到 Redis,我想大家并不陌生,基本上每个项目中都会有它的身影出现。作为一款性能卓越的中间件,其功能强大,在系统中经常扮演着十分重要的角色,像缓存、分布式锁和消息队列等,都是我们所熟知的功能。Redis 在我们的项目中频繁出现的原因,主要是它可以提升系统的性能,支撑起系统的高并发。那么 Redis 这么优秀的原因是什么呢?这时我们可能会想到它基于内存的存储介质,多路复用的IO方式,以及主模块的单线程模型等等,但往往忽视了一点,就是 Redis 在底层数据结构上的实现。当提及 Redis 的数据结构,我们一般会想到 STRING、LIST、SET、ZSET、HASH等,没错,这些在我们平时使用 Redis 的过程中,是直接能够感知到的数据结构。不过,这仅仅是从使用者的角度去看,如果从内部实现角度,这些高层的数据结构还依赖于更底层的实现,在 Redis 对象系统的实现源码中,就可以找到底层数据结构的编码,如下所示。注:若没作说明,本文源码参考 Redis 6.0.0版本。编码常量常量值编码对应的底层数据结构OBJ_ENCODING_RAW 0简单动态字符串OBJ_ENCODING_INT 1long类型的整数OBJ_ENCODING_HT 2 字典OBJ_ENCODING_ZIPMAP 3压缩映射OBJ_ENCODING_LINKEDLIST 4双端链表OBJ_ENCODING_ZIPLIST 5压缩列表OBJ_ENCODING_INTSET 6整数集合OBJ_ENCODING_SKIPLIST 7跳跃表和字典OBJ_ENCODING_EMBSTR 8embstr编码的简单动态字符串OBJ_ENCODING_QUICKLIST 9快表OBJ_ENCODING_STREAM 10流下面,我们依次介绍下 Redis 的底层数据结构,来看看它们是如何实现的。简单动态字符串Redis 是采用 C 语言实现的,但 Redis 并没有直接使用 C 语言中传统的字符串表示,而是自己构建了一个名为简单动态字符串(simple dynamic string,SDS)的抽象类型,在 Redis 的不同版本中,其实现也是不同的,我们先来看一下 2.8 版本的实现。struct sdshdr {int len;int free;char buf[];};其中:属性说明len 记录 buf 数组中已使用字节的数量,等于 SDS 所保存字符串的长度free 记录 buf 数组中未使用字节的数量buf 字节数组,用于保存字符串,遵循 C 字符串以空字符结尾的惯例,但保存空字符的 1 字节空间不计算在 len 属性里与C字符串的比较那就有一个问题,Redis 为什么构建了自己的字符串类型,它与 C 字符串的区别在哪,有着什么样的优势呢,下面我们就来看一下。1. 降低获取字符串长度的复杂度C 字符串:获取字符串长度时,需要对字符串进行遍历,时间复杂度为 O(n)。SDS:其实现中通过 len 属性记录了字符串的长度,使得获取字符串长度的时间复杂度降低到了O(1),这就确保了获取字符串长度不会成为 Redis 的性能瓶颈。2. 拼接操作安全性C 字符串:进行拼接操作时,使用者需要先检查是否分配了足够的空间,如果忘记了,就有可能造成缓冲区溢出,导致其它空间的值被修改。SDS:进行字符串修改时,API 会先检查是否有足够的空间,不满足的话,会按照一定的策略进行空间扩展,然后再执行修改操作,这样就杜绝了缓冲区的溢出。3. 减少内存重分配的次数对于字符串,我们经常会进行修改操作,变长或者变短,这会涉及到内存重分配,可能需要执行系统调用,如果修改操作执行得比较频繁,过多的内存重分配操作就会影响到系统的性能。C 字符串:修改N次字符串,需要执行 N 次内存重分配。SDS:通过 free 属性实现了空间预分配和惰性空间释放,将修改 N 次字符串,需要执行内存重分配的次数从 N 次降低为最多 N 次。空间预分配:当要对字符串的长度进行扩展时,如果空间不够,SDS 是会通过一定的策略分配更多的空间,将未使用的空间记录到 free 属性中,这样就避免下次扩展时再进行申请空间的操作。惰性空间释放:当要缩短字符串时,SDS 并不会释放掉其对应的空间,而是记录到 free 属性中,以便于下次扩展空间时使用。4. 二进制安全C 字符串:字符必须符合某种编码,并且必须以空字符结尾,这就导致其他位置不能出现空字符,限制了 C 字符串保存的内容只能是文本数据,不能保存图片、音频等二进制数据。SDS:虽然也以空字符结尾,但它是以 len 属性的值来判断是否结束,所以可以保存任何格式的二进制数据,数据在写入时是什么样,被读取时就是什么样。5. 兼容性SDS 遵循 C 字符串以空字符结尾的惯例,这样就可以重用一部分 C 字符串的库函数。版本优化上面我们分析了 Redis 为何设计了自己的字符串类型,但你以为这样就可以了吗,在 3.2 及之后的版本中,Redis 又对 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[];};其中:属性说明len 记录 buf 数组中已使用字节的数量,等于 SDS 所保存字符串的长度alloc 字节数组的总长度,alloc – len 就是之前版本中的 freeflags 类型标识,低三位存储类型,高5位预留,在 sdshdr5 中存储已使用长度buf 字节数组,用于保存字符串,遵循 C 字符串以空字符结尾的惯例,但保存空字符的 1 字节空间不计算在 len 和 alloc 属性里

所做优化3.0 及之前的版本,不同的字符串占用的头部都是相同的,对于短字符串来说,很浪费空间,所以 3.2 及之后的版本针对不同长度的字符串进行了类型优化,可以节省更多内存空间。取消了编译时的对齐填充,让编译器以紧凑的形式分配内存,进一步节省了内存空间;而且也可以方便地进行一些操作,比如,要得到该 SDS 的类型,直接向低地址偏移 1 个字节来获取 flags 字段等。使用场景Redis 中字符串对象的底层实现之一就使用了 SDS,像 AOF 模块中的 AOF 缓冲区,以及客户端状态中的输入缓冲区,也是由SDS实现的。前言中列举的 OBJ_ENCODING_EMBSTR、OBJ_ENCODING_INT 也是字符串对象的底层实现,这两种底层结构会在讲解 Redis 的对象系统中讲解。链表链表提供了高效的节点重排能力,以及顺序性的节点访问方式,而且增删节点只需要修改前后节点的指针,非常方便,可以灵活地调整链表的长度。链表是一种非常常见的数据结构,像高级编程语言中都有它的身影,比如 Java 中的 LinkedList,但 C 语言中却没有内置自己的链表实现,所以 Redis 自己实现了一个链表结构。实现typedef struct list {listNode *head; // 表头节点listNode *tail; // 表尾节点void *(*dup)(void *ptr); // 节点值复制函数void (*free)(void *ptr); // 节点值释放函数int (*match)(void *ptr, void *key); // 节点值对比函数unsigned long len; // 链表包含的节点数量} list;typedef struct listNode {struct listNode *prev; // 前置节点struct listNode *next; // 后置节点void *value; // 节点值} listNode;

Redis实现的链表可以总结如下:双端无环链表,以 O(1) 的时间复杂度获取某个节点的前置节点和后置接点;表头的前置节点和表尾的后置节点为 null,对链表的访问以 null 结束。以 O(1) 的时间复杂度获取表头节点和表尾节点。以 O(1) 的时间复杂度获取链表的长度。节点使用 void* 指针保存节点值,具有多态性,可以用于保存各种不同类型的值,可以通过 list 结构的三个属性(dup、free、match)设置类型特定函数。使用场景Redis 中列表对象的底层实现之一就使用了链表,但在 Redis 3.2 之后,被快表取代,除了这个,像 Redis 中的发布与订阅、慢查询、监视器等功能也使用到了链表,以及 Redis 服务器本身的客户端状态信息,以及客户端的输出缓冲区,也都使用到了链表。字典同链表一样,C 语言没有像 Java 等高级编程语言一样内置自己的字典实现,所以 Redis 构建了自己的字典实现。Redis 的字典是一个用于维护 key 和 value 映射关系的数据结构,底层使用哈希表实现,一个哈希表里可以有多个哈希节点,每个哈希节点保存了一个键值对。实现// 字典结构定义typedef struct dict {// 类型特定函数dictType *type;// 私有数据,保存了需要传给类型特定函数的可选参数void *privdata;// 哈希表dictht ht[2];// rehash索引 当没有进行rehash是,值为 -1long rehashidx; /* rehashing not in progress if rehashidx == -1 */unsigned long iterators; /* number of iterators currently running */} dict;// 类型特定函数结构定义typedef struct dictType {// 计算哈希值的函数uint64_t (*hashFunction)(const void *key);// 复制键的函数void *(*keyDup)(void *privdata, const void *key);// 复制值的函数void *(*valDup)(void *privdata, const void *obj);// 对比键的函数int (*keyCompare)(void *privdata, const void *key1, const void *key2);// 销毁键的函数void (*keyDestructor)(void *privdata, void *key);// 销毁值的函数void (*valDestructor)(void *privdata, void *obj);} dictType;// 哈希表结构定义typedef struct dictht {// 哈希表数组dictEntry **table;// 哈希表大小unsigned long size;// 哈希表大小掩码,用于计算索引值,总是等于 size – 1unsigned long sizemask;// 哈希表已有节点的数量unsigned long used;} dictht;// 哈希表节点结构定义typedef struct dictEntry {// 键void *key;// 值union {void *val;uint64_t u64;int64_t s64;double d;} v;// 指向下个哈希表节点,形成链表struct dictEntry *next;} dictEntry;

从上面字典的结构定义,可以看出,它类似 Java 中的 HashMap。谈到哈希,总会伴随一些问题的出现,比如,采用的哈希算法,哈希冲突的解决,重哈希等问题,下面我们来看下 Redis 设计的字典是如何解决这些问题的。哈希算法Redis 使用的哈希算法为 MurmurHash 算法;其索引值的计算,依赖于哈希值和 sizemask 属性。当给定一个 KV 值时,其索引值的计算过程如下:首先根据类型特定函数中设置的哈希函数,计算出 key 的哈希值:hash = dict -> type -> hashFunction(key)然后再根据哈希值和 sizemask 属性,计算出索引值:index = hash & dict -> ht[x].sizemask哈希冲突从哈希表节点结构定义中,可以看出,Redis 字典解决哈希冲突的方法为链地址法,因为哈希表节点中没有指向链表表尾的指针,所以采用的是头插法,也就是后插入的节点在前面。rehash随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,这时其负载因子可能会在一个不合理的范围内,太多就会造成哈希冲突的增加,影响查询的效率,太低就会造成空间存储效率的下降,因此当负载因子超过设定的阈值,哈希表就会进行相应的扩展或者收缩。其中,哈希表的负载因子计算方式为:load_factor = ht[0].used / ht[0].size。rehash的条件服务器目前没有活动着的保存 RDB、AOF 重写或者 Redis 加载的模块派生的子进程,比如执行 BGSAVE 或者 BGREWRITEAOF 等命令时,并且负载因子大于等于 1,执行扩展操作。如果存在(1)中所述的进程,比如服务器正在执行 BGSAVE 或者 BGREWRITEAOF 命令,并且负载因子大于等于 5,执行扩展操作。哈希表的负载因子小于 0.1 时,会执行收缩操作。可以看出,当 BGSAVE 或者 BGREWRITEAOF 命令执行时,负载因子的阈值会变大,这是因为这两个命令需要创建当前服务器进程的子进程,而大多数操作都采用写时复制来优化子进程的使用效率,所以这里调整阈值上限,可以避免不必要的内存写入操作,最大限度地节约内存,这一点可以从源码中得知。/* This function is called once a background process of some kind terminates,* as we want to avoid resizing the hash tables when there is a child in order* to play well with copy-on-write (otherwise when a resize happens lots of* memory pages are copied). The goal of this function is to update the ability* for dict.c to resize the hash tables accordingly to the fact we have o not* running childs. */void updateDictResizePolicy(void) {if (!hasActiveChildProcess())dictEnableResize();elsedictDisableResize();}/* Return true if there are no active children processes doing RDB saving,* AOF rewriting, or some side process spawned by a loaded module. */int hasActiveChildProcess() {return server.rdb_child_pid != -1 ||server.aof_child_pid != -1 ||server.module_child_pid != -1;}那 rehash 的扩展和收缩操作的空间是怎么来决定的呢?如果执行的是扩展操作,那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n;如果执行的是收缩操作,那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n。Redis 在执行 rehash 时,也是比较特别的,它并不是一次性、集中式的完成的,而是分多次、渐进式地完成。这样做的原因在于,如果哈希表中只有很少的键值对,那么可以瞬间将 ht[0] 中的所有数据转移到 h[1] 中,但如果数据的量级达到了百万千万或者亿呢,那么一次性的转移对于 Redis 将是个灾难。这时,rehashidx 这个属性就有了用武之地,当执行 rehash 时,它会记录 rehash 的进度,每次对字典执行添加、删除、查找或者更新时,程序除了会执行指定的操作外,还会将 rehashidx 索引上的键值对重新哈希到 ht[1] 中。特别的,在执行 rehash 期间,添加操作会把新的键值对直接放到 h[1] 中。使用场景Redis 中的哈希对象的底层实现之一就是字典,当包含的键值对比较多,或者键值对中的元素都是比较长的字符串时,Redis 就会使用字典作为哈希对象的底层实现。还有一个重要的用途就是,我们通过 Redis 提供的 API 操作数据时,数据存放的数据结构也是字典。

数据结构 考研(数据结构考研参考)

zhaoSheng.net【考研招生网】汇集全国,名牌大学院校考研信息,研招网,考研经验,复习资料,考研调剂,录取分数线,考研真题,专业目录,考研辅导班,考研成绩查询等高校招生信息。

免责声明:本文中引用的各种信息及资料(包括但不限于文字、数据、图表及超链接等)均来源于该信息及资料的相关主体(包括但不限于公司、媒体、协会等机构)的官方网站或公开发表的信息。内容仅供参考使用,不准确地方联系删除处理!

联系电话:135-2467-2021