当前位置:   article > 正文

快速列表quicklist

快速列表quicklist

目录

为什么使用快速列表quicklist

对比双向链表

对比压缩列表ziplist

quicklist结构

节点结构quicklistNode

quicklist 

管理ziplist信息的结构quicklistEntry

 迭代器结构quicklistIter

 quicklist的API

1.创建快速列表

2.创建快速列表节点

3.头插quicklistPushHead 和尾插quicklistPushTail

4.特定位置插入元素(不是节点)

5.删除元素 

6.查找元素

总结 quicklist的特性


为什么使用快速列表quicklist

​在 Redis 的早期设计中,如果列表类型的对象中元素的长度较小或数量比较少的,就采用压缩列表来存储,反之则使用双向链表

对比双向链表

双向链表便于在链表的两端进行插入和删除操作,在插入节点上复杂度很低,但是它的内存开销比较大,每个节点除了要保存数据之外,还要额外保存两个指针,并且双向链表的各个节点是单独的内存块,地址不连续,容易产生内存碎片

对比压缩列表ziplist

压缩列表存储在一段连续的内存上,所以存储效率高。但是,它每次变更的时间复杂度都比较高,插入和删除操作需要频繁的申请和释放内存,如果压缩列表长度很长,一次 realloc 可能会导致大批量的数据拷贝。

如何保留ziplist的空间高效性,又能不让其更新复杂度过高?

​Redis 在 3.2 版本之后引入了快速列表,列表类型的对象其底层都是由快速列表实现。快速列表是双向链表和压缩列表的混合体,它将双向链表按段切分,每一段都使用压缩列表来紧凑存储,多个压缩列表之间使用双向指针关联起来。

quicklist结构

quicklist是个双端链表,节点结构是quicklistNode,节点的zl字段指向压缩列表。

节点结构quicklistNode

quicklistNode是快速列表的节点。

  1. typedef struct quicklistNode {
  2. struct quicklistNode *prev;
  3. struct quicklistNode *next;
  4. unsigned char *zl; //指向ziplist
  5. unsigned int sz; /* ziplist size in bytes */
  6. unsigned int count : 16; /* count of items in ziplist */
  7. unsigned int encoding : 2; /* RAW==1 or LZF==2 */
  8. unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
  9. unsigned int recompress : 1; /* was this node previous compressed? */
  10. unsigned int attempted_compress : 1; /* node can't compress; too small */
  11. unsigned int extra : 10; /* more bits to steal for future usage */
  12. } quicklistNode;
  • prev:前驱节点指针。
  • next:后驱节点指针。
  • zl:数据指针,如果当前节点的数据没有压缩,则指向一个 ziplist 结构,否则指向一个 quicklistLZF 结构。
  • sz:表示zl指向的数据总大小。注意,若数据被压缩,其表示压缩前的数据长度大小。
  • count:占16bit,表示当前节点的ziplist的entry的个数
  • encoding:占2bit,表示当前节点的数据是否被压缩。1表示没有压缩;2是压缩,用的是LZF算法。
  • container:是一个预留字段,本来设计是用来表明一个quicklist节点下面是直接存数据,还是使用ziplist存数据,或者用其它的结构来存数据(用作数据容器,所以叫container)。目前这个值是一个固定的值2,表示使用 ziplist 作为数据容器
  • recompress:当我们使用类似 lindex 这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置 recompress=1 做一个标记,等有机会再把数据重新压缩。
  • extra:其它扩展字段。目前Redis的实现里也没用上。

quicklist 

