跳跃表
跳跃表的引入
无论是数组还是链表在插入新数据的时候,都会存在性能问题。排好序的数据,如果使用数组,插入新数据的方式如下:
如果要插入数据3,首先要知道这个数据应该插入的位置。使用二分查找可以最快定位,这一步时间复杂度是O(logN)。插入过程中,原数组中所有大于3的商品都要右移,这一步时间复杂度是O(N)。所以总体时间复杂度是O(N)。
如果使用链表,插入新的数据方式如下:
如果要插入数据3,首先要知道这个商品应该插入的位置。链表无法使用二分查找,智能和原链表中的节点逐一比较大小来确定位置。这一步的时间复杂度是O(N)。插入的过程倒是很容易,直接改变节点指针的目标,时间复杂度O(1)。因此总体的时间复杂度也是O(N)。
跳跃表原理
基本概念
如果对于有几十万的数据集合来说,这两种方法显然都太慢了。为此可以引入跳跃表(skiplist),跳跃表是一种基于有序链表的扩展,简称跳表。利用类似索引的思想,提取出链表中的部分关键节点。比如给定一个长度是7的有序链表,节点值依次是1->2->3->4->5->6->7->8。那么我们可以取出所有值为奇数的节点作为关键点。
此时如果插入一个值是4的新节点,不再需要和原节点8,7,6,5,3逐一比较,只需要比较关键节点7,5,3。
确定了新节点在关键点中的位置(3和5之间),就可以回到原链表,迅速定位到对应的位置插入(同样是3和5之间)。
现在节点数目少,优化效果还不明显,如果链表中1w甚至10w个节点,比较次数就整整减少了一半。这样做虽然增加50%的额外的空间,但是性能提高了一倍。不过可以进一步思考,既然已经提取出了一层关键节点作为索引,那我们可以从索引中进一步提取,提出一层索引的索引。
当节点足够多的时候,我们不止能提出两层索引,还可以向更高层次提取,保证每一层是上一层节点数的一半。至于提取的极限,则是同一层只有两个节点的时候,因为一个节点没有比较的意义。这样的多层链表结构就是所谓的跳跃表。
插入节点
有一个问题需要注意,当大量的新节点通过逐层比较,最终插入到原链表之后,上层的索引节点会渐渐变得不够用。这时候需要从新节点当中选取一部分提到上一层,可是究竟应该提拔谁、忽略谁?关于这一点,跳跃表的设计者采用了一种有趣的办法:抛硬币。也就是随机决定新节点是否提拔,每向上提拔一层的几率是50%。仍然借用刚才的例子,假如值为9新节点插入原链表
使用抛硬币的方法的原因是因为跳跃表删除和添加的节点是不可预测道德,很难用一种有效的算法来保证跳跃表的索引部分始终均匀。随机抛硬币的方法虽然不能保证索引绝对均匀分布,却可以让大体趋于均匀。总结一下,跳跃表插入节点的流程有以下几步:
- 新节点和各层索引节点逐一比较,确定原链表的插入位置。O(logN)
- 把索引插入到原链表。O(1)
- 利用抛硬币的随机方式,决定新节点是否提升为上一级索引。结果为"正"则提升并继续抛硬币,结果为"负"则停止。O(logN)
总体上,跳跃表插入操作的时间复杂度是O(logN),而这种数据结构所占空间是2N,即空间复杂度是O(N)
删除节点
删除操作比较简单,只要在索引层找到要删除的节点,然后顺藤摸瓜,删除每一层的相同节点即可。如果某一层索引在删除后只剩下一个节点,那么整个一层就可以干掉了。还用原来的例子,如果要删除的节点值是5
总结一下跳跃表删除节点的步骤:
- 自上而下,查找第一次出现节点的索引,并逐层找到每一层对应的节点。O(logN)
- 删除每一层查找到的节点,如果该层只剩下1个节点,删除整个一层(原链表除外)。O(logN)
总体上,跳跃表删除操作的时间复杂度是O(logN)。
跳跃表java实现
定义节点
public class SkipListNode implements Comparable { private int value; private SkipListNode next = null; private SkipListNode downNext = null; @Override protected void finalize() throws Throwable { super.finalize(); System.out.printf("123"); } public int getValue() { return value; } public void setValue(int value) { this.value = value; } public SkipListNode getNext() { return next; } public void setNext(SkipListNode next) { this.next = next; } public SkipListNode getDownNext() { return downNext; } public void setDownNext(SkipListNode downNext) { this.downNext = downNext; } @Override public int compareTo(Object o) { return this.value > ((SkipListNode)o).value ? 1 : -1; } }
插入、删除操作类
import java.util.Random; public class SkipList { public int level = 0; public SkipListNode top = null; public SkipList() { this(4); } //跳跃表的初始化 public SkipList(int level) { this.level = level; SkipListNode skipListNode = null; SkipListNode temp = top; SkipListNode tempDown = null; SkipListNode tempNextDown = null; int tempLevel = level; while(tempLevel -- != 0){ skipListNode = createNode(Integer.MIN_VALUE); temp = skipListNode; skipListNode = createNode(Integer.MAX_VALUE); temp.setNext(skipListNode); temp.setDownNext(tempDown); temp.getNext().setDownNext(tempNextDown); tempDown = temp; tempNextDown = temp.getNext(); } top = temp; } //随机产生数k,k层下的都需要将值插入 public int randomLevel(){ int k = 1; while(new Random().nextInt()%2 == 0){ k ++; } return k > level ? level : k; } //查找 public SkipListNode find(int value){ SkipListNode node = top; while(true){ while(node.getNext().getValue() < value){ node = node.getNext(); } if(node.getDownNext() == null){ //返回要查找的节点的前一个节点 return node; } node = node.getDownNext(); } } //删除一个节点 public boolean delete(int value){ int tempLevel = level; SkipListNode skipListNode = top; SkipListNode temp = skipListNode; boolean flag = false; while(tempLevel -- != 0){ while(temp.getNext().getValue() < value){ temp = temp.getNext(); } if(temp.getNext().getValue() == value){ temp.setNext(temp.getNext().getNext()); flag = true; } temp = skipListNode.getDownNext(); } return flag; } //插入一个节点 public void insert(int value){ SkipListNode skipListNode = null; int k = randomLevel(); SkipListNode temp = top; int tempLevel = level; SkipListNode tempNode = null; //当在第n行插入后,在第n - 1行插入时需要将第n行backTempNode的DownNext域指向第n - 1的域 SkipListNode backTempNode = null; int flag = 1; while(tempLevel-- != k){ temp = temp.getDownNext(); } tempLevel++; tempNode = temp; //小于k层的都需要进行插入 while(tempLevel-- != 0){ //在第k层寻找要插入的位置 while(tempNode.getNext().getValue() < value){ tempNode = tempNode.getNext(); } skipListNode = createNode(value); //如果是顶层 if(flag != 1){ backTempNode.setDownNext(skipListNode); } backTempNode = skipListNode; skipListNode.setNext(tempNode.getNext()); tempNode.setNext(skipListNode); flag = 0; tempNode = tempNode.getDownNext(); } } //创建一个节点 private SkipListNode createNode(int value){ SkipListNode node = new SkipListNode(); node.setValue(value); return node; } }
测试类
import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; public class SkipListTest { public static void main(String[] args) { long startTime = 0; long endTime = 0; SkipList skipList = new SkipList(12); Set<Integer> set = new HashSet(); List<Integer> list = new LinkedList<>(); //测试跳跃表性能 startTime = System.currentTimeMillis(); for (int i = 1; i < 1000; i++) { skipList.insert(i); } endTime = System.currentTimeMillis(); System.out.printf("createSkipList:%d\n", endTime - startTime); startTime = System.currentTimeMillis(); System.out.printf("find(555):%d\n", skipList.find(55555).getNext().getValue()); endTime = System.currentTimeMillis(); System.out.printf("skipListFindTime:%d\n\n", endTime - startTime); //测试LinkedList性能 startTime = System.currentTimeMillis(); for (int i = 1; i < 100; i++) { list.add(i); } endTime = System.currentTimeMillis(); System.out.printf("createList:%d\n", endTime - startTime); startTime = System.currentTimeMillis(); System.out.printf("find(555):%b\n", list.contains(55555)); endTime = System.currentTimeMillis(); System.out.printf("linkedListFindTime:%d\n\n", endTime - startTime); //测试hashSet性能 startTime = System.currentTimeMillis(); for (int i = 1; i < 100000; i++) { set.add(i); } endTime = System.currentTimeMillis(); System.out.printf("createSet:%d\n", endTime - startTime); startTime = System.currentTimeMillis(); System.out.printf("find(555):%b\n", set.contains(55555)); endTime = System.currentTimeMillis(); System.out.printf("hashSetFindTime:%d\n", endTime - startTime); } }
Redis中的有序集合
Redis使用跳跃表作为有序集合键的的底层实现,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时Redis就会使用跳跃表来作为有序集合键的底层实现。Redis只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。
Redis为什么要使用跳跃表而不是红黑树?
其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。对于按照区间查找数据这个操作,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。当然,Redis 之所以用跳表来实现有序集合,还有其他原因,比如,跳表更容易代码实现。虽然跳表的实现也不简单,但比起红黑树来说还是好懂、好写多了,而简单就意味着可读性好,不容易出错。还有,跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。不过,跳表也不能完全替代红黑树。因为红黑树比跳表的出现要早一些,很多编程语言中的 Map 类型都是通过红黑树来实现的。我们做业务开发的时候,直接拿来用就可以了,不用费劲自己去实现一个红黑树,但是跳表并没有一个现成的实现,所以在开发中,如果你想使用跳表,必须要自己实现。
基本命令
zadd key score element #如果元素存在,则改变它的分数
> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
实战
排行榜功能在很多应用都是普遍存在的,例如音乐排行榜、电影排行榜、文章排行榜、热门视频等等。类似这种场景就可以使用有序集合来实现。
可以使用zadd去添加元素和初始分数,然后使用zincrby实现分数的更新,使用zrem将一些元素删除榜外,使用zrangebyscore获取一定范围分数的榜单等等。
那么这里最核心的就是分数具体代表什么,例如最新榜单可以使用timeStamp作为分数,销售量可以使用saleCount,关注量使用followCount。然后使用相关的API进行业务操作,也可以对多个集合进行汇总根据一定的规则作为类似综合排序的结果。