- 浏览: 87957 次
- 性别:
- 来自: 广州
-
文章分类
最新评论
-
songfantasy:
不错,学习了
java与c/c++之间的数据交互-----jni点滴 -
wen0301:
有时间,读读看,并且看看花多长时间。
为了练好口语,你敢不敢每天读一遍,坚持一个月? -
wen0301:
加一些 代码,效果会更好。。。
设计模式:简单工厂、工厂方法、抽象工厂之小结与区别 -
wen0301:
能加一些实际代码 效果会更好吧~~
设计模式:简单工厂、工厂方法、抽象工厂之小结与区别 -
lijiancool:
每天一遍,都不要做其他事情了,哦滴神呀。。。。
为了练好口语,你敢不敢每天读一遍,坚持一个月?
<div class="iteye-blog-content-contain" style="font-size: 14px"><p><div id="Article"> </p>
<p> <p><a href="https://www.2cto.com/ym/" target="_blank" class="keylink">源码</a>版本: redis-4.0.1</p> </p>
<p> <p>源码位置:</p> server.h :zskiplistNode和zskiplist的数据结构定义。 t_zset.c: 以zsl开头的函数是SkipList相关的操作函数。 </p>
<p> <h1 id="一跳跃表简介">一、跳跃表简介</h1> </p>
<p> <p>跳跃表(SkipList),其实也是解决查找问题的一种数据结构,但是它既不属于平衡树结构,也不属于Hash结构,它的特点是元素是有序的。有关于跳跃表的更多解释,大家可以参考 张铁蕾老师 - Redis内部数据结构详解(6)——skiplist 中有关跳跃表的描述部分,我接下来主要分析有关于Redis跳跃表本身的代码部分,Redis作者antirez提到Redis中的实现的跳跃表与一般跳跃表相比具有以下三个特点:</p> </p>
<p> <blockquote> </p>
<p> <p>a) this implementation allows for repeated scores. // 允许分值重复<br /> b) the comparison is not just by key (our ‘score’) but by satellite data. //对比的时候不仅比较分值还比较对象的值<br /> c) there is a back pointer, so it’s a doubly linked list with the back pointers being only at “level 1”. //有一个后退指针,即在第一层实现了一个双向链表,允许后退遍历</p> </p>
<p> </blockquote> </p>
<p> <p>接下来我们去看下SkipList的数据结构定义。</p> </p>
<p> <h1 id="二数据结构定义">二、数据结构定义</h1> </p>
<p> <p>有许多数据结构的定义其实是按照(结点+组织方式)来的,结点就是一个数据点,组织方式就是把结点组织起来形成数据结构,比如 双端链表 (ListNode+list)、字典(dictEntry+dictht+dict)等,今天所说的SkipList其实也一样,我们首先看下它的结点定义:</p> </p>
<p> <pre class="brush:sql;"></p>
<p>typedef struct zskiplistNode { </p>
<p> sds ele; //数据域</p>
<p> double score; //分值 </p>
<p> struct zskiplistNode *backward; //后向指针,使得跳表第一层组织为双向链表</p>
<p> struct zskiplistLevel { //每一个结点的层级</p>
<p> struct zskiplistNode *forward; //某一层的前向结点</p>
<p> unsigned int span; //某一层距离下一个结点的跨度</p>
<p> } level[]; //level本身是一个柔性数组,最大值为32,由 ZSKIPLIST_MAXLEVEL 定义</p>
<p>} zskiplistNode;</p>
<p></pre> </p>
<p> <p>接下来是组织方式,即使用上面的zskiplistNode组织起一个SkipList:</p> </p>
<p> <pre class="brush:sql;"></p>
<p>typedef struct zskiplist {</p>
<p> struct zskiplistNode *header; //头部</p>
<p> struct zskiplistNode *tail; //尾部</p>
<p> unsigned long length; //长度,即一共有多少个元素</p>
<p> int level; //最大层级,即跳表目前的最大层级</p>
<p>} zskiplist;</pre> </p>
<p> <p>核心的数据结构就是上面两个。</p> </p>
<p> <h1 id="三创建插入查找删除释放">三、创建、插入、查找、删除、释放</h1> </p>
<p> <p>我们以下面这个例子来跟踪SkipList的代码,其中会涉及到的操作有创建、插入、查找、删除、释放等。(ps:将Redis中main函数的代码替换成下面的代码就可以测试)</p> </p>
<p> <pre class="brush:sql;"></p>
<p>// 需要声明下 zslGetElementByRank() 函数,main函数中使用</p>
<p>zkiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank);</p>
<p> </p>
<p>int main(int argc, char **argv) {</p>
<p> </p>
<p> </p>
<p> unsigned long ret;</p>
<p> zskiplistNode *node;</p>
<p> zskiplist *zsl = zslCreate();</p>
<p> </p>
<p> zslInsert(zsl, 65.5, sdsnew(&quot;tom&quot;)); //level = 1</p>
<p> zslInsert(zsl, 87.5, sdsnew(&quot;jack&quot;)); //level = 4</p>
<p> zslInsert(zsl, 70.0, sdsnew(&quot;alice&quot;)); //level = 3</p>
<p> zslInsert(zsl, 95.0, sdsnew(&quot;tony&quot;)); //level = 2</p>
<p> </p>
<p> zrangespec spec = { //定义一个区间, 70.0 &lt;= x &lt;= 90.0</p>
<p> .min = 70.0,</p>
<p> .max = 90.0,</p>
<p> .minex = 0,</p>
<p> .maxex = 0};</p>
<p> </p>
<p> printf(&quot;zslFirstInRange 70.0 &lt;= x &lt;= 90.0, x is:&quot;); // 找到符合区间的最小值</p>
<p> node = zslFirstInRange(zsl, &amp;spec);</p>
<p> printf(&quot;%s-&gt;%f\n&quot;, node-&gt;ele, node-&gt;score);</p>
<p> </p>
<p> printf(&quot;zslLastInRange 70.0 &lt;= x &lt;= 90.0, x is:&quot;); // 找到符合区间的最大值</p>
<p> node = zslLastInRange(zsl, &amp;spec);</p>
<p> printf(&quot;%s-&gt;%f\n&quot;, node-&gt;ele, node-&gt;score);</p>
<p> </p>
<p> printf(&quot;tony's Ranking is :&quot;); // 根据分数获取排名</p>
<p> ret = zslGetRank(zsl, 95.0, sdsnew(&quot;tony&quot;));</p>
<p> printf(&quot;%lu\n&quot;, ret);</p>
<p> </p>
<p> printf(&quot;The Rank equal 4 is :&quot;); // 根据排名获取分数</p>
<p> node = zslGetElementByRank(zsl, 4);</p>
<p> printf(&quot;%s-&gt;%f\n&quot;, node-&gt;ele, node-&gt;score);</p>
<p> </p>
<p> ret = zslDelete(zsl, 70.0, sdsnew(&quot;alice&quot;), &amp;node); // 删除元素</p>
<p> if (ret == 1) {</p>
<p> printf(&quot;Delete node:%s-&gt;%f success!\n&quot;, node-&gt;ele, node-&gt;score);</p>
<p> }</p>
<p> </p>
<p> zslFree(zsl); // 释放zsl</p>
<p> </p>
<p> return 0;</p>
<p>}</p>
<p> </p>
<p>Out &gt; </p>
<p>zslFirstInRange 70.0 &lt;= x &lt;= 90.0, x is:alice-&gt;70.000000</p>
<p>zslLastInRange 70.0 &lt;= x &lt;= 90.0, x is:jack-&gt;87.500000</p>
<p>tony's Ranking is :4</p>
<p>The Rank equal 4 is :tony-&gt;95.000000</p>
<p>Delete node:alice-&gt;70.000000 success!</pre> 接下来我们逐行分析代码,首先zskiplist *zsl = zslCreate();创建了一个SkipList,需要关注的重点是会初始化zsl-&gt;header为最大层级32,因为 ZSKIPLIST_MAXLEVEL 定义为32,这个原因与SkipList中获取Level的随机函数有关,具体参考文章开头给的博客链接。我们看下zslCreate的代码: </p>
<p> <pre class="brush:sql;"></p>
<p>zskiplist *zslCreate(void) {</p>
<p> int j;</p>
<p> zskiplist *zsl;</p>
<p> </p>
<p> zsl = zmalloc(sizeof(*zsl)); // 申请空间</p>
<p> zsl-&gt;level = 1; // 初始层级定义为1</p>
<p> zsl-&gt;length = 0;</p>
<p> zsl-&gt;header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); // 初始化header为32层</p>
<p> for (j = 0; j &lt; ZSKIPLIST_MAXLEVEL; j++) {</p>
<p> zsl-&gt;header-&gt;level[j].forward = NULL;</p>
<p> zsl-&gt;header-&gt;level[j].span = 0;</p>
<p> }</p>
<p> zsl-&gt;header-&gt;backward = NULL; </p>
<p> zsl-&gt;tail = NULL; // tail目前为NULL</p>
<p> return zsl; </p>
<p>}</p>
<p> </p>
<p>// zslCreateNode根据传入的level和score以及ele创建一个level层的zskiplistNode</p>
<p>zskiplistNode *zslCreateNode(int level, double score, sds ele) { </p>
<p> zskiplistNode *zn =</p>
<p> zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));</p>
<p> zn-&gt;score = score;</p>
<p> zn-&gt;ele = ele;</p>
<p> return zn;</p>
<p>}</pre> </p>
<p> <p>目前我们的zsl如下图所示:</p> </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090518123.jpg" /></p> 接下来我们开始向zsl中插入数据,zslInsert(zsl, 65.5, sdsnew(&quot;tom&quot;));zslInsert的代码如下所示: </p>
<p> <pre class="brush:sql;"></p>
<p>zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {</p>
<p> </p>
<p> /* 虽然整个代码较长,但是从整体逻辑上可以分为三部分:</p>
<p> * 1:根据目前传入的score找到插入位置x,这个过程会保存各层x的前一个位置节点 </p>
<p> * 就像我们对有序单链表插入节点的时候先要找到比目前数字小的节点保存下来。</p>
<p> * 2:根据随机函数获取level,生成新的节点</p>
<p> * 3:修改各个指针的指向,将创建的新节点插入。</p>
<p> */ </p>
<p> </p>
<p> zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;</p>
<p> unsigned int rank[ZSKIPLIST_MAXLEVEL];</p>
<p> int i, level;</p>
<p> </p>
<p> /* 第一步: 根据目前传入的score找到插入位置x,并且将各层的前置节点保存至rank[]中 */</p>
<p> serverAssert(!isnan(score));</p>
<p> x = zsl-&gt;header;</p>
<p> for (i = zsl-&gt;level-1; i &gt;= 0; i--) {</p>
<p> /* store rank that is crossed to reach the insert position */</p>
<p> rank[i] = i == (zsl-&gt;level-1) ? 0 : rank[i+1];</p>
<p> while (x-&gt;level[i].forward &amp;&amp;</p>
<p> (x-&gt;level[i].forward-&gt;score &lt; score ||</p>
<p> (x-&gt;level[i].forward-&gt;score == score &amp;&amp;</p>
<p> sdscmp(x-&gt;level[i].forward-&gt;ele,ele) &lt; 0)))</p>
<p> {</p>
<p> rank[i] += x-&gt;level[i].span;</p>
<p> x = x-&gt;level[i].forward;</p>
<p> }</p>
<p> update[i] = x;</p>
<p> }</p>
<p> /* we assume the element is not already inside, since we allow duplicated</p>
<p> * scores, reinserting the same element should never happen since the</p>
<p> * caller of zslInsert() should test in the hash table if the element is</p>
<p> * already inside or not. */</p>
<p> </p>
<p> /* 第二步:获取level,生成新的节点 */</p>
<p> level = zslRandomLevel(); </p>
<p> if (level &gt; zsl-&gt;level) {</p>
<p> for (i = zsl-&gt;level; i &lt; level; i++) {</p>
<p> rank[i] = 0;</p>
<p> update[i] = zsl-&gt;header;</p>
<p> update[i]-&gt;level[i].span = zsl-&gt;length;</p>
<p> }</p>
<p> zsl-&gt;level = level;</p>
<p> }</p>
<p> x = zslCreateNode(level,score,ele);</p>
<p> </p>
<p> /* 第三步:修改各个指针的指向,将创建的新节点插入 */</p>
<p> for (i = 0; i &lt; level; i++) {</p>
<p> x-&gt;level[i].forward = update[i]-&gt;level[i].forward;</p>
<p> update[i]-&gt;level[i].forward = x;</p>
<p> </p>
<p> /* update span covered by update[i] as x is inserted here */</p>
<p> x-&gt;level[i].span = update[i]-&gt;level[i].span - (rank[0] - rank[i]);</p>
<p> update[i]-&gt;level[i].span = (rank[0] - rank[i]) + 1;</p>
<p> }</p>
<p> </p>
<p> /* increment span for untouched levels */</p>
<p> for (i = level; i &lt; zsl-&gt;level; i++) {</p>
<p> update[i]-&gt;level[i].span++;</p>
<p> }</p>
<p> </p>
<p> /* 更新backword的指向 */</p>
<p> x-&gt;backward = (update[0] == zsl-&gt;header) ? NULL : update[0];</p>
<p> if (x-&gt;level[0].forward)</p>
<p> x-&gt;level[0].forward-&gt;backward = x;</p>
<p> else</p>
<p> zsl-&gt;tail = x;</p>
<p> zsl-&gt;length++;</p>
<p> return x;</p>
<p>}</pre> </p>
<p> <p>需要注意的是span的含义,它表示当前节点距离下一个节点的跨度,之所以可以根据rank排名获取元素,就是根据span确定的。update[i]保存的就是第 i 层应该插入节点的前一个节点,在第三步更新指针的时候使用。插入了一个元素的zsl如下图所示(level=1):</p> </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090518124.jpg" /></p> 接着我们继续插入后面的三条数据,他们的level分别为jack-&gt;4、alice-&gt;3、tony-&gt;2,此时的zsl如下图所示,注意span的更新: </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090519125.jpg" /></p> 好了,插入终于结束啦!接下来我们看下查找的相关操作,上面的代码中有关查找举了4个例子,分别是:</p>
<p> <br /> 1)查找指定范围内最小的元素</p>
<p> <br /> 2)查找指定范围内最大的元素</p>
<p> <br /> 3)根据名称获取排名</p>
<p> <br /> 4)根据排名获取名称 </p>
<p> <p>我们分析下(1)和(4),(2)、(3)同理。首先来看(1),用zrangespec结构体定义了一个范围为70.0 &lt;= x &lt;= 90.0,有关zrangespec结构体如下所示:</p> </p>
<p> <pre class="brush:sql;"></p>
<p>typedef struct {</p>
<p> double min, max; // 定义最小范围和最大范围</p>
<p> int minex, maxex; // 是否包含最小和最大本身,为 0 表示包含,1 表示不包含</p>
<p>} zrangespec;</p>
<p> </p>
<p>/* 定义范围的代码如下所示 */</p>
<p>zrangespec spec = { //定义spec, 70.0 &lt;= x &lt;= 90.0</p>
<p> .min = 70.0, </p>
<p> .max = 90.0,</p>
<p> .minex = 0,</p>
<p> .maxex = 0}; //为结构体元素赋值</pre> </p>
<p> <p>下面调用zslFirstInRange()函数遍历得到了满足70.0 &lt;= x &lt;= 90.0的最小节点,代码如下:</p> </p>
<p> <pre class="brush:sql;"></p>
<p>/* Find the first node that is contained in the specified range.</p>
<p> * Returns NULL when no element is contained in the range. */</p>
<p>zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) {</p>
<p> zskiplistNode *x;</p>
<p> int i;</p>
<p> </p>
<p> /* If everything is out of range, return early. */</p>
<p> if (!zslIsInRange(zsl,range)) return NULL; // 判断给定的范围是否合法</p>
<p> </p>
<p> x = zsl-&gt;header; </p>
<p> for (i = zsl-&gt;level-1; i &gt;= 0; i--) { // 从最高的Level开始 </p>
<p> /* Go forward while *OUT* of range. */ </p>
<p> while (x-&gt;level[i].forward &amp;&amp; //只要没结束 &amp;&amp; 目前结点的score小于目标score</p>
<p> !zslValueGteMin(x-&gt;level[i].forward-&gt;score,range))</p>
<p> // 将结点走到当前的节点</p>
<p> x = x-&gt;level[i].forward;</p>
<p> }</p>
<p> </p>
<p> /* This is an inner range, so the next node cannot be NULL. */</p>
<p> x = x-&gt;level[0].forward; // 找到了符合的点</p>
<p> serverAssert(x != NULL); </p>
<p> </p>
<p> /* Check if score &lt;= max. */</p>
<p> if (!zslValueLteMax(x-&gt;score,range)) return NULL; // 判断返回的值是否小于max值</p>
<p> return x;</p>
<p>}</pre> </p>
<p> <p>可以看到,遍历的核心思想是:<br /> (1) 高Level -&gt; 低Level<br /> (2) 小score -&gt; 大score<br /> 即在从高Level遍历比较过程中,如果此时的score小于了某个高level的值,就在这个节点前一个节点降低一层Level继续往前遍历,我们找70.0的路线如下图所示(图中红线):</p> </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090519126.jpg" /></p> 继续看下根据排名获取元素的函数zslGetElementByRank(),主要是根据span域来完成,代码如下所示: </p>
<p> <pre class="brush:sql;"></p>
<p>zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {</p>
<p> zskiplistNode *x;</p>
<p> unsigned long traversed = 0;</p>
<p> int i;</p>
<p> </p>
<p> x = zsl-&gt;header;</p>
<p> for (i = zsl-&gt;level-1; i &gt;= 0; i--) {</p>
<p> while (x-&gt;level[i].forward &amp;&amp; (traversed + x-&gt;level[i].span) &lt;= rank)</p>
<p> {</p>
<p> traversed += x-&gt;level[i].span;</p>
<p> x = x-&gt;level[i].forward;</p>
<p> }</p>
<p> if (traversed == rank) {</p>
<p> return x;</p>
<p> }</p>
<p> }</p>
<p> return NULL;</p>
<p>}</pre> </p>
<p> <p>遍历的思想和之前没有什么差别,本次遍历路线如下图所示:</p> </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090520128.jpg" /></p> 接着我们看下Delete()函数,ret = zslDelete(zsl, 70.0, sdsnew(&quot;alice&quot;), &amp;node); 表示删除zsl中score为70.0,数据为alice的元素,这也是Redis SkipList的第二个特征,比较一个元素不仅比较score,而且比较数据,下面看下zslDelete的代码: </p>
<p> <pre class="brush:sql;"></p>
<p>int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {</p>
<p> zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;</p>
<p> int i;</p>
<p> </p>
<p> x = zsl-&gt;header;</p>
<p> for (i = zsl-&gt;level-1; i &gt;= 0; i--) {</p>
<p> while (x-&gt;level[i].forward &amp;&amp;</p>
<p> (x-&gt;level[i].forward-&gt;score &lt; score ||</p>
<p> (x-&gt;level[i].forward-&gt;score == score &amp;&amp;</p>
<p> sdscmp(x-&gt;level[i].forward-&gt;ele,ele) &lt; 0)))</p>
<p> {</p>
<p> x = x-&gt;level[i].forward;</p>
<p> }</p>
<p> update[i] = x;</p>
<p> }</p>
<p> /* We may have multiple elements with the same score, what we need</p>
<p> * is to find the element with both the right score and object. */</p>
<p> x = x-&gt;level[0].forward;</p>
<p> if (x &amp;&amp; score == x-&gt;score &amp;&amp; sdscmp(x-&gt;ele,ele) == 0) {</p>
<p> zslDeleteNode(zsl, x, update);</p>
<p> if (!node)</p>
<p> zslFreeNode(x);</p>
<p> else</p>
<p> *node = x;</p>
<p> return 1;</p>
<p> }</p>
<p> return 0; /* not found */</p>
<p>}</pre> 需要注意的是zslDelete()第四个参数,是一个zskipListNode **类型,它如果不为NULL,那么代码在遍历找到node之后不会将其直接释放,而是将地址交给它,后续这块空间的释放就必须由我们手动处理。 遍历比较的思想和之前还是一样, 在update[]中记录下各层删除节点之前的节点。 while循环比较条件,sdscmp(x-&gt;level[i].forward-&gt;ele,ele) &lt; 0是因为插入函数zslInsert()也是按照这个逻辑插入的。 最后需要再次比较if (x &amp;&amp; score == x-&gt;score &amp;&amp; sdscmp(x-&gt;ele,ele) == 0)是因为Redis SkipList允许相同score的元素存在。 </p>
<p> <p>最后看看释放函数zslFree(zsl),思想很简单,因为level[0]一定是连续的(并且是一个双向链表),所以从level[0]依次遍历释放就行了。</p> </p>
<p> <pre class="brush:sql;"></p>
<p>/* Free a whole skiplist. */</p>
<p>void zslFree(zskiplist *zsl) {</p>
<p> zskiplistNode *node = zsl-&gt;header-&gt;level[0].forward, *next;</p>
<p> </p>
<p> zfree(zsl-&gt;header);</p>
<p> while(node) {</p>
<p> next = node-&gt;level[0].forward;</p>
<p> zslFreeNode(node);</p>
<p> node = next;</p>
<p> }</p>
<p> zfree(zsl);</p>
<p>}</pre> </p>
<p> <p>通过上面的例子,我们分析了zskiplist的创建、插入、查找、删除、释放等操作,结合数据结构的定义,基本上分析清楚了zskiplist。其实zskiplist在Redis中的主要用处是和dict一起实现Sorted Set,这个我们后续看Sorted Set的时候再分析。</p> </p>
<p> <h1 id="四性能分析">四、性能分析</h1> </p>
<p> <table> </p>
<p> <thead> </p>
<p> <tr> </p>
<p> <th> 操作</th> </p>
<p> <th> 一般性能</th> </p>
<p> <th> 最坏性能</th> </p>
<p> </tr> </p>
<p> </thead> </p>
<p> <tbody> </p>
<p> <tr> </p>
<p> <td>插入</td> </p>
<p> <td>O(log n)</td> </p>
<p> <td>O(n)</td> </p>
<p> </tr> </p>
<p> <tr> </p>
<p> <td>删除</td> </p>
<p> <td>O(log n)</td> </p>
<p> <td>O(n)</td> </p>
<p> </tr> </p>
<p> <tr> </p>
<p> <td>搜索</td> </p>
<p> <td>O(log n)</td> </p>
<p> <td>O(n)</td> </p>
<p> </tr> </p>
<p> </tbody> </p>
<p> </table> </p>
<p> <p>如果想了解更多关于跳表本身,比如RandomLevel()的随机性等,一定不要错过 维基百科 上的内容。</p> </p>
<p></div></p></div>
<p> <p><a href="https://www.2cto.com/ym/" target="_blank" class="keylink">源码</a>版本: redis-4.0.1</p> </p>
<p> <p>源码位置:</p> server.h :zskiplistNode和zskiplist的数据结构定义。 t_zset.c: 以zsl开头的函数是SkipList相关的操作函数。 </p>
<p> <h1 id="一跳跃表简介">一、跳跃表简介</h1> </p>
<p> <p>跳跃表(SkipList),其实也是解决查找问题的一种数据结构,但是它既不属于平衡树结构,也不属于Hash结构,它的特点是元素是有序的。有关于跳跃表的更多解释,大家可以参考 张铁蕾老师 - Redis内部数据结构详解(6)——skiplist 中有关跳跃表的描述部分,我接下来主要分析有关于Redis跳跃表本身的代码部分,Redis作者antirez提到Redis中的实现的跳跃表与一般跳跃表相比具有以下三个特点:</p> </p>
<p> <blockquote> </p>
<p> <p>a) this implementation allows for repeated scores. // 允许分值重复<br /> b) the comparison is not just by key (our ‘score’) but by satellite data. //对比的时候不仅比较分值还比较对象的值<br /> c) there is a back pointer, so it’s a doubly linked list with the back pointers being only at “level 1”. //有一个后退指针,即在第一层实现了一个双向链表,允许后退遍历</p> </p>
<p> </blockquote> </p>
<p> <p>接下来我们去看下SkipList的数据结构定义。</p> </p>
<p> <h1 id="二数据结构定义">二、数据结构定义</h1> </p>
<p> <p>有许多数据结构的定义其实是按照(结点+组织方式)来的,结点就是一个数据点,组织方式就是把结点组织起来形成数据结构,比如 双端链表 (ListNode+list)、字典(dictEntry+dictht+dict)等,今天所说的SkipList其实也一样,我们首先看下它的结点定义:</p> </p>
<p> <pre class="brush:sql;"></p>
<p>typedef struct zskiplistNode { </p>
<p> sds ele; //数据域</p>
<p> double score; //分值 </p>
<p> struct zskiplistNode *backward; //后向指针,使得跳表第一层组织为双向链表</p>
<p> struct zskiplistLevel { //每一个结点的层级</p>
<p> struct zskiplistNode *forward; //某一层的前向结点</p>
<p> unsigned int span; //某一层距离下一个结点的跨度</p>
<p> } level[]; //level本身是一个柔性数组,最大值为32,由 ZSKIPLIST_MAXLEVEL 定义</p>
<p>} zskiplistNode;</p>
<p></pre> </p>
<p> <p>接下来是组织方式,即使用上面的zskiplistNode组织起一个SkipList:</p> </p>
<p> <pre class="brush:sql;"></p>
<p>typedef struct zskiplist {</p>
<p> struct zskiplistNode *header; //头部</p>
<p> struct zskiplistNode *tail; //尾部</p>
<p> unsigned long length; //长度,即一共有多少个元素</p>
<p> int level; //最大层级,即跳表目前的最大层级</p>
<p>} zskiplist;</pre> </p>
<p> <p>核心的数据结构就是上面两个。</p> </p>
<p> <h1 id="三创建插入查找删除释放">三、创建、插入、查找、删除、释放</h1> </p>
<p> <p>我们以下面这个例子来跟踪SkipList的代码,其中会涉及到的操作有创建、插入、查找、删除、释放等。(ps:将Redis中main函数的代码替换成下面的代码就可以测试)</p> </p>
<p> <pre class="brush:sql;"></p>
<p>// 需要声明下 zslGetElementByRank() 函数,main函数中使用</p>
<p>zkiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank);</p>
<p> </p>
<p>int main(int argc, char **argv) {</p>
<p> </p>
<p> </p>
<p> unsigned long ret;</p>
<p> zskiplistNode *node;</p>
<p> zskiplist *zsl = zslCreate();</p>
<p> </p>
<p> zslInsert(zsl, 65.5, sdsnew(&quot;tom&quot;)); //level = 1</p>
<p> zslInsert(zsl, 87.5, sdsnew(&quot;jack&quot;)); //level = 4</p>
<p> zslInsert(zsl, 70.0, sdsnew(&quot;alice&quot;)); //level = 3</p>
<p> zslInsert(zsl, 95.0, sdsnew(&quot;tony&quot;)); //level = 2</p>
<p> </p>
<p> zrangespec spec = { //定义一个区间, 70.0 &lt;= x &lt;= 90.0</p>
<p> .min = 70.0,</p>
<p> .max = 90.0,</p>
<p> .minex = 0,</p>
<p> .maxex = 0};</p>
<p> </p>
<p> printf(&quot;zslFirstInRange 70.0 &lt;= x &lt;= 90.0, x is:&quot;); // 找到符合区间的最小值</p>
<p> node = zslFirstInRange(zsl, &amp;spec);</p>
<p> printf(&quot;%s-&gt;%f\n&quot;, node-&gt;ele, node-&gt;score);</p>
<p> </p>
<p> printf(&quot;zslLastInRange 70.0 &lt;= x &lt;= 90.0, x is:&quot;); // 找到符合区间的最大值</p>
<p> node = zslLastInRange(zsl, &amp;spec);</p>
<p> printf(&quot;%s-&gt;%f\n&quot;, node-&gt;ele, node-&gt;score);</p>
<p> </p>
<p> printf(&quot;tony's Ranking is :&quot;); // 根据分数获取排名</p>
<p> ret = zslGetRank(zsl, 95.0, sdsnew(&quot;tony&quot;));</p>
<p> printf(&quot;%lu\n&quot;, ret);</p>
<p> </p>
<p> printf(&quot;The Rank equal 4 is :&quot;); // 根据排名获取分数</p>
<p> node = zslGetElementByRank(zsl, 4);</p>
<p> printf(&quot;%s-&gt;%f\n&quot;, node-&gt;ele, node-&gt;score);</p>
<p> </p>
<p> ret = zslDelete(zsl, 70.0, sdsnew(&quot;alice&quot;), &amp;node); // 删除元素</p>
<p> if (ret == 1) {</p>
<p> printf(&quot;Delete node:%s-&gt;%f success!\n&quot;, node-&gt;ele, node-&gt;score);</p>
<p> }</p>
<p> </p>
<p> zslFree(zsl); // 释放zsl</p>
<p> </p>
<p> return 0;</p>
<p>}</p>
<p> </p>
<p>Out &gt; </p>
<p>zslFirstInRange 70.0 &lt;= x &lt;= 90.0, x is:alice-&gt;70.000000</p>
<p>zslLastInRange 70.0 &lt;= x &lt;= 90.0, x is:jack-&gt;87.500000</p>
<p>tony's Ranking is :4</p>
<p>The Rank equal 4 is :tony-&gt;95.000000</p>
<p>Delete node:alice-&gt;70.000000 success!</pre> 接下来我们逐行分析代码,首先zskiplist *zsl = zslCreate();创建了一个SkipList,需要关注的重点是会初始化zsl-&gt;header为最大层级32,因为 ZSKIPLIST_MAXLEVEL 定义为32,这个原因与SkipList中获取Level的随机函数有关,具体参考文章开头给的博客链接。我们看下zslCreate的代码: </p>
<p> <pre class="brush:sql;"></p>
<p>zskiplist *zslCreate(void) {</p>
<p> int j;</p>
<p> zskiplist *zsl;</p>
<p> </p>
<p> zsl = zmalloc(sizeof(*zsl)); // 申请空间</p>
<p> zsl-&gt;level = 1; // 初始层级定义为1</p>
<p> zsl-&gt;length = 0;</p>
<p> zsl-&gt;header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL); // 初始化header为32层</p>
<p> for (j = 0; j &lt; ZSKIPLIST_MAXLEVEL; j++) {</p>
<p> zsl-&gt;header-&gt;level[j].forward = NULL;</p>
<p> zsl-&gt;header-&gt;level[j].span = 0;</p>
<p> }</p>
<p> zsl-&gt;header-&gt;backward = NULL; </p>
<p> zsl-&gt;tail = NULL; // tail目前为NULL</p>
<p> return zsl; </p>
<p>}</p>
<p> </p>
<p>// zslCreateNode根据传入的level和score以及ele创建一个level层的zskiplistNode</p>
<p>zskiplistNode *zslCreateNode(int level, double score, sds ele) { </p>
<p> zskiplistNode *zn =</p>
<p> zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));</p>
<p> zn-&gt;score = score;</p>
<p> zn-&gt;ele = ele;</p>
<p> return zn;</p>
<p>}</pre> </p>
<p> <p>目前我们的zsl如下图所示:</p> </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090518123.jpg" /></p> 接下来我们开始向zsl中插入数据,zslInsert(zsl, 65.5, sdsnew(&quot;tom&quot;));zslInsert的代码如下所示: </p>
<p> <pre class="brush:sql;"></p>
<p>zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {</p>
<p> </p>
<p> /* 虽然整个代码较长,但是从整体逻辑上可以分为三部分:</p>
<p> * 1:根据目前传入的score找到插入位置x,这个过程会保存各层x的前一个位置节点 </p>
<p> * 就像我们对有序单链表插入节点的时候先要找到比目前数字小的节点保存下来。</p>
<p> * 2:根据随机函数获取level,生成新的节点</p>
<p> * 3:修改各个指针的指向,将创建的新节点插入。</p>
<p> */ </p>
<p> </p>
<p> zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;</p>
<p> unsigned int rank[ZSKIPLIST_MAXLEVEL];</p>
<p> int i, level;</p>
<p> </p>
<p> /* 第一步: 根据目前传入的score找到插入位置x,并且将各层的前置节点保存至rank[]中 */</p>
<p> serverAssert(!isnan(score));</p>
<p> x = zsl-&gt;header;</p>
<p> for (i = zsl-&gt;level-1; i &gt;= 0; i--) {</p>
<p> /* store rank that is crossed to reach the insert position */</p>
<p> rank[i] = i == (zsl-&gt;level-1) ? 0 : rank[i+1];</p>
<p> while (x-&gt;level[i].forward &amp;&amp;</p>
<p> (x-&gt;level[i].forward-&gt;score &lt; score ||</p>
<p> (x-&gt;level[i].forward-&gt;score == score &amp;&amp;</p>
<p> sdscmp(x-&gt;level[i].forward-&gt;ele,ele) &lt; 0)))</p>
<p> {</p>
<p> rank[i] += x-&gt;level[i].span;</p>
<p> x = x-&gt;level[i].forward;</p>
<p> }</p>
<p> update[i] = x;</p>
<p> }</p>
<p> /* we assume the element is not already inside, since we allow duplicated</p>
<p> * scores, reinserting the same element should never happen since the</p>
<p> * caller of zslInsert() should test in the hash table if the element is</p>
<p> * already inside or not. */</p>
<p> </p>
<p> /* 第二步:获取level,生成新的节点 */</p>
<p> level = zslRandomLevel(); </p>
<p> if (level &gt; zsl-&gt;level) {</p>
<p> for (i = zsl-&gt;level; i &lt; level; i++) {</p>
<p> rank[i] = 0;</p>
<p> update[i] = zsl-&gt;header;</p>
<p> update[i]-&gt;level[i].span = zsl-&gt;length;</p>
<p> }</p>
<p> zsl-&gt;level = level;</p>
<p> }</p>
<p> x = zslCreateNode(level,score,ele);</p>
<p> </p>
<p> /* 第三步:修改各个指针的指向,将创建的新节点插入 */</p>
<p> for (i = 0; i &lt; level; i++) {</p>
<p> x-&gt;level[i].forward = update[i]-&gt;level[i].forward;</p>
<p> update[i]-&gt;level[i].forward = x;</p>
<p> </p>
<p> /* update span covered by update[i] as x is inserted here */</p>
<p> x-&gt;level[i].span = update[i]-&gt;level[i].span - (rank[0] - rank[i]);</p>
<p> update[i]-&gt;level[i].span = (rank[0] - rank[i]) + 1;</p>
<p> }</p>
<p> </p>
<p> /* increment span for untouched levels */</p>
<p> for (i = level; i &lt; zsl-&gt;level; i++) {</p>
<p> update[i]-&gt;level[i].span++;</p>
<p> }</p>
<p> </p>
<p> /* 更新backword的指向 */</p>
<p> x-&gt;backward = (update[0] == zsl-&gt;header) ? NULL : update[0];</p>
<p> if (x-&gt;level[0].forward)</p>
<p> x-&gt;level[0].forward-&gt;backward = x;</p>
<p> else</p>
<p> zsl-&gt;tail = x;</p>
<p> zsl-&gt;length++;</p>
<p> return x;</p>
<p>}</pre> </p>
<p> <p>需要注意的是span的含义,它表示当前节点距离下一个节点的跨度,之所以可以根据rank排名获取元素,就是根据span确定的。update[i]保存的就是第 i 层应该插入节点的前一个节点,在第三步更新指针的时候使用。插入了一个元素的zsl如下图所示(level=1):</p> </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090518124.jpg" /></p> 接着我们继续插入后面的三条数据,他们的level分别为jack-&gt;4、alice-&gt;3、tony-&gt;2,此时的zsl如下图所示,注意span的更新: </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090519125.jpg" /></p> 好了,插入终于结束啦!接下来我们看下查找的相关操作,上面的代码中有关查找举了4个例子,分别是:</p>
<p> <br /> 1)查找指定范围内最小的元素</p>
<p> <br /> 2)查找指定范围内最大的元素</p>
<p> <br /> 3)根据名称获取排名</p>
<p> <br /> 4)根据排名获取名称 </p>
<p> <p>我们分析下(1)和(4),(2)、(3)同理。首先来看(1),用zrangespec结构体定义了一个范围为70.0 &lt;= x &lt;= 90.0,有关zrangespec结构体如下所示:</p> </p>
<p> <pre class="brush:sql;"></p>
<p>typedef struct {</p>
<p> double min, max; // 定义最小范围和最大范围</p>
<p> int minex, maxex; // 是否包含最小和最大本身,为 0 表示包含,1 表示不包含</p>
<p>} zrangespec;</p>
<p> </p>
<p>/* 定义范围的代码如下所示 */</p>
<p>zrangespec spec = { //定义spec, 70.0 &lt;= x &lt;= 90.0</p>
<p> .min = 70.0, </p>
<p> .max = 90.0,</p>
<p> .minex = 0,</p>
<p> .maxex = 0}; //为结构体元素赋值</pre> </p>
<p> <p>下面调用zslFirstInRange()函数遍历得到了满足70.0 &lt;= x &lt;= 90.0的最小节点,代码如下:</p> </p>
<p> <pre class="brush:sql;"></p>
<p>/* Find the first node that is contained in the specified range.</p>
<p> * Returns NULL when no element is contained in the range. */</p>
<p>zskiplistNode *zslFirstInRange(zskiplist *zsl, zrangespec *range) {</p>
<p> zskiplistNode *x;</p>
<p> int i;</p>
<p> </p>
<p> /* If everything is out of range, return early. */</p>
<p> if (!zslIsInRange(zsl,range)) return NULL; // 判断给定的范围是否合法</p>
<p> </p>
<p> x = zsl-&gt;header; </p>
<p> for (i = zsl-&gt;level-1; i &gt;= 0; i--) { // 从最高的Level开始 </p>
<p> /* Go forward while *OUT* of range. */ </p>
<p> while (x-&gt;level[i].forward &amp;&amp; //只要没结束 &amp;&amp; 目前结点的score小于目标score</p>
<p> !zslValueGteMin(x-&gt;level[i].forward-&gt;score,range))</p>
<p> // 将结点走到当前的节点</p>
<p> x = x-&gt;level[i].forward;</p>
<p> }</p>
<p> </p>
<p> /* This is an inner range, so the next node cannot be NULL. */</p>
<p> x = x-&gt;level[0].forward; // 找到了符合的点</p>
<p> serverAssert(x != NULL); </p>
<p> </p>
<p> /* Check if score &lt;= max. */</p>
<p> if (!zslValueLteMax(x-&gt;score,range)) return NULL; // 判断返回的值是否小于max值</p>
<p> return x;</p>
<p>}</pre> </p>
<p> <p>可以看到,遍历的核心思想是:<br /> (1) 高Level -&gt; 低Level<br /> (2) 小score -&gt; 大score<br /> 即在从高Level遍历比较过程中,如果此时的score小于了某个高level的值,就在这个节点前一个节点降低一层Level继续往前遍历,我们找70.0的路线如下图所示(图中红线):</p> </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090519126.jpg" /></p> 继续看下根据排名获取元素的函数zslGetElementByRank(),主要是根据span域来完成,代码如下所示: </p>
<p> <pre class="brush:sql;"></p>
<p>zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {</p>
<p> zskiplistNode *x;</p>
<p> unsigned long traversed = 0;</p>
<p> int i;</p>
<p> </p>
<p> x = zsl-&gt;header;</p>
<p> for (i = zsl-&gt;level-1; i &gt;= 0; i--) {</p>
<p> while (x-&gt;level[i].forward &amp;&amp; (traversed + x-&gt;level[i].span) &lt;= rank)</p>
<p> {</p>
<p> traversed += x-&gt;level[i].span;</p>
<p> x = x-&gt;level[i].forward;</p>
<p> }</p>
<p> if (traversed == rank) {</p>
<p> return x;</p>
<p> }</p>
<p> }</p>
<p> return NULL;</p>
<p>}</pre> </p>
<p> <p>遍历的思想和之前没有什么差别,本次遍历路线如下图所示:</p> </p>
<p> <p><img alt="这里写图片描述" src="/uploadfile/Collfiles/20171114/20171114090520128.jpg" /></p> 接着我们看下Delete()函数,ret = zslDelete(zsl, 70.0, sdsnew(&quot;alice&quot;), &amp;node); 表示删除zsl中score为70.0,数据为alice的元素,这也是Redis SkipList的第二个特征,比较一个元素不仅比较score,而且比较数据,下面看下zslDelete的代码: </p>
<p> <pre class="brush:sql;"></p>
<p>int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {</p>
<p> zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;</p>
<p> int i;</p>
<p> </p>
<p> x = zsl-&gt;header;</p>
<p> for (i = zsl-&gt;level-1; i &gt;= 0; i--) {</p>
<p> while (x-&gt;level[i].forward &amp;&amp;</p>
<p> (x-&gt;level[i].forward-&gt;score &lt; score ||</p>
<p> (x-&gt;level[i].forward-&gt;score == score &amp;&amp;</p>
<p> sdscmp(x-&gt;level[i].forward-&gt;ele,ele) &lt; 0)))</p>
<p> {</p>
<p> x = x-&gt;level[i].forward;</p>
<p> }</p>
<p> update[i] = x;</p>
<p> }</p>
<p> /* We may have multiple elements with the same score, what we need</p>
<p> * is to find the element with both the right score and object. */</p>
<p> x = x-&gt;level[0].forward;</p>
<p> if (x &amp;&amp; score == x-&gt;score &amp;&amp; sdscmp(x-&gt;ele,ele) == 0) {</p>
<p> zslDeleteNode(zsl, x, update);</p>
<p> if (!node)</p>
<p> zslFreeNode(x);</p>
<p> else</p>
<p> *node = x;</p>
<p> return 1;</p>
<p> }</p>
<p> return 0; /* not found */</p>
<p>}</pre> 需要注意的是zslDelete()第四个参数,是一个zskipListNode **类型,它如果不为NULL,那么代码在遍历找到node之后不会将其直接释放,而是将地址交给它,后续这块空间的释放就必须由我们手动处理。 遍历比较的思想和之前还是一样, 在update[]中记录下各层删除节点之前的节点。 while循环比较条件,sdscmp(x-&gt;level[i].forward-&gt;ele,ele) &lt; 0是因为插入函数zslInsert()也是按照这个逻辑插入的。 最后需要再次比较if (x &amp;&amp; score == x-&gt;score &amp;&amp; sdscmp(x-&gt;ele,ele) == 0)是因为Redis SkipList允许相同score的元素存在。 </p>
<p> <p>最后看看释放函数zslFree(zsl),思想很简单,因为level[0]一定是连续的(并且是一个双向链表),所以从level[0]依次遍历释放就行了。</p> </p>
<p> <pre class="brush:sql;"></p>
<p>/* Free a whole skiplist. */</p>
<p>void zslFree(zskiplist *zsl) {</p>
<p> zskiplistNode *node = zsl-&gt;header-&gt;level[0].forward, *next;</p>
<p> </p>
<p> zfree(zsl-&gt;header);</p>
<p> while(node) {</p>
<p> next = node-&gt;level[0].forward;</p>
<p> zslFreeNode(node);</p>
<p> node = next;</p>
<p> }</p>
<p> zfree(zsl);</p>
<p>}</pre> </p>
<p> <p>通过上面的例子,我们分析了zskiplist的创建、插入、查找、删除、释放等操作,结合数据结构的定义,基本上分析清楚了zskiplist。其实zskiplist在Redis中的主要用处是和dict一起实现Sorted Set,这个我们后续看Sorted Set的时候再分析。</p> </p>
<p> <h1 id="四性能分析">四、性能分析</h1> </p>
<p> <table> </p>
<p> <thead> </p>
<p> <tr> </p>
<p> <th> 操作</th> </p>
<p> <th> 一般性能</th> </p>
<p> <th> 最坏性能</th> </p>
<p> </tr> </p>
<p> </thead> </p>
<p> <tbody> </p>
<p> <tr> </p>
<p> <td>插入</td> </p>
<p> <td>O(log n)</td> </p>
<p> <td>O(n)</td> </p>
<p> </tr> </p>
<p> <tr> </p>
<p> <td>删除</td> </p>
<p> <td>O(log n)</td> </p>
<p> <td>O(n)</td> </p>
<p> </tr> </p>
<p> <tr> </p>
<p> <td>搜索</td> </p>
<p> <td>O(log n)</td> </p>
<p> <td>O(n)</td> </p>
<p> </tr> </p>
<p> </tbody> </p>
<p> </table> </p>
<p> <p>如果想了解更多关于跳表本身,比如RandomLevel()的随机性等,一定不要错过 维基百科 上的内容。</p> </p>
<p></div></p></div>
发表评论
-
Oracle的where条件in/not in中包含NULL时的处理
2018-01-15 13:15 1614创建一个测试表t_inlinuxidc@TEST> ... -
在sae中设置django,让sae的工作环境跟本地python环境一致
2018-01-15 13:15 443sae中安装有python环境,想让sae导入自己下载的d ... -
win10下Django工程的创建
2018-01-11 13:39 382一、配置环境 win ... -
MySQL 5.7.18 zip文件安装教程
2018-01-11 13:47 535MySQL 5.7.18 zip 文件安装教程 安装 ... -
win10下Django工程的创建
2018-01-11 13:34 599一、配置环境 win10、python3.6、p ... -
MySql数据库逻辑架构讲解
2018-01-11 13:44 638与其他数据库相比,My ... -
Python使用虚拟环境
2018-01-11 13:44 451这里想象一下需 ... -
Kivy 中文教程 实例入门 简易画板 (Simple Paint App):1. 自定义窗口部件 (widget)
2018-01-05 10:01 9111. 框架代码 用 PyCharm 新建一个名为 S ... -
Python 零基础 快速入门 趣味教程 (咪博士 海龟绘图 turtle) 2. 变量
2018-01-05 10:01 1153大家在中学就已经学过变量的概念了。例如:我们令 x = 1 ... -
面向对象1
2018-01-05 09:49 468面向对象概念 面向对象是利用类和对象来创建各种模型对 ... -
Python列表及元组操作
2018-01-05 09:49 581#列表(一组有序 ... -
Python的hasattr() getattr() setattr() 函数使用方法详解
2018-01-05 09:57 732hasattr(object, name)判断一个对象里面 ... -
利用DBUTILS获得刚插入自增id记录的id信息的方法及代码
2018-01-02 12:36 2099我在做两个需要关联的表的时候,第二张表需要知道第一张表的i ... -
win10如何打开sqlserver配置管理器
2018-01-02 12:24 1081win10如何打开sqlserver配置管理器,windo ... -
数据库查询语言DQL使用介绍
2018-01-02 12:24 524(1)C++ppentry.C++om/list.php? ... -
Redis Basic在CentOS下安装指令
2018-01-02 12:23 518安装 C++entOS下安装指令: wge ... -
SpringBoot整合PageHelper实现数据库分页的代码教程
2018-01-02 12:23 777最近学习了SpringBoot 由于需要数据库分页功能 再 ... -
Day01_计算机硬件及启动流程
2017-12-26 17:23 568一.计算机硬件介绍 概念:由一条总线把CPU、内存和 ... -
python+Eclipse+pydev环境搭建
2017-12-26 17:12 421本文重点介绍使用Eclipse+pydev插件来写Pyth ... -
Python学习一:序列基础详解
2017-12-26 17:20 451作者:NiceCui 本文谢绝转载,如需转载需征得作 ...
相关推荐
-有序集合:元素有序且不重复,使用跳跃表(Skip List)实现,支持范围查询。 2. Redis的核心组件: - Redis服务器:处理客户端连接,解析命令,执行操作并返回结果。 - RDB持久化:定期将内存中的数据快照到...
"深入redis学习(九)--redis skiplist.doc"讲解了跳跃表,这是Redis实现有序集合排序的底层数据结构,提供O(log N)的查找、插入和删除效率,比红黑树更节省内存。 "深入redis学习(十)--redis object management....
List:跳跃表、为啥 Redis 使用跳表(Skip List)而不是使用 Red-Black? Java 的 PriorityQueue 文档 https://docs.oracle.com/javase/10/docs/api/java/util/PriorityQueue.html Java 的 Stack 源码 ...
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
8c71b76fb2ec10cf50fc6b0308d3dcfc_9545878e2b97a84b2e089ece58da9e82
Android逆向过程学习
内容概要:本文详细介绍了基于西门子S7-200 PLC的糖果包装控制系统的设计与实现。首先阐述了PLC在工业自动化领域的优势及其在糖果包装生产线中的重要性。接着深入探讨了系统的硬件连接方式,包括传感器、执行机构与PLC的具体接口配置。随后展示了关键的编程实现部分,如糖果计数、包装执行、送膜控制、称重判断以及热封温度控制等具体梯形图代码片段。此外,还分享了一些实用的经验技巧,如防止信号抖动、PID参数优化、故障诊断方法等。最后总结了该系统的优势,强调其对提高生产效率和产品质量的重要作用。 适合人群:从事工业自动化控制、PLC编程的技术人员,尤其是对小型PLC系统感兴趣的工程师。 使用场景及目标:适用于糖果制造企业,旨在提升包装生产线的自动化程度,确保高效稳定的生产过程,同时降低维护成本并提高产品一致性。 其他说明:文中不仅提供了详细的理论讲解和技术指导,还结合实际案例进行了经验分享,有助于读者更好地理解和掌握相关知识。
内容概要:本文详细介绍了参与西门子杯比赛中关于三部十层电梯系统的博图V15.1程序设计及其WinCC画面展示的内容。文中不仅展示了电梯系统的基本架构,如抢单逻辑、方向决策、状态机管理等核心算法(采用SCL语言编写),还分享了许多实际调试过程中遇到的问题及解决方案,例如未初始化变量导致的异常行为、状态机遗漏空闲状态、WinCC画面动态显示的挑战以及通信配置中的ASCII码解析错误等问题。此外,作者还特别提到一些创意性的设计,如电梯同时到达同一层时楼层显示器变为闪烁爱心的效果,以及节能模式下电梯自动停靠中间楼层的功能。 适合人群:对PLC编程、工业自动化控制、电梯调度算法感兴趣的工程技术人员,尤其是准备参加类似竞赛的学生和技术爱好者。 使用场景及目标:适用于希望深入了解PLC编程实践、掌握电梯群控系统的设计思路和技术要点的人士。通过学习本文可以更好地理解如何利用PLC进行复杂的机电一体化项目的开发,提高解决实际问题的能力。 其他说明:文章风格幽默诙谐,将严肃的技术话题融入轻松的生活化比喻之中,使得原本枯燥的专业知识变得生动有趣。同时,文中提供的经验教训对于从事相关领域的工作者来说非常宝贵,能够帮助他们少走弯路并激发更多创新思维。
慧荣量产工具合集.zip
内容概要:本文详细介绍了永磁同步电机(PMSM)的FOC(磁场定向控制)和SVPWM(空间矢量脉宽调制)算法的仿真模型。首先解释了FOC的基本原理及其核心的坐标变换(Clark变换和Park变换),并给出了相应的Python代码实现。接下来探讨了SVPWM算法的工作机制,包括扇区判断和占空比计算的方法。此外,文章还讨论了电机的PI双闭环控制结构,即速度环和电流环的设计与实现。文中不仅提供了详细的理论背景,还分享了一些实用的编程技巧和注意事项,帮助读者更好地理解和应用这些算法。 适合人群:电气工程专业学生、从事电机控制系统开发的技术人员以及对永磁同步电机控制感兴趣的科研人员。 使用场景及目标:① 学习和掌握永磁同步电机的FOC控制和SVPWM算法的具体实现;② 提供丰富的代码示例和实践经验,便于快速搭建和调试仿真模型;③ 探讨不同参数设置对电机性能的影响,提高系统的稳定性和效率。 其他说明:文章强调了在实际应用中需要注意的一些细节问题,如坐标变换中的系数选择、SVPWM算法中的扇区判断优化以及PI控制器的参数调整等。同时,鼓励读者通过动手实验来加深对各个模块的理解。
# 压缩文件中包含: 中文文档 jar包下载地址 Maven依赖 Gradle依赖 源代码下载地址 # 本文件关键字: jar中文文档.zip,java,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压最外层zip,再解压其中的zip包,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件;
Android逆向过程学习
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
3dmax插件
# 【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar中文文档.zip】 中包含: 中文文档:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7-javadoc-API文档-中文(简体)版.zip】 jar包下载地址:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar下载地址(官方地址+国内镜像地址).txt】 Maven依赖:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar Maven依赖信息(可用于项目pom.xml).txt】 Gradle依赖:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar Gradle依赖信息(可用于项目build.gradle).txt】 源代码下载地址:【spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7-sources.jar下载地址(官方地址+国内镜像地址).txt】 # 本文件关键字: spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar中文文档.zip,java,spring-ai-autoconfigure-vector-store-qdrant-1.0.0-M7.jar,org.springframework.ai,spring-ai-autoconfigure-vector-store-qdrant,1.0.0-M7,org.springframework.ai.vectorstore.qdr
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
内容概要:本文详细介绍了平方根容积卡尔曼滤波(SRCKF)在永磁同步电机(PMSM)控制系统中的应用及其相对于传统CKF的优势。文章首先指出传统CKF在处理协方差矩阵时存在的数值不稳定性和非正定问题,导致系统性能下降。接着,作者通过引入SRCKF,利用Cholesky分解和QR分解来确保协方差矩阵的正定性,从而提高状态估计的精度和稳定性。文中展示了具体的电机模型和状态方程,并提供了详细的代码实现,包括状态预测、容积点生成以及观测更新等关键步骤。此外,文章还分享了实际调试过程中遇到的问题及解决方案,如选择合适的矩阵分解库和处理电机参数敏感性。最终,通过实验数据对比,证明了SRCKF在突加负载情况下的优越表现。 适合人群:从事永磁同步电机控制研究的技术人员、研究生及以上学历的研究者。 使用场景及目标:适用于需要高精度状态估计的永磁同步电机控制系统的设计与优化,特别是在处理非线性问题和提高数值稳定性方面。 其他说明:文章引用了相关领域的权威文献,如Arasaratnam的TAC论文和Zhong的《PMSM无传感器控制综述》,并强调了实际工程实践中代码调试的重要性。
# 【tokenizers-***.jar***文档.zip】 中包含: ***文档:【tokenizers-***-javadoc-API文档-中文(简体)版.zip】 jar包下载地址:【tokenizers-***.jar下载地址(官方地址+国内镜像地址).txt】 Maven依赖:【tokenizers-***.jar Maven依赖信息(可用于项目pom.xml).txt】 Gradle依赖:【tokenizers-***.jar Gradle依赖信息(可用于项目build.gradle).txt】 源代码下载地址:【tokenizers-***-sources.jar下载地址(官方地址+国内镜像地址).txt】 # 本文件关键字: tokenizers-***.jar***文档.zip,java,tokenizers-***.jar,ai.djl.huggingface,tokenizers,***,ai.djl.engine.rust,jar包,Maven,第三方jar包,组件,开源组件,第三方组件,Gradle,djl,huggingface,中文API文档,手册,开发手册,使用手册,参考手册 # 使用方法: 解压 【tokenizers-***.jar***文档.zip】,再解压其中的 【tokenizers-***-javadoc-API文档-中文(简体)版.zip】,双击 【index.html】 文件,即可用浏览器打开、进行查看。 # 特殊说明: ·本文档为人性化翻译,精心制作,请放心使用。 ·只翻译了该翻译的内容,如:注释、说明、描述、用法讲解 等; ·不该翻译的内容保持原样,如:类名、方法名、包名、类型、关键字、代码 等。 # 温馨提示: (1)为了防止解压后路径太长导致浏览器无法打开,推荐在解压时选择“解压到当前文件夹”(放心,自带文件夹,文件不会散落一地); (2)有时,一套Java组件会有多个jar,所以在下载前,请仔细阅读本篇描述,以确保这就是你需要的文件; # Maven依赖: ``` <dependency> <groupId>ai.djl.huggingface</groupId> <artifactId>tokenizers</artifactId> <version>***</version> </dependency> ``` # Gradle依赖: ``` Gradle: implementation group: 'ai.djl.huggingface', name: 'tokenizers', version: '***' Gradle (Short): implementation 'ai.djl.huggingface:tokenizers:***' Gradle (Kotlin): implementation("ai.djl.huggingface:tokenizers:***") ``` # 含有的 Java package(包): ``` ai.djl.engine.rust ai.djl.engine.rust.zoo ai.djl.huggingface.tokenizers ai.djl.huggingface.tokenizers.jni ai.djl.huggingface.translator ai.djl.huggingface.zoo ``` # 含有的 Java class(类): ``` ai.djl.engine.rust.RsEngine ai.djl.engine.rust.RsEngineProvider ai.djl.engine.rust.RsModel ai.djl.engine.rust.RsNDArray ai.djl.engine.rust.RsNDArrayEx ai.djl.engine.rust.RsNDArrayIndexer ai.djl.engine.rust.RsNDManager ai.djl.engine.rust.RsSymbolBlock ai.djl.engine.rust.RustLibrary ai.djl.engine.rust.zoo.RsModelZoo ai.djl.engine.rust.zoo.RsZooProvider ai.djl.huggingface.tokenizers.Encoding ai.djl.huggingface.tokenizers.HuggingFaceTokenizer ai.djl.huggingface.tokenizers.HuggingFaceTokenizer.Builder ai.djl.hu
3
pchook源码纯源码不是dll