快速列表的结构,从其结构可以看出其是一个链表,保存了头尾节点。

  1. #if UINTPTR_MAX == 0xffffffff
  2. /* 32-bit */
  3. # define QL_FILL_BITS 14
  4. # define QL_COMP_BITS 14
  5. # define QL_BM_BITS 4
  6. #elif UINTPTR_MAX == 0xffffffffffffffff
  7. /* 64-bit */
  8. # define QL_FILL_BITS 16
  9. # define QL_COMP_BITS 16
  10. # define QL_BM_BITS 4 /* we can encode more, but we rather limit the user
  11. since they cause performance degradation. */
  12. #else
  13. # error unknown arch bits count
  14. #endif
  15. //上面的表示:QL_FILL_BITS值在32位机器上是14,64位机器上是16
  16. typedef struct quicklist {
  17. quicklistNode *head; //头节点
  18. quicklistNode *tail; //尾结点
  19. unsigned long count; /* total count of all entries in all ziplists */
  20. unsigned long len; /* number of quicklistNodes */
  21. int fill : QL_FILL_BITS; /* fill factor for individual nodes */
  22. unsigned int compress : QL_COMP_BITS; /* depth of end nodes not to compress;0=off */
  23. unsigned int bookmark_count: QL_BM_BITS;
  24. quicklistBookmark bookmarks[];
  25. } quicklist;
  26. //当指定使用lzf压缩算法压缩ziplist的entry节点时,quicklistNode结构的zl成员指向quicklistLZF结构
  27. typedef struct quicklistLZF {
  28. //表示被LZF算法压缩后的ziplist的大小
  29. unsigned int sz; /* LZF size in bytes*/
  30. //保存压缩后的ziplist的数组,柔性数组
  31. char compressed[];
  32. } quicklistLZF;
  33. /* quicklist node encodings */
  34. #define QUICKLIST_NODE_ENCODING_RAW 1
  35. #define QUICKLIST_NODE_ENCODING_LZF 2
  36. /* quicklist compression disable */
  37. #define QUICKLIST_NOCOMPRESS 0
  38. /* quicklist container formats */
  39. #define QUICKLIST_NODE_CONTAINER_NONE 1
  40. #define QUICKLIST_NODE_CONTAINER_ZIPLIST 2
  41. #define quicklistNodeIsCompressed(node) \
  42. ((node)->encoding == QUICKLIST_NODE_ENCODING_LZF)
  • count:所有压缩列表的节点数量之和
  • len:快速类别的节点数量
  • fill:存放 list-max-ziplist-size 参数的值,用于设置每个quicklistnode的压缩列表的所有entry的总长度大小。其值默认是-2,表示每个quicklistNode节点的ziplist所占字节数不能超过8kb。若是任意正数:,表示ziplist结构所最多包含的entry个数,最大为215215。
  • compress:存放 list-compress-depth 参数的值,用于设置压缩深度。快速列表默认的压缩深度为 0,即不压缩。为了支持快速的 push/pop 操作,快速列表的首尾两个节点不压缩,此时压缩深度就是1。若压缩深度为2,表示快速列表的首尾第一个及第二个节点都不压缩。
  • bookmark_count:占 4 bit,bookmarks 数组的长度。 
  • bookmarks:这是一个可选字段,快速列表重新分配内存时使用,不使用时不占用空间。

管理ziplist信息的结构quicklistEntry

和压缩列表一样,entry结构在储存时是一连串的内存块,需要将其每个entry节点的信息读取到管理该信息的结构体中,以便操作。

  1. //管理quicklist中quicklistNode节点中ziplist信息的结构
  2. typedef struct quicklistEntry {
  3. const quicklist *quicklist; //指向所属的quicklist的指针
  4. quicklistNode *node; //指向所属的quicklistNode节点的指针
  5. unsigned char *zi; //指向当前ziplist结构的指针
  6. unsigned char *value; //查找到的元素如果是字符串,则存在value字段
  7. long long longval; //查找到的元素如果是整数,则存在longval字段
  8. unsigned int sz; //保存当前元素的长度
  9. int offset; //保存查找到的元素距离压缩列表头部/尾部隔了多少个节点
  10. } quicklistEntry;

迭代器结构quicklistIter

在Redis的quicklist结构中,实现了自己的迭代器,用于遍历节点。

  1. //quicklist的迭代器结构
  2. typedef struct quicklistIter {
  3. const quicklist *quicklist; //指向所属的quicklist的指针
  4. quicklistNode *current; //指向当前迭代的quicklist节点的指针
  5. unsigned char *zi; //指向当前quicklist节点中迭代的ziplist
  6. long offset; //当前ziplist结构中的偏移量
  7. int direction; //迭代方向
  8. } quicklistIter;

 quicklist的API

1.创建快速列表

