如何理解“跳表”?
跳表=链表+多级索引结构
用跳表查询到底有多快?
跳表是不是很浪费内存?
高效的动态插入和删除
插入
删除
跳表索引动态更新
跳跃表的实现步骤分析
思路
先讨论插入,我们先看理想的跳跃表结构,L2层的元素个数是L1层元素个数的1/2,L3层的元素个数是L2层的元素个数的1/2,以此类推。从这里,我们可以想到,只要在插入时尽量保证上一层的元素个数是下一层元素的1/2,我们的跳跃表就能成为理想的跳跃表。那么怎么样才能在插入时保证上一层元素个数是下一层元素个数的1/2呢?很简单,抛硬币就能解决了!假设元素X要插入跳跃表,很显然,L1层肯定要插入X。那么L2层要不要插入X呢?我们希望上层元素个数是下层元素个数的1/2,所以我们有1/2的概率希望X插入L2层,那么抛一下硬币吧,正面就插入,反面就不插入。那么L3到底要不要插入X呢?相对于L2层,我们还是希望1/2的概率插入,那么继续抛硬币吧!以此类推,元素X插入第n层的概率是(1/2)的n次。这样,我们能在跳跃表中插入一个元素了。
在此还是以上图为例:跳跃表的初试状态如下图,表中没有一个元素:
如果我们要插入元素2,首先是在底部插入元素2,如下图:
然后我们抛硬币,结果是正面,那么我们要将2插入到L2层,如下图:
继续抛硬币,结果是反面,那么元素2的插入操作就停止了,插入后的表结构就是上图所示。接下来,我们插入元素33,跟元素2的插入一样,现在L1层插入33,如下图:
然后抛硬币,结果是反面,那么元素33的插入操作就结束了,插入后的表结构就是上图所示。接下来,我们插入元素55,首先在L1插入55,插入后如下图:
然后抛硬币,结果是正面,那么L2层需要插入55,如下图:
继续抛硬币,结果又是正面,那么L3层需要插入55,如下图:
以此类推,我们插入剩余的元素。当然因为规模小,结果很可能不是一个理想的跳跃表。但是如果元素个数n的规模很大,学过概率论的同学都知道,最终的表结构肯定非常接近于理想跳跃表。
代码实现
采用随机数生成的方式来获取新元素插入的最高层数。我们先估摸一下n的规模,然后定义跳跃表的最大层数maxLevel,那么底层,也就是第0层,元素是一定要插入的,概率为1;最高层,也就是maxLevel层,元素插入的概率为1/2^maxLevel。
我们先随机生成一个范围为0~2^maxLevel-1的一个整数r。那么元素r小于2^(maxLevel-1)的概率为1/2,r小于2^(maxLevel-2)的概率为1/4,……,r小于2的概率为1/2^(maxLevel-1),r小于1的概率为1/2^maxLevel。
举例,假设maxLevel为4,那么r的范围为0~15,则r小于8的概率为1/2,r小于4的概率为1/4,r小于2的概率为1/8,r小于1的概率为1/16。1/16正好是maxLevel层插入元素的概率,1/8正好是maxLevel层插入的概率,以此类推。
通过这样的分析,我们可以先比较r和1,如果r<1,那么元素就要插入到maxLevel层以下;否则再比较r和2,如果r<2,那么元素就要插入到maxLevel-1层以下;再比较r和4,如果r<4,那么元素就要插入到maxLevel-2层以下……如果r>2^(maxLevel - 1),那么元素就只要插入在底层即可。
package arithmetic.com.ty.binary; import java.util.ArrayList; import java.util.List; import java.util.Random; /** * 实现跳跃表:能够对递增链表实现logN的查询时间 */ public class SkipList<T> { // 约束整个跳表的最大层级。2^6(2的6次方) private static final int MAX_LEVEL = 1 << 6; // 跳跃表数据结构 private SkipNode<T> top; // 跳表默认层级数 private int level = 0; // 用于产生随机数的Random对象 private Random random = new Random(); public SkipList() { // 创建默认初始高度的跳跃表 this(4); } // 跳表的初始化 public SkipList(int level) { this.level = level; int i = level; SkipNode<T> temp = null; SkipNode<T> prev = null; while (i-- != 0) { /** * 从下往上进行初始化。假设level为3,每个层级初始化一个值为null,分值为最小double的初始节点 * +---+ * | 3 | * +---+ * +---+ * | 2 | * +---+ * +---+ * | 1 | * +---+ */ temp = new SkipNode<T>(null, Double.MIN_VALUE); temp.down = prev; prev = temp; } // 头节点 top = temp; } /** * 产生节点的高度。使用抛硬币 * * @return */ private int getRandomLevel() { int lev = 1; /** * 核心:初始层级为1,那么此算法需要保证为2的概率为1/2,为3的概率为1/4,如此循环下去。。。 * 这样可以很好的保证第2层节点数量是第1层的两倍,第3层节点数量是第2层节点数量的两倍。第n层节点数量是第n-1层节点数量的两倍。 * 因为如果每层节点数量过多,那么就跟单链表的查询一样了,影响性能。 */ while (random.nextInt() % 2 == 0) { lev++; } return lev > MAX_LEVEL ? MAX_LEVEL : lev; } /** * 存放一个数据到跳表中 * @param score * @param val */ public void put(double score, T val) { // 若cur不为空,表示当前score值的节点存在 SkipNode<T> t = top, cur = null; /** * path存的是每一层需要插入新节点的前驱节点的集合。 * 假设对于三层层级的跳表来说,需要插入分值为4的,那么path里的最终数据为[3,3,1]。 * 第一个3代表是第一层的3,第二个3代表是第二层的3,第三个1代表是第三层的1,代表新节点将要插在这些节点的后面,也可以说这些节点是新节点的前驱节点。 * level3 1 * | * level2 1---->3---->6 * | | | * level1 1->2->3->5->6 */ List<SkipNode<T>> path = new ArrayList<>(); //当头节点不为空的时候,一直轮询 while (t != null) { //若存在分值相同的,直接退出轮询。若分值相同,则会做覆盖处理 if (t.score == score) { cur = t; // 表示存在该值的点,表示需要更新该节点 break; } //如果右节点不存在,那么开始往下找,这一层也就结束,因此需要记录当前节点到path中 if (t.next == null) { path.add(t); //当down节点存在时,此次循环结束,继续往下找;否则退出轮询 if (t.down != null) { t = t.down; continue; } else { break; } } //如果右节点的分数大于新节点分值,那么此层查找结束,继续查down节点,并记录当前层的当前节点 if (t.next.score > score) { path.add(t); if (t.down == null) { break; } t = t.down; } else t = t.next; } /** * 如果存在相同分值的,直接更改down这条竖线上所有数据就好。例如分值为3,那么改3这条竖线上所有的值就好 * level3 1 * | * level2 1---->3---->6 * | | | * level1 1->2->3->5->6 */ if (cur != null) { while (cur != null) { cur.val = val; cur = cur.down; } } else { // 当前表中不存在score值的节点,需要从下到上插入 int lev = getRandomLevel(); /** * 当翻硬币的层级大于当前层级时,需要更新top这一列的节点数量,同时需要在path中增加这些新的首节点。当小于当前层级时,直接在 */ if (lev > level) { /** * 假如翻硬币的层级为4,那么对于如下跳表 * level3 1 * | * level2 1---->3---->6 * | | | * level1 1->2->3->5->6 * -----变成------ * level4 null * | * level3 1 * | * level2 1---->3---->6 * | | | * level1 1->2->3->5->6 * 新的top头节点为level4的null */ SkipNode<T> temp = null; // 前驱节点现在是top了 SkipNode<T> prev = top; while (level++ != lev) { temp = new SkipNode<T>(null, Double.MIN_VALUE); // 加到path的首部 path.add(0, temp); temp.down = prev; prev = temp; } // 头节点 top = temp; // level长度增加到新的长度 level = lev; } /** * 从后向前遍历path中的每一个节点,在其后面增加一个新的节点 * 注:从第一层开始往上添加。path中的数据是从最高层级添加下来的,因此需要从最后一位取,代表是第一层的新节点的前驱节点 */ SkipNode<T> downTemp = null, temp = null, prev = null; //由于是从path中倒序取数,因此i>level-lev,因为level-1到level-lev之间的举例为lev-1,就是翻硬币翻出来的层数 for (int i = level - 1; i >= level - lev; i--) { temp = new SkipNode<T>(val, score); prev = path.get(i); temp.next = prev.next; prev.next = temp; temp.down = downTemp; downTemp = temp; } } } /** * 查找跳跃表中的一个值 * * @param score * @return */ public T get(double score) { SkipNode<T> t = top; while (t != null) { if (t.score == score) return t.val; if (t.next == null) { if (t.down != null) { t = t.down; continue; } else return null; } if (t.next.score > score) { t = t.down; } else t = t.next; } return null; } /** * 根据score的值来删除节点。 * * @param score */ public void delete(double score) { // 1,查找到节点列的第一个节点的前驱 SkipNode<T> t = top; while (t != null) { if (t.next == null) { t = t.down; continue; } if (t.next.score == score) {// 在这里说明找到了该删除的节点 t.next = t.next.next; t = t.down;// 删除当前节点后,还需要继续查找之后需要删除的节点 continue; } if (t.next.score > score) t = t.down; else t = t.next; } } @Override public String toString() { StringBuilder sb = new StringBuilder(); SkipNode<T> t = top, next = null; while (t != null) { next = t; while (next != null) { sb.append(next.score + " "); next = next.next; } sb.append(" "); t = t.down; } return sb.toString(); } /** * 跳跃表的节点的构成 * * @param <E> */ private static class SkipNode<E> { // 存储的数据 E val; /** * 跳跃表按照这个分数值进行从小到大排序。 注:通过引入分值,以分值大小进行排序,这样跳表中存储的数据可以是对象等复杂类型数据 */ double score; // next指针,down指针。一个指向右边元素,一个指向下层元素 SkipNode<E> next, down; SkipNode(E val, double score) { this.val = val; this.score = score; } } public static void main(String[] args) { SkipList<String> list = new SkipList<>(); list.put(1.0, "1.0"); System.out.println(list); list.put(2.0, "2.0"); System.out.println(list); list.put(3.0, "3.0"); System.out.println(list); list.put(4.0, "4.0"); System.out.println(list); list.put(5.0, "5.0"); System.out.println(list); list.delete(3.0); System.out.println(list); System.out.println("查找4.0" + list.get(4.0)); } }
运行结果:
当然每次运行结果层数都可能会不一样,这也正是翻硬币的作用所在。
4.9E-324是double最小值,也就是我们初始化节点的默认value值。