跳表的基本原理介绍我是从极客学院王争老师的课程中了解到的,由于其实现相比红黑树简单,并且有与之媲美的性能,便想要实现一下,但纸上得来终觉浅,实际编码过程中遇到了不少的困难,希望本文对大家实现跳表有所帮助。
原理
待补充,可先移步至csdn相关博客 ,内容和王争老师文章差别不大
实现
本文采用java语言实现,为了让删除的时间复杂度达到O(1),采用了双链表结构,代码地址在我的Github数据结构专栏 。
节点数据结构
首先说下节点的数据结构代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Node { private Integer data; private Node[] forwards; private Node[] previous; public Node (Integer data, int level) { this .data = data; forwards = new Node [level]; previous = new Node [level]; } @Override public String toString () { return data == null ? "" : String.valueOf(data); } public Node boyNextDoor (int level) { return forwards[level]; } public Node previous (int level) { return previous[level]; } }
可以看到有两个数组,相信一开始很多人和我一样百思不得解两个数组是干什么的,相信大家见到的跳表差不多是这样的
上图红线给出了查找20的路径,一开始我也觉得很好理解,但随即两个问题困扰了我很久
图中有必要每层都存数据域吗,比如图中的10,为什么有5个?一个数据域+N和前后指针就可以了啊
向下的箭头怎么表示? 再来个down指针?
在找跳表相关文章时,看到一位作者引用了跳表的论文,点进去一看有种恍然大悟的感觉,跳表其实应该是这样的
于是我又手绘了一张跳表的结构图(省略了前驱指针),一边画一边思考,发现问题1和问题2解决之后变得很简单
最终得到了上面的代码,而forwards正是红色部分表示的后驱指针,这个后驱指针数组的长度完全取决于它所在的层数,但HEAD节点默认初始化为最大层数
可以看到previous和boyNextDoor(手动斜眼)两个方法提供了获取一个节点在x层上获取前驱和后继节点的能力
初始化及成员变量介绍
成员变量如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 private static int MAX_LEVEL = 16 ;private Node head;private int size;private int height;private Random random = new Random ();
构造函数
1 2 3 4 5 6 7 public SkipList () { head = new Node (null , MAX_LEVEL); height = 1 ; }
公共方法findFirstGreater
在讲解插入,删除,查找之前,先介绍findFirstGreater方法,这是一个简单且重要的公共方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private Node findFirstGreater (int data, Node current, int level) { Node nextDoor = current.boyNextDoor(level); while (nextDoor != null ) { if (data < nextDoor.data) { break ; } current = nextDoor; nextDoor = current.boyNextDoor(level); } return current; }
无论是查找,插入,删除,我们都要找到第一个比data大的节点的前一个节点,假设data=5,链表为 2 3(5) 7, 第一个大于5的是7,则返回3(5),该值<=5
插入
插入的思路是这样的,首先通过随机函数决定节点有多高,当然最高不超过MAX_LEVEL,这个随机算法直接从redis源码里拿过来的,然后在每一层通过findFirstGreater方法找到第一个比data大的节点的前一个节点,剩下的就是双链表的插入了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public void insert (int data) { if (contains(data)) { return ; } int level = randomLevel(); Node newNode = new Node (data, level); if (level > height) { height = level; } level--; Node current = head; while (level >= 0 ) { current = findFirstGreater(data, head, level); Node nextDoor = current.boyNextDoor(level); newNode.forwards[level] = nextDoor; newNode.previous[level] = current; current.forwards[level] = newNode; if (nextDoor != null ) nextDoor.previous[level] = newNode; level--; } size++; }
contains方法
contains的实现很简单,就是调用了查找方法
1 2 3 4 public boolean contains (int data) { Node node = find(data); return node != null && node.data != null && data == node.data; }
随机算法
返回1到MAX_LEVEL之间的随机数,符合正态分布
1 2 3 4 5 6 7 8 9 private int randomLevel () { int newLevel = 1 ; while ((random.nextInt() & 0xFFFF ) < (0xFFFF >> 2 )) { newLevel++; } return (newLevel < MAX_LEVEL) ? newLevel : MAX_LEVEL; }
查找
查找就是我们所熟知的跳表查询方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public Node find (int data) { Node node = head; int level = height - 1 ; while (level >= 0 ) { node = findFirstGreater(data, node, level); level--; } return node; }
删除
删除相比查找,需要判断findFirstGreater返回的节点值等于data才能删,然后就是简单的双链表删除操作,过程也很简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void remove (int data) { if (!contains(data)) { return ; } Node current = head; int level = height - 1 ; while (level >= 0 ) { current = findFirstGreater(data, current, level); if (current != head && current.data == data) { Node previous = current.previous(level); Node nextDoor = current.boyNextDoor(level); System.out.println(previous.data + "-->" + current.data + "-->" + (nextDoor == null ? "" : nextDoor.data)); previous.forwards[level] = nextDoor; if (nextDoor != null ) { nextDoor.previous[level] = previous; } size--; } level--; } }
打印
说实话最耽误时间的反而是这个打印功能,我希望打印出如下形式,这我心目中这是最直观的,但这就要考虑两个节点间的空格数量,看我的代码会比较复杂,但只要自己沉下心来认真想一下,相信大家都能很快有思路
1 2 3 level 2: 29 level 1: 29 57 148 level 0: 2 13 18 29 48 50 51 57 131 132 148
我的方式不一定适合每个人,遇到两个节点只是简单的从level0遍历,大数据量不适合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public void easyPrint () { for (int i = height - 1 ; i >= 0 ; i--) { System.out.print("level " + i + ": " ); Node current = head.forwards[i]; Node next = current.boyNextDoor(i); StringBuilder builder = new StringBuilder (); if (current.data != head.forwards[0 ].data) { builder.append(whiteSpaceHelper(current.data)); } builder.append(current.data); while (next != null ) { builder.append(whiteSpaceHelper(current.data, next.data)).append(next.data); current = current.boyNextDoor(i); next = next.boyNextDoor(i); } System.out.println(builder.toString()); } } private String whiteSpaceHelper (int pre, int next) { Node current = head.boyNextDoor(0 ); while (current != null ) { if (current.data == pre) { break ; } current = current.boyNextDoor(0 ); } Node node = current.boyNextDoor(0 ); int count = 0 ; while (node != null ) { count++; if (node.data == next) { break ; } count += node.data.toString().length(); node = node.boyNextDoor(0 ); } StringBuilder builder = new StringBuilder (); for (int i = 0 ; i < count; i++) { builder.append(" " ); } return builder.toString(); } private String whiteSpaceHelper (int next) { Node node = head.forwards[0 ]; int count = 0 ; while (node != null ) { if (node.data == next) { break ; } count += node.data.toString().length(); count++; node = node.boyNextDoor(0 ); } StringBuilder builder = new StringBuilder (); for (int i = 0 ; i < count; i++) { builder.append(" " ); } return builder.toString(); }
测试用例
依次对跳表的插入,删除,打印查找做测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public static void main (String[] args) { SkipList skipList = new SkipList (); for (int i = 0 ; i < 100 ; i++) { int number = skipList.random.nextInt(1000 ); skipList.insert(number); if (i == 15 ) { skipList.insert(666 ); } } skipList.easyPrint(); skipList.remove(666 ); System.out.println("===================split line=================" ); skipList.easyPrint(); System.out.println(skipList.find(666 )); System.out.println(skipList.find(1024 )); }
回顾
纵观跳表的插入,查找,删除操作,只涉及了简单的查找,双链表的增加,删除节点等基本操作,比起红黑树的实现简单太多,但红黑树在java中有jdk实现,跳表却没有,有些遗憾
跳表与红黑树
跳表与红黑树相比有以下特性
跳表采用的是空间换时间策略,也就是多了两个指针数组,如果对内存空间十分敏感的场景不太适合,即使是redis也是基于大内存+分布式
跳表和红黑树都有O(logN)的时间复杂度,并且跳表更易于维护
跳表适合做区间查找,且十分高效