创建快速列表,快速列表的成员变量都使用默认值

  1. /* Create a new quicklist.
  2. * Free with quicklistRelease(). */
  3. //创建快速列表,并对各个字段进行初始化
  4. quicklist *quicklistCreate(void) {
  5. struct quicklist *quicklist;
  6. quicklist = zmalloc(sizeof(*quicklist));
  7. quicklist->head = quicklist->tail = NULL;
  8. quicklist->len = 0;
  9. quicklist->count = 0;
  10. quicklist->compress = 0;
  11. quicklist->fill = -2;
  12. quicklist->bookmark_count = 0;
  13. return quicklist;
  14. }

创建列表,传入自己设置的参数

  1. //设置压缩深度
  2. #define COMPRESS_MAX ((1 << QL_COMP_BITS)-1)
  3. void quicklistSetCompressDepth(quicklist *quicklist, int compress) {
  4. if (compress > COMPRESS_MAX) {
  5. compress = COMPRESS_MAX;
  6. } else if (compress < 0) {
  7. compress = 0;
  8. }
  9. quicklist->compress = compress;
  10. }
  11. //设置压缩列表的大小(成员fill),即是每个压缩列表的总entry的总长度大小
  12. #define FILL_MAX ((1 << (QL_FILL_BITS-1))-1)
  13. void quicklistSetFill(quicklist *quicklist, int fill) {
  14. if (fill > FILL_MAX) {
  15. fill = FILL_MAX;
  16. } else if (fill < -5) {
  17. fill = -5;
  18. }
  19. quicklist->fill = fill;
  20. }
  21. //设置快速列表的参数
  22. void quicklistSetOptions(quicklist *quicklist, int fill, int depth) {
  23. quicklistSetFill(quicklist, fill);
  24. quicklistSetCompressDepth(quicklist, depth);
  25. }
  26. //通过一些默认参数创建快速列表,就是调用了前面这些封装好的函数
  27. quicklist *quicklistNew(int fill, int compress) {
  28. quicklist *quicklist = quicklistCreate();
  29. quicklistSetOptions(quicklist, fill, compress);
  30. return quicklist;
  31. }

2.创建快速列表节点

  1. REDIS_STATIC quicklistNode *quicklistCreateNode(void) {
  2. quicklistNode *node;
  3. node = zmalloc(sizeof(*node));
  4. node->zl = NULL;
  5. node->count = 0;
  6. node->sz = 0;
  7. node->next = node->prev = NULL;
  8. node->encoding = QUICKLIST_NODE_ENCODING_RAW; //默认不压缩
  9. node->container = QUICKLIST_NODE_CONTAINER_ZIPLIST; //默认使用压缩列表结构来存数据
  10. node->recompress = 0;
  11. return node;
  12. }

3.头插quicklistPushHead 和尾插quicklistPushTail

  1. /* 头部插入
  2. * 如果在已存在节点插入,返回0
  3. * 如果是在新的头结点插入,返回1 */
  4. int quicklistPushHead(quicklist *quicklist, void *value, size_t sz) {
  5. quicklistNode *orig_head = quicklist->head;
  6. //判断头节点的空间是否足够容纳要添加的元素
  7. if (likely(
  8. _quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz))) {
  9. quicklist->head->zl =
  10. ziplistPush(quicklist->head->zl, value, sz, ZIPLIST_HEAD); // 在头结点对应的ziplist中插入
  11. quicklistNodeUpdateSz(quicklist->head);
  12. } else { // 否则新建一个头结点,然后插入数据
  13. quicklistNode *node = quicklistCreateNode();
  14. node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
  15. quicklistNodeUpdateSz(node);
  16. //新增元素添加进这个新的快速列表节点里
  17. _quicklistInsertNodeBefore(quicklist, quicklist->head, node);
  18. }
  19. quicklist->count++;
  20. quicklist->head->count++;
  21. return (orig_head != quicklist->head);
  22. }
  23. /* 尾部插入。
  24. * 如果在已存在节点插入,返回0
  25. * 如果是在新的头结点插入,返回1 */
  26. int quicklistPushTail(quicklist *quicklist, void *value, size_t sz) {
  27. quicklistNode *orig_tail = quicklist->tail;
  28. if (likely(
  29. _quicklistNodeAllowInsert(quicklist->tail, quicklist->fill, sz))) {
  30. quicklist->tail->zl =
  31. ziplistPush(quicklist->tail->zl, value, sz, ZIPLIST_TAIL);
  32. quicklistNodeUpdateSz(quicklist->tail);
  33. } else {
  34. quicklistNode *node = quicklistCreateNode();
  35. node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_TAIL);
  36. quicklistNodeUpdateSz(node);
  37. _quicklistInsertNodeAfter(quicklist, quicklist->tail, node);
  38. }
  39. quicklist->count++;
  40. quicklist->tail->count++;
  41. return (orig_tail != quicklist->tail);
  42. }

