跳跃表的实现<转>

Redis 的跳跃表由 redis.h/zskiplistNode 和 redis.h/zskiplist 两个结构定义, 其中 zskiplistNode 结构用于表示跳跃表节点, 而zskiplist 结构则用于保存跳跃表节点的相关信息, 比如节点的数量, 以及指向表头节点和表尾节点的指针, 等等。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header;    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-1    一个跳跃表";}

图 5-1 展示了一个跳跃表示例, 位于图片最左边的是 zskiplist 结构, 该结构包含以下属性:

  • header :指向跳跃表的表头节点。
  • tail :指向跳跃表的表尾节点。
  • level :记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
  • length :记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。

位于 zskiplist 结构右方的是四个 zskiplistNode 结构, 该结构包含以下属性:

  • 层(level):节点中用 L1 、 L2 、 L3 等字样标记节点的各个层, L1 代表第一层, L2 代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。
  • 后退(backward)指针:节点中用 BW 字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
  • 分值(score):各个节点中的 1.0 、 2.0 和 3.0 是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
  • 成员对象(obj):各个节点中的 o1 、 o2 和 o3 是节点所保存的成员对象。

注意表头节点和其他节点的构造是一样的: 表头节点也有后退指针、分值和成员对象, 不过表头节点的这些属性都不会被用到, 所以图中省略了这些部分, 只显示了表头节点的各个层。

本节接下来的内容将对 zskiplistNode 和 zskiplist 两个结构进行更详细的介绍。

跳跃表节点

跳跃表节点的实现由 redis.h/zskiplistNode 结构定义:

typedef struct zskiplistNode {

    // 层
    struct zskiplistLevel {

        // 前进指针
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

    // 后退指针
    struct zskiplistNode *backward;

    // 分值
    double score;

    // 成员对象
    robj *obj;

} zskiplistNode;

跳跃表节点的 level 数组可以包含多个元素, 每个元素都包含一个指向其他节点的指针, 程序可以通过这些层来加快访问其他节点的速度, 一般来说, 层的数量越多, 访问其他节点的速度就越快。

每次创建一个新跳跃表节点的时候, 程序都根据幂次定律 (power law,越大的数出现的概率越小) 随机生成一个介于 1 和 32 之间的值作为 level 数组的大小, 这个大小就是层的“高度”。

图 5-2 分别展示了三个高度为 1 层、 3 层和 5 层的节点, 因为 C 语言的数组索引总是从 0 开始的, 所以节点的第一层是 level[0], 而第二层是 level[1] , 以此类推。

digraph {    label = "\n 图 5-2    带有不同层高的节点";    rankdir = LR;    //    node [shape = record];    n1 [label = " zskiplistNode | level[0] | backward | score | obj "];    n2 [label = " zskiplistNode | level[2] | level[1] | level[0] | backward | score | obj "];    n3 [label = " zskiplistNode | level[4] | level[3] | level[2] | level[1] | level[0] | backward | score | obj "];    //    edge [style = invis];    n1 -> n2 -> n3;}

前进指针

每个层都有一个指向表尾方向的前进指针(level[i].forward 属性), 用于从表头向表尾方向访问节点。

图 5-3 用虚线表示出了程序从表头向表尾方向, 遍历跳跃表中所有节点的路径:

  1. 迭代程序首先访问跳跃表的第一个节点(表头), 然后从第四层的前进指针移动到表中的第二个节点。
  2. 在第二个节点时, 程序沿着第二层的前进指针移动到表中的第三个节点。
  3. 在第三个节点时, 程序同样沿着第二层的前进指针移动到表中的第四个节点。
  4. 当程序再次沿着第四个节点的前进指针移动时, 它碰到一个 NULL , 程序知道这时已经到达了跳跃表的表尾, 于是结束这次遍历。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header [style = dashed];    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1", style = dashed];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1", style = dashed];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1", style = dashed];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0", style = dashed];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-3    遍历整个跳跃表";}