头插和尾插都是先调用了_quicklistNodeAllowInsert来判断能否在当前头/尾节点插入。如果能插入就直接插入到对应的ziplist中,否则就需要新建一个新节点再进行操作。

前面讲解过的quicklist结构的fill字段,其实_quicklistNodeAllowInsert就是根据fill的值来判断是否已经超过最大容量的。

其中使用到函数_quicklistInsertNodeBefore 和 _quicklistInsertNodeBefore,这两个就是在指定位置插入元素

4.特定位置插入元素(不是节点)

注意:我们使用Redis,接触到的快速列表插入的都是插入元素,不是插入快速列表的节点。

插入元素会使用到结构体quicklistEntry

  1. void quicklistInsertBefore(quicklist *quicklist, quicklistEntry *entry,
  2. void *value, const size_t sz) {
  3. _quicklistInsert(quicklist, entry, value, sz, 0);
  4. }
  5. void quicklistInsertAfter(quicklist *quicklist, quicklistEntry *entry,
  6. void *value, const size_t sz) {
  7. _quicklistInsert(quicklist, entry, value, sz, 1);
  8. }
  9. /* 在一个已经存在的entry前面或者后面插入一个新的entry
  10. * 如果after==1表示插入到后面,否则是插入到前面 */
  11. REDIS_STATIC void _quicklistInsert(quicklist *quicklist, quicklistEntry *entry,
  12. void *value, const size_t sz, int after) {
  13. int full = 0, at_tail = 0, at_head = 0, full_next = 0, full_prev = 0;
  14. int fill = quicklist->fill;
  15. //1. 获取插入位置的quicklist的节点,通过entry的node字段
  16. quicklistNode *node = entry->node;
  17. quicklistNode *new_node = NULL;
  18. assert(sz < UINT32_MAX); /* TODO: add support for quicklist nodes that are sds encoded (not zipped) */
  19. //2. 该节点不存在,也只有当快速列表的头或尾节点存在才会进入这个if条件
  20. if (!node) {
  21. /* we have no reference node, so let's create only node in the list */
  22. D("No node given!");
  23. new_node = quicklistCreateNode();
  24. //将添加的元素插入快速列表节点对应的压缩列表的节点头部
  25. new_node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
  26. __quicklistInsertNode(quicklist, NULL, new_node, after);
  27. new_node->count++;
  28. quicklist->count++;
  29. return;
  30. }
  31. //3. 判断node节点对应的压缩列表是否能够容纳得下要添加的元素,即node节点是否已满
  32. if (!_quicklistNodeAllowInsert(node, fill, sz)) {
  33. D("Current node is full with count %d with requested fill %lu",
  34. node->count, fill);
  35. //表示当前节点的数量已满
  36. full = 1;
  37. }
  38. //4. 判断是否需要是在尾部添加
  39. if (after && (entry->offset == node->count)) {
  40. //表示在尾部添加
  41. at_tail = 1;
  42. if (!_quicklistNodeAllowInsert(node->next, fill, sz)) {
  43. D("Next node is full too.");
  44. //表示下一quicklistNode的ziplist的entry已满了
  45. full_next = 1;
  46. }
  47. }
  48. //5.判断是否在头部添加
  49. if (!after && (entry->offset == 0)) {
  50. D("At Head");
  51. at_head = 1;
  52. if (!_quicklistNodeAllowInsert(node->prev, fill, sz)) {
  53. D("Prev node is full too.");
  54. //表示前一节点已满
  55. full_prev = 1;
  56. }
  57. }
  58. //未完待续..........,后面再讲解
  59. }
  • 1.通过通过entry的node字段获取插入位置的quicklist的节点
  • 2.判断该节点是否存在,若不存在,就创建节点,并创建ziplist,插入该元素到ziplist
  • 3.判断node节点对应的压缩列表是否能够容纳得下要添加的元素,即node节点是否已满,用来设置变量full
  • 4.判断是否在该quicklistNode的ziplist的尾部添加,并判断该quicklistNode的下一节点的ziplist是否已满
  • 5.判断是否在该quicklistNode的ziplist的头部添加,并判断该quicklistNode的前驱节点的ziplist是否已满
  1. REDIS_STATIC void _quicklistInsert(quicklist *quicklist, quicklistEntry *entry,
  2. void *value, const size_t sz, int after) {
  3. //..........................................
  4. /* Now determine where and how to insert the new element */
  5. if (!full && after) {
  6. //6. 当前节点的zipList不满,并且是在当前位置的后面插入
  7. D("Not full, inserting after current position.");
  8. quicklistDecompressNodeForUse(node); //当前节点解压缩
  9. //entry->zi是ziplist的一个entry,返回entry->zi的下一个ziplist的entry
  10. unsigned char *next = ziplistNext(node->zl, entry->zi);
  11. if (next == NULL) {
  12. node->zl = ziplistPush(node->zl, value, sz, ZIPLIST_TAIL);
  13. } else {
  14. node->zl = ziplistInsert(node->zl, next, value, sz);
  15. }
  16. node->count++;
  17. quicklistNodeUpdateSz(node);
  18. //添加完元素后再根据node->recompress判断是否对压缩列表进行压缩
  19. quicklistRecompressOnly(quicklist, node);
  20. } else if (!full && !after) {
  21. //7. 当前节点的ziplist不满,在当前entry的前面插入
  22. D("Not full, inserting before current position.");
  23. quicklistDecompressNodeForUse(node);
  24. node->zl = ziplistInsert(node->zl, entry->zi, value, sz);
  25. node->count++;
  26. quicklistNodeUpdateSz(node);
  27. quicklistRecompressOnly(quicklist, node);
  28. } else if (full && at_tail && node->next && !full_next && after) {
  29. /* If we are: at tail, next has free space, and inserting after:
  30. * - insert entry at head of next node. */
  31. //8.
  32. D("Full and tail, but next isn't full; inserting next node head");
  33. new_node = node->next;
  34. quicklistDecompressNodeForUse(new_node);
  35. new_node->zl = ziplistPush(new_node->zl, value, sz, ZIPLIST_HEAD);
  36. new_node->count++;
  37. quicklistNodeUpdateSz(new_node);
  38. quicklistRecompressOnly(quicklist, new_node);
  39. } else if (full && at_head && node->prev && !full_prev && !after) {
  40. /* If we are: at head, previous has free space, and inserting before:
  41. * - insert entry at tail of previous node. */
  42. //9.
  43. D("Full and head, but prev isn't full, inserting prev node tail");
  44. new_node = node->prev;
  45. quicklistDecompressNodeForUse(new_node);
  46. new_node->zl = ziplistPush(new_node->zl, value, sz, ZIPLIST_TAIL);
  47. new_node->count++;
  48. quicklistNodeUpdateSz(new_node);
  49. quicklistRecompressOnly(quicklist, new_node);
  50. } else if (full && ((at_tail && node->next && full_next && after) ||
  51. (at_head && node->prev && full_prev && !after))) {
  52. /* If we are: full, and our prev/next is full, then:
  53. * - create new node and attach to quicklist */
  54. //10.
  55. D("\tprovisioning new node...");
  56. new_node = quicklistCreateNode();
  57. new_node->zl = ziplistPush(ziplistNew(), value, sz, ZIPLIST_HEAD);
  58. new_node->count++;
  59. quicklistNodeUpdateSz(new_node);
  60. __quicklistInsertNode(quicklist, node, new_node, after);
  61. } else if (full) {
  62. /* else, node is full we need to split it. */
  63. /* covers both after and !after cases */
  64. D("\tsplitting node...");
  65. //11
  66. quicklistDecompressNodeForUse(node);
  67. new_node = _quicklistSplitNode(node, entry->offset, after);
  68. new_node->zl = ziplistPush(new_node->zl, value, sz,
  69. after ? ZIPLIST_HEAD : ZIPLIST_TAIL);
  70. new_node->count++;
  71. quicklistNodeUpdateSz(new_node);
  72. __quicklistInsertNode(quicklist, node, new_node, after);
  73. _quicklistMergeNodes(quicklist, node);
  74. }
  75. quicklist->count++;
  76. }

这部分主要是分了几种情况来插入:

  • 6.当前节点的ziplist没满,并在当前entry的后面插入
  • 7.当前节点的ziplist没满,并在当前entry的前面插入
  • 8.当前节点的ziplist已满、要添加在尾部、并且后移节点是存在的、后一节点的ziplist没满,那就添加到后一节点对应的ziplist的第一个entry的前面
  • 9.当前节点的ziplist已满、要添加在头部、并且前一节点存在、前一节点的ziplist没满,就添加到前一节点的ziplist的尾部
  • 10.当前节点的ziplist已满、插入的位置是在头/尾的、并且当前节点的前/后节点的ziplist已满,则需要创建新的quicklistNode来存放要放的元素。
  • 11.当前节点的ziplist已满,但是插入的位置不在两端的,则要从插入位置把当前节点分裂成两个节点

5.删除元素 

 快速列表删除元素有两个函数 quicklistDelEntry 和 quicklistDelRange,分别是删除单个元素删除某个区间的元素。

删除元素使用到了迭代器结构quicklistIter,需要更新迭代器对应的节点等信息。

  1. /* Delete one element represented by 'entry'*/
  2. void quicklistDelEntry(quicklistIter *iter, quicklistEntry *entry) {
  3. quicklistNode *prev = entry->node->prev;
  4. quicklistNode *next = entry->node->next;
  5. //删除元素,返回值 deleted_node 表示当前节点是否要删除。1表示该节点已删除
  6. int deleted_node = quicklistDelIndex((quicklist *)entry->quicklist,
  7. entry->node, &entry->zi);
  8. /* after delete, the zi is now invalid for any future usage. */
  9. iter->zi = NULL;
  10. /* If current node is deleted, we must update iterator node and offset. */
  11. if (deleted_node) {
  12. if (iter->direction == AL_START_HEAD) {
  13. iter->current = next;
  14. iter->offset = 0;
  15. } else if (iter->direction == AL_START_TAIL) {
  16. iter->current = prev;
  17. iter->offset = -1;
  18. }
  19. }
  20. }
  21. REDIS_STATIC int quicklistDelIndex(quicklist *quicklist, quicklistNode *node,
  22. unsigned char **p) {
  23. int gone = 0;
  24. //删除 node 节点对应的压缩列表 p 位置的entry,返回新的zipList
  25. node->zl = ziplistDelete(node->zl, p);
  26. node->count--;
  27. if (node->count == 0) {
  28. gone = 1;
  29. __quicklistDelNode(quicklist, node);//当前节点的ziplist的entry个数为0,就删除该节点
  30. } else {
  31. quicklistNodeUpdateSz(node); //更新该节点的ziplist的总长度大小
  32. }
  33. quicklist->count--;
  34. /* If we deleted the node, the original node is no longer valid */
  35. return gone ? 1 : 0;
  36. }

6.查找元素

Redis中没有提供直接查找元素的API。查找元素是通过遍历查找的,这就需要通过上面所讲的迭代器quicklistIter

  1. quicklistIter *iter = quicklistGetIterator(ql, AL_START_HEAD);
  2. quicklistEntry entry;
  3. int i = 0;
  4. while (quicklistNext(iter, &entry)) { //获取迭代器中的下一个元素
  5. //比较当前的压缩列表节点存储的元素与所要查找的是否相同
  6. if (quicklistCompare(entry.zi, (unsigned char *)"bar", 3)) {
  7. //进行一些操作...........
  8. }
  9. i++;
  10. }

总结 quicklist的特性

  • 其是一个节点为ziplist的双端链表
  • 节点采用了ziplist,解决了传统链表的内存占用和易产生内存碎片问题
  • 对比单个ziplist,quicklist使用了多个ziplist,那每个ziplist的entry个数就可以控制的比较小,解决了连续空间申请效率和ziplist变更的时间复杂度过于大的问题
  • 中间节点可以压缩,进一步节省了内存
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/426996
推荐阅读
相关标签
  

闽ICP备14008679号