跨度

层的跨度(level[i].span 属性)用于记录两个节点之间的距离:

  • 两个节点之间的跨度越大, 它们相距得就越远。
  • 指向 NULL 的所有前进指针的跨度都为 0 , 因为它们没有连向任何节点。

初看上去, 很容易以为跨度和遍历操作有关, 但实际上并不是这样 —— 遍历操作只使用前进指针就可以完成了, 跨度实际上是用来计算排位(rank)的: 在查找某个节点的过程中, 将沿途访问过的所有层的跨度累计起来, 得到的结果就是目标节点在跳跃表中的排位。

举个例子, 图 5-4 用虚线标记了在跳跃表中查找分值为 3.0 、 成员对象为 o3 的节点时, 沿途经历的层: 查找的过程只经过了一个层, 并且层的跨度为 3 , 所以目标节点在跳跃表中的排位为 3 。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header [style = dashed];    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3", style = dashed];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-4    计算节点的排位";}

再举个例子, 图 5-5 用虚线标记了在跳跃表中查找分值为 2.0 、 成员对象为 o2 的节点时, 沿途经历的层: 在查找节点的过程中, 程序经过了两个跨度为 1 的节点, 因此可以计算出, 目标节点在跳跃表中的排位为 2 。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header [style = dashed];    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1", style = dashed];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1", style = dashed];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-5    另一个计算节点排位的例子";}

后退指针

节点的后退指针(backward 属性)用于从表尾向表头方向访问节点: 跟可以一次跳过多个节点的前进指针不同, 因为每个节点只有一个后退指针, 所以每次只能后退至前一个节点。

图 5-6 用虚线展示了如果从表尾向表头遍历跳跃表中的所有节点: 程序首先通过跳跃表的 tail 指针访问表尾节点, 然后通过后退指针访问倒数第二个节点, 之后再沿着后退指针访问倒数第三个节点, 再之后遇到指向 NULL 的后退指针, 于是访问结束。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header;    l:tail -> C [style = dashed];    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back, style = dashed];    label = "\n 图 5-6    从表尾向表头方向遍历跳跃表";}

分值和成员

节点的分值(score 属性)是一个 double 类型的浮点数, 跳跃表中的所有节点都按分值从小到大来排序。

节点的成员对象(obj 属性)是一个指针, 它指向一个字符串对象, 而字符串对象则保存着一个 SDS 值。

在同一个跳跃表中, 各个节点保存的成员对象必须是唯一的, 但是多个节点保存的分值却可以是相同的: 分值相同的节点将按照成员对象在字典序中的大小来进行排序, 成员对象较小的节点会排在前面(靠近表头的方向), 而成员对象较大的节点则会排在后面(靠近表尾的方向)。

举个例子, 在图 5-7 所示的跳跃表中, 三个跳跃表节点都保存了相同的分值 10086.0 , 但保存成员对象 o1 的节点却排在保存成员对象 o2 和 o3 的节点之前, 而保存成员对象 o2 的节点又排在保存成员对象 o3 的节点之前, 由此可见, o1 、 o2 、 o3 三个成员对象在字典中的排序为 o1 <= o2 <= o3 。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 10086.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 10086.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 10086.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header;    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-7    三个带有相同分值的跳跃表节点";}

跳跃表

虽然仅靠多个跳跃表节点就可以组成一个跳跃表, 如图 5-8 所示。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    //l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    //l:header -> header;    //l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-8    由多个跳跃表节点组成的跳跃表";}

但通过使用一个 zskiplist 结构来持有这些节点, 程序可以更方便地对整个跳跃表进行处理, 比如快速访问跳跃表的表头节点和表尾节点, 又或者快速地获取跳跃表节点的数量(也即是跳跃表的长度)等信息, 如图 5-9 所示。

digraph {    rankdir = LR;    node [shape = record, width = "0.5"];    //    l [label = " <header> header | <tail> tail | level \n 5 | length \n 3 "];    subgraph cluster_nodes {        style = invisible;        header [label = " <l32> L32 | ... | <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 "];        bw_null [label = "NULL", shape = plaintext];        level_null [label = "NULL", shape = plaintext];        A [label = " <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 1.0 | o1 "];        B [label = " <l2> L2 | <l1> L1 | <backward> BW | 2.0 | o2 "];        C [label = " <l5> L5 | <l4> L4 | <l3> L3 | <l2> L2 | <l1> L1 | <backward> BW | 3.0 | o3 "];    }    subgraph cluster_nulls {        style = invisible;        n1 [label = "NULL", shape = plaintext];        n2 [label = "NULL", shape = plaintext];        n3 [label = "NULL", shape = plaintext];        n4 [label = "NULL", shape = plaintext];        n5 [label = "NULL", shape = plaintext];    }    //    l:header -> header;    l:tail -> C;    header:l32 -> level_null [label = "0"];    header:l5 -> C:l5 [label = "3"];    header:l4 -> A:l4 [label = "1"];    header:l3 -> A:l3 [label = "1"];    header:l2 -> A:l2 [label = "1"];    header:l1 -> A:l1 [label = "1"];    A:l4 -> C:l4 [label = "2"];    A:l3 -> C:l3 [label = "2"];    A:l2 -> B:l2 [label = "1"];    A:l1 -> B:l1 [label = "1"];    B:l2 -> C:l2 [label = "1"];    B:l1 -> C:l1 [label = "1"];    C:l5 -> n5 [label = "0"];    C:l4 -> n4 [label = "0"];    C:l3 -> n3 [label = "0"];    C:l2 -> n2 [label = "0"];    C:l1 -> n1 [label = "0"];    bw_null -> A:backward -> B:backward -> C:backward [dir = back];    label = "\n 图 5-9    带有 zskiplist 结构的跳跃表";}

zskiplist 结构的定义如下:

typedef struct zskiplist {

    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;

    // 表中节点的数量
    unsigned long length;

    // 表中层数最大的节点的层数
    int level;

} zskiplist;

header 和 tail 指针分别指向跳跃表的表头和表尾节点, 通过这两个指针, 程序定位表头节点和表尾节点的复杂度为 O(1) 。

通过使用 length 属性来记录节点的数量, 程序可以在 O(1) 复杂度内返回跳跃表的长度。

level 属性则用于在 O(1) 复杂度内获取跳跃表中层高最大的那个节点的层数量, 注意表头节点的层高并不计算在内。

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>排行榜</title> <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> <script> function refreshPage() { location.reload(); } // 设置每隔一段时间(如30秒)刷新页面 setInterval(refreshPage, 30000); </script> </head> <body> <div class="container"> <!-- 左侧排行榜 --> <div class="leaderboard left-board"> <h2><i class="trophy"></i> 赛事瞬间(团队)</h2> <table> <thead> <tr> <th>团队排名</th> <th>团队</th> <th>权益</th> <th>盈亏</th> <th>收益率</th> </tr> </thead> <tbody> {% for item in left_data %} <tr class="{% if loop.index == 1 %}gold{% elif loop.index == 2 %}silver{% elif loop.index == 3 %}bronze{% endif %}"> <td>{{ loop.index}}</td> <td>{{ item.团队}}</td> <td>{{ item.权益}}</td> <td>{{ item.盈亏}}</td> <!-- 修正:多余的<去掉,并修正达式括号 --> <td>{{ ((item.盈亏 / 1500000) * 100) | round(2) }}%</td> </tr> {% endfor %} </tbody> </table> </div> <!-- 右侧排行榜 --> <div class="leaderboard right-board"> <h2><i class="trophy"></i> 赛事瞬间(个人)</h2> <table> <thead> <tr> <th>个人排名</th> <th>姓名</th> <th>权益</th> <th>盈亏</th> <th>收益率</th> </tr> </thead> <tbody> {% for item in right_data %} <tr class="{% if loop.index == 1 %}gold{% elif loop.index == 2 %}silver{% elif loop.index == 3 %}bronze{% endif %}"> <td>{{ loop.index}}</td> <td>{{ item.姓名}}</td> <td>{{ item.权益}}</td> <td>{{ item.盈亏}}</td> <td>{{ ((item.盈亏 / 1500000) * 100) | round(2) }}%</td> </tr> {% endfor %} </tbody> </table> </div> </div> </body> </html> import os from flask import Flask, render_template import pandas as pd import asyncio from concurrent.futures import ThreadPoolExecutor app = Flask(__name__) executor = ThreadPoolExecutor(max_workers=4) # 创建一个线程池 async def read_excel_sheet(path: str, sheet_name: str) -> list: """异步读取Excel工作数据""" loop = asyncio.get_event_loop() def _sync_read(): df = pd.read_excel(path, sheet_name=sheet_name, engine='openpyxl') df.columns = [col.strip() for col in df.columns] # 列名标准化 if '权益' not in df.columns: raise ValueError(f"'{sheet_name}' 中缺少 '权益' 列") return df.sort_values('权益', ascending=False).head(10).to_dict('records') # 将阻塞的读取操作放入线程池中执行 return await loop.run_in_executor(executor, _sync_read) @app.route('/') async def show_leaderboard(): """异步渲染排行榜视图""" base_dir = os.path.dirname(os.path.abspath(__file__)) excel_path = os.path.join(base_dir, "rank_data_1.xlsx") try: # 并发读取两个工作 left_task = read_excel_sheet(excel_path, 'Sheet1') right_task = read_excel_sheet(excel_path, 'Sheet2') left_data, right_data = await asyncio.gather(left_task, right_task) except Exception as e: print(f"❌ 数据处理错误: {e}") left_data = right_data = [] return render_template('Moban.html', left_data=left_data, right_data=right_data) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True, use_reloader=False) 要求改进代码实现,权益相同的并列名次展示
最新发布
09-18
<think> 我们有一个需求:改进包含Flask和HTML的代码,实现权益相同的项并列名次展示。代码包含异步读取Excel数据和排行榜渲染功能。 根据引用[^1][^2][^3]提供的背景,我们了解到系统使用Flask作为后端,前端使用HTML/JavaScript,并且有数据交互。 思路: 1. 在读取Excel数据并计算权益后,我们需要对数据进行排名。排名规则:权益相同的并列同一名次,然后跳过相同数量的名次。 例如:权益为[100, 90, 90, 80] -> 排名为[1,2,2,4](注意:这里并列第二后,下一个是第四名,因为第三名被跳过了) 但通常的并列排名规则有两种常见方式: a. 密集排名(1,2,2,3) b. 标准竞赛排名(1,2,2,4) -> 我们这里要求的就是标准竞赛排名。 实际上,标准竞赛排名就是:排名等于比当前分数高的不同分数的个数+1。 2. 具体步骤: - 将数据按照权益降序排序。 - 遍历排序后的列,为每个对象计算排名。 - 初始化一个变量`rank`示当前排名,以及一个变量`prev_equity`示前一个的权益值。 - 遍历时,如果当前权益等于前一个权益,则当前排名等于前一个的排名(并列);否则,当前排名等于当前遍历的索引+1(但是注意:由于有并列的情况,我们不能直接用索引,因为索引是连续的,而排名可能跳跃)。 但实际上,我们可以这样想:当前排名 = 当前是第几个不同的权益(按从大到小顺序)?但是相同权益的排名应该相同,并且下一个不同的权益的排名应该是当前排名+相同权益的个数(而不是+1)?不对。 另一种方法: 我们可以先记录每个权益对应的排名,然后给每个数据分配排名。 步骤: a. 对权益列进行去重、降序排序。 b. 然后每个权益的排名就是它在去重排序列中的索引+1。 但是这样分配后,每条数据的排名就是该数据权益对应的那个排名(即相同权益的排名相同)。 然而,我们要求的排名是:如果有并列,那么并列之后的第一个不同权益的排名是:当前排名+并列的人数。 例如:100,90,90,80 -> 去重排序后:[100,90,80] -> 对应的排名分别是1,2,3?但是我们希望80的排名是4?不对。 因此,采用标准竞赛排名的计算方法: 排名 = 1 对于第一个数据,排名=1 对于后续数据: 如果当前权益等于上一个权益,则当前排名等于上一个的排名 否则,当前排名 = 当前数据在排序数组中的位置索引+1(注意:第一个位置索引是0,排名是1) 但是这样计算出来的排名是:100->1, 90->2, 90->2, 80->4? 但是80的位置索引是3,排名应该是4。 为什么80的排名是4?因为有两个90,所以80前面有3个人(100和两个90),所以80排第4。 所以,我们可以这样:当权益不等于上一个时,当前排名 = 当前数据在排序数组中的位置索引+1(位置索引从0开始,加1后就是该位置的排名,也就是比它大的不同值的个数+1)。相等则继承上一个的排名。 因此算法: sorted_list = sorted(data, key=lambda x: x['equity'], reverse=True) rank = 1 prev_equity = None for idx, item in enumerate(sorted_list): if prev_equity is None: item['rank'] = rank else: if item['equity'] == prev_equity: item['rank'] = rank # 这里rank还是上一个的排名 else: # 注意:因为已经排序,所以当前权益一定小于上一个(如果存在上一个且不等于) # 那么当前排名应该是 idx+1 (因为从0开始,第0名是1,第1名是2,但是注意:如果有并列,上一个并列的多个数据占据了多个位置,所以当前索引就是已经处理的数据个数,所以排名应该是索引+1) rank = idx+1 item['rank'] = rank prev_equity = item['equity'] # 但是这样,对于第一个数据,我们用rank=1,然后prev_equity设置为第一个的权益。 # 然后第二个数据开始,判断是否相等,相等就继承,不等就设置rank为当前索引+1。 例子: 数据: [100,90,90,80] 索引0: 100 -> rank=1, 设置prev=100 索引1: 90 -> 不等于100,所以rank=索引1+1=2, 设置prev=90 索引2: 90 -> 等于prev(90),所以rank=2(不变),prev还是90 索引3: 80 -> 不等于prev(90),所以rank=索引3+1=4 -> 符合要求。 3. 在后端处理数据时,在计算完每个用户的权益后,按照上述方法计算排名(并列名次),然后将排名数据一并返回给前端。 4. 前端展示时,直接使用后端返回的排名进行展示。 具体代码改进步骤: 后端(Flask)部分: 假设我们有一个异步读取Excel数据的函数,返回一个包含用户信息的列,每个用户信息是一个字典,包含至少用户ID、姓名、权益等字段。 然后我们计算排名,并将排名添加到每个用户的字典中。 例如: users = get_users_from_excel() # 返回列,每个元素如{'id':1, 'name':'Alice', 'equity':100} # 按照权益降序排序 sorted_users = sorted(users, key=lambda x: x['equity'], reverse=True) # 计算排名 if sorted_users: sorted_users[0]['rank'] = 1 prev_equity = sorted_users[0]['equity'] for idx in range(1, len(sorted_users)): if sorted_users[idx]['equity'] == prev_equity: sorted_users[idx]['rank'] = sorted_users[idx-1]['rank'] else: # 注意:这里排名应该是当前下标+1(因为下标从0开始,所以第idx个数据实际是第idx+1个位置) sorted_users[idx]['rank'] = idx+1 prev_equity = sorted_users[idx]['equity'] # 现在sorted_users中每个用户都有'rank'字段 然后,将这个列传递给前端模板。 前端(HTML/JavaScript)部分: 我们假设使用模板引擎(如Jinja2)来渲染排名榜。 在模板中,我们可以遍历这个用户列,并显示每个用户的排名、姓名、权益等。 例如: <table> <thead> <tr> <th>排名</th> <th>姓名</th> <th>权益</th> </tr> </thead> <tbody> {% for user in users %} <tr> <td>{{ user.rank }}</td> <td>{{ user.name }}</td> <td>{{ user.equity }}</td> </tr> {% endfor %} </tbody> </table> 注意:因为我们已经在后端计算好了排名,所以前端直接展示即可。 但是,如果数据是前端通过AJAX异步加载的,那么我们在后端API中就应该返回包含排名的数据。前端在接收到数据后,再进行渲染。 根据引用[^2]中的代码,我们有一个路由返回渲染的模板,并传递了数据(htmlcode)。但是,如果我们的排行榜数据是通过AJAX加载的,那么我们应该单独写一个API返回JSON格式的数据,然后前端用JavaScript动态生成格。 假设我们采用第二种方式(异步加载): 后端: @app.route('/api/ranking') def get_ranking(): users = get_users_from_excel() # 读取Excel数据 # 计算排名(同上) sorted_users = ... # 排序并添加排名字段 return jsonify(ranking=sorted_users) 前端: 使用JavaScript发送请求获取数据,然后动态生成格。 根据需求,代码包含异步读取Excel数据和排行榜渲染功能。因此,我们假设已经有一个异步加载的机制。 因此,我们需要改进的是: 1. 在后端数据准备阶段,计算排名(采用上述并列排名算法)。 2. 将排名数据返回给前端(无论是通过模板渲染还是JSON返回)。 由于引用[^2]中示例代码是使用模板渲染的(return render_template('main.html', htmlcode=htmlcode)),我们假设我们的排行榜是在服务器端渲染的。 但是,考虑到引用[^1]中提到“前端通过AJAX与后端API进行交互”,也有可能使用的是AJAX。所以我们需要明确。 根据问题描述,我们需要改进包含Flask和HTML的代码。为了通用性,我们分别给出两种情况: 情况1:服务器端渲染(SSR) 在后端处理数据时,计算排名,然后将用户列(包含排名)传递给模板。 在模板中循环展示。 情况2:客户端渲染(AJAX) 后端提供一个API,返回包含排名的用户列(JSON格式)。 前端通过AJAX请求这个API,获取数据后,用JavaScript动态生成格,并展示排名。 由于问题没有明确,我们可以选择其中一种或者两种都提供。但根据引用[^2]中的代码,它使用的是模板渲染(传递htmlcode)。不过,我们注意到传递的是htmlcode,这意味着在生成htmlcode的时候,我们就应该已经计算好排名。 但是,为了更符合现代Web应用,我们也可以改为使用AJAX的方式。下面我们分别给出两种方式的改进代码。 由于问题描述中要求包含异步读取Excel数据(可能是后端异步读取?)和排行榜渲染功能。这里我们理解为后端读取Excel数据(同步或异步)后进行处理,然后渲染。 我们假设原始代码中已经可以从Excel读取数据并传递给模板,但没有排名计算(或者原有的排名计算没有处理并列)。 因此,我们只需要在读取Excel数据后,进行排名计算(按照上述算法),然后将计算好排名的数据传递给模板即可。 具体代码改进(后端): 假设原始函数如下(类似于引用[^2]中的index函数): @app.route('/') def index(): # 读取Excel数据,得到用户列 users = get_users_from_excel() # 假设这个函数已经存在 # 原始可能没有排名计算,或者原有排名计算是简单的顺序排名(没有处理并列) # 所以我们要改进这里:计算并列排名 # 按照权益降序排序 sorted_users = sorted(users, key=lambda x: x['equity'], reverse=True) if sorted_users: # 初始化第一个的排名 sorted_users[0]['rank'] = 1 prev_equity = sorted_users[0]['equity'] for i in range(1, len(sorted_users)): if sorted_users[i]['equity'] == prev_equity: # 与前一个权益相同,则排名与前一个相同 sorted_users[i]['rank'] = sorted_users[i-1]['rank'] else: # 权益不同,排名为当前索引+1(注意:索引从0开始,所以第i个元素的索引就是i) sorted_users[i]['rank'] = i+1 prev_equity = sorted_users[i]['equity'] else: sorted_users = [] # 现在sorted_users中每个用户都有rank字段 # 然后将这个列传递给模板 return render_template('ranking.html', users=sorted_users) 前端模板(ranking.html): <table> <tr> <th>排名</th> <th>姓名</th> <th>权益</th> </tr> {% for user in users %} <tr> <td>{{ user.rank }}</td> <td>{{ user.name }}</td> <td>{{ user.equity }}</td> </tr> {% endfor %} </table> 如果原始数据已经是排序的,但是排名计算不对,我们只需要在传递到模板前重新计算排名即可。 如果原始数据不是排序的,那么排序后计算排名,然后传递给模板。 注意:上述排名算法的时间复杂度是O(n log n)(主要是排序),后面的遍历是O(n),所以可以接受。 另外,如果权益值可能是浮点数,注意比较相等时可能要考虑精度问题(比如用abs(a-b)<1e-6),但如果是整数则可以直接比较。 如果权益值可能出现None(空值)的情况,我们需要在排序前处理(比如将None视为0或者排除),这里假设数据完整。 如果有多个权益相同,我们希望排名相同,并且后续的排名跳过这些并列占用的名次,上述算法满足要求。 如果使用AJAX方式: 后端API返回JSON: @app.route('/api/ranking') def get_ranking(): users = get_users_from_excel() # 同样的排名计算 sorted_users = sorted(users, key=lambda x: x['equity'], reverse=True) if sorted_users: sorted_users[0]['rank'] = 1 prev_equity = sorted_users[0]['equity'] for i in range(1, len(sorted_users)): if sorted_users[i]['equity'] == prev_equity: sorted_users[i]['rank'] = sorted_users[i-1]['rank'] else: sorted_users[i]['rank'] = i+1 prev_equity = sorted_users[i]['equity'] else: sorted_users = [] return jsonify(users=sorted_users) 前端用JavaScript请求这个API,然后用JavaScript动态生成格。 由于问题中要求包含异步读取Excel数据,如果是指前端异步加载,那么第二种方式更符合。 因此,我们根据引用[^1]中提到“前端通过AJAX与后端API进行交互”,我们采用第二种方式(AJAX)来改进。 具体前后端代码: 后端(Flask): 增加一个API端点:/api/ranking,返回JSON数据。 前端HTML/JavaScript: 在页面加载时,使用AJAX请求/api/ranking,获取数据后,动态生成格。 示例前端代码(在HTML文件中): <table id="ranking-table"> <thead> <tr> <th>排名</th> <th>姓名</th> <th>权益</th> </tr> </thead> <tbody id="ranking-body"> <!-- 数据由JavaScript动态填充 --> </tbody> </table> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script> $(document).ready(function(){ $.ajax({ url: '/api/ranking', type: 'GET', success: function(data) { let tbody = $('#ranking-body'); tbody.empty(); data.users.forEach(function(user) { let row = $('<tr>'); row.append($('<td>').text(user.rank)); row.append($('<td>').text(user.name)); row.append($('<td>').text(user.equity)); tbody.append(row); }); }, error: function() { alert('加载排行榜失败'); } }); }); </script> 这样,我们就实现了并列排名的展示,并且数据是异步加载的。 总结: 后端: 读取Excel数据 -> 按权益降序排序 -> 计算并列排名(标准竞赛排名)-> 返回JSON 前端: 页面加载时,通过AJAX请求排名数据,然后动态生成格。 注意:如果数据量很大,可能需要考虑分页,但题目没有要求,暂不考虑。 以上代码改进实现了需求。 </think> 以下是改进代码的方案,实现权益相同的项并列名次展示: ### 后端Flask代码改进 ```python from flask import Flask, jsonify, request import pandas as pd import numpy as np app = Flask(__name__) def calculate_ranking(data): """计算并列排名(标准竞赛排名)""" data = data.sort_values(by='权益', ascending=False) data['原始索引'] = range(len(data)) # 保留原始索引用于还原顺序 # 计算并列排名 data['排名'] = data['权益'].rank(method='min', ascending=False).astype(int) # 分组处理并列项 grouped = data.groupby('排名') result = [] current_rank = 1 for _, group in grouped: group_size = len(group) # 并列项使用相同排名 group['最终排名'] = f"{current_rank}-{current_rank + group_size - 1}" if group_size > 1 else str(current_rank) result.append(group) current_rank += group_size # 跳过已占用的名次 return pd.concat(result).sort_values('原始索引') @app.route('/api/get_ranking') def get_ranking(): # 异步读取Excel数据(示例路径) try: df = pd.read_excel("data.xlsx") # 计算排名(添加并列处理) ranked_df = calculate_ranking(df) # 换为前端需要的格式 result = ranked_df.to_dict(orient='records') return jsonify({"status": "success", "data": result}) except Exception as e: return jsonify({"status": "error", "message": str(e)}), 500 if __name__ == '__main__': app.run(debug=True) ``` ### 前端HTML/JavaScript改进 ```html <!DOCTYPE html> <html> <head> <title>排行榜</title> <style> .same-rank { background-color: #f0f8ff; } </style> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> </head> <body> <h1>权益排行榜</h1> <table id="ranking-table" border="1"> <thead> <tr> <th>排名</th> <th>名称</th> <th>权益值</th> </tr> </thead> <tbody> <!-- 数据由JavaScript动态填充 --> </tbody> </table> <script> $(document).ready(function() { // 异步获取排行榜数据 $.ajax({ url: '/api/get_ranking', type: 'GET', success: function(response) { if(response.status === "success") { renderRanking(response.data); } else { alert("加载数据失败: " + response.message); } }, error: function() { alert("服务器请求失败"); } }); function renderRanking(data) { const tbody = $('#ranking-table tbody'); tbody.empty(); let currentRank = null; data.forEach((item, index) => { // 添加并列项的特殊样式 const isSameRank = (index > 0 && item.排名 === data[index-1].排名); const rowClass = isSameRank ? 'same-rank' : ''; const row = $('<tr>').addClass(rowClass); row.append($('<td>').text(item.最终排名)); row.append($('<td>').text(item.名称)); row.append($('<td>').text(item.权益)); tbody.append(row); // 更新当前排名 currentRank = item.排名; }); } }); </script> </body> </html> ``` ### 关键技术说明 1. **并列排名算法**: - 使用Pandas的`rank(method='min')`计算基础排名 - 分组处理相同权益值:`1-3`示并列第1名(3人并列) - 排名计算示例:权益值 `[100, 100, 90, 80]` → 排名 `["1-2", "1-2", "3", "4"]` 2. **数据流改进**: ```mermaid graph LR A[Excel数据] --> B[Flask后端] B --> C{排名计算} C -->|并列处理| D[JSON数据] D --> E[前端AJAX] E --> F[动态渲染] ``` 3. **用户体验优化**: - 相同排名项目添加浅蓝色背景 `.same-rank` - 并列排名显示为区间(如"1-3") - 异步加载防止页面卡顿 ### 部署注意事项 1. 确保Excel文件路径正确(或改为数据库读取) 2. 安装依赖:`pip install pandas flask openpyxl` 3. 跨域处理(若前端分离部署需添加CORS支持) 4. 大型数据集建议添加分页加载功能 > **示例Excel数据结构**: > | 名称 | 权益 | > |------|------| > | 张三 | 100 | > | 李四 | 100 | > | 王五 | 90 | 此方案实现了: - ✓ 权益相同项目并列展示 - ✓ 异步数据加载 - ✓ 可视化排名区分 - ✓ 前后端分离架构 [^1][^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值