Java基础 - 跳表(SkipList)

来源:互联网 发布:linux maven 打包war 编辑:程序博客网 时间:2024/05/29 19:47

跳表(skiplist)是一个非常优秀的数据结构,实现简单,插入、删除、查找的复杂度均为O(logN)。LevelDB的核心数据结构是用跳表实现的,redis的sorted set数据结构也是有跳表实现的。

跳表同时是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,这样也就是说跳表的插入和删除的工作是比较简单的。下面来研究一下跳表的核心思想:

下面给出一个完整的跳表的图示:

首先从考虑一个有序表开始:


从该有序表中搜索元素 < 23, 43, 59 > ,需要比较的次数分别为 < 2, 4, 6 >,总共比较的次数为 2 + 4 + 6 = 12 次。有没有优化的算法吗? 链表是有序的,但不能使用二分查找。类似二叉搜索树,我们把一些节点提取出来,作为索引。得到如下结构:

这里我们把 < 14, 34, 50, 72 > 提取出来作为一级索引,这样搜索的时候就可以减少比较次数了。我们还可以再从一级索引提取一些元素出来,作为二级索引,变成如下结构:

   

     这里元素不多,体现不出优势,如果元素足够多,这种索引结构就能体现出优势来了。

跳表

下面的结构是就是跳表:

 其中 -1 表示 INT_MIN, 链表的最小值,1 表示 INT_MAX,链表的最大值。

跳表具有如下性质:

(1) 由很多层结构组成

(2) 每一层都是一个有序的链表

(3) 最底层(Level 1)的链表包含所有元素

(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。

(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。


 跳表的搜索

例子:查找元素 117

(1) 比较 21, 比 21 大,往后面找

(2) 比较 37, 比 37大,比链表最大值小,从 37 的下面一层开始找

(3) 比较 71, 比 71 大,比链表最大值小,从 71 的下面一层开始找

(4) 比较 85, 比 85 大,从后面找

(5) 比较 117, 等于 117, 找到了节点。


跳表的插入:

先确定该元素要占据的层数 K(采用丢硬币的方式,这完全是随机的)然后在 Level 1 ... Level K 各个层的链表都插入元素。

例子:插入 119, K = 2

 

如果 K 大于链表的层数,则要添加新的层。

例子:插入 119, K = 4


跳表的删除

在各个层中找到包含 x 的节点,使用标准的 delete from list 方法删除该节点。

例子:删除 71



丢硬币决定 K

插入元素的时候,元素所占有的层数完全是随机的。相当与做一次丢硬币的实验,如果遇到正面,继续丢,遇到反面,则停止,用实验中丢硬币的次数 K 作为元素占有的层数。


下面是代码实现:
package com.yc.list;import java.util.ArrayList;import java.util.Arrays;import java.util.HashMap;import java.util.Iterator;import java.util.List;import java.util.Map;import java.util.Random;/** * @author wb * *在这里我也从网上查了一些关于跳表的资料,发现了跳表的两种数据结构的设计 *1. class Node{ *int data; //用于存放数据 *Node next;//用于指向同一层的下一Node *Node down;//用于指向在数据相同的下一层Node *} *2. class Node{ *int data; *Node[] forword; //看图可以说明一切,用于指向可以到达的节点 *//随机高度数 k决定节点的高度 h,节点的高度 h决定节点中forword的长度; *} * *比较上面第一种和第二种数据结构:我选择了第二种,因为我目前觉得 *例如:新添加一个节点,节点的高度为10,节点数据为2,采用第一种结构,它必定要new 10个Node,然后还得存储相同的数据2, *虽然down和next会有不一样,但还是浪费。如果是第二种结构,只需new 一个Node,然后Node中的forward长度设为10,就这样。 *虽然JVM在创建对象时对对象中的引用和数组是不一样的(next和down是纯粹的引用,而forward是引用数组),但我相信new一次应该比new *10次耗时更少吧。 * */public class SkipList {private class Node{//存储的数据,当然也可以用泛型int data;//leavel层数组Node[] forword;//int index; //这个变量是专门为了后面的输出好看添加的。//这个完全没有必要为了好看就去做,因为一旦这样做了,那么在数据跳表中有了相当多的数据节点N时,很不幸(也就//是在最坏的情况下),如果再添加一个新的元素,而这个元素恰好在header后面的第一个位置,这会导致后面的所有的//的节点都要去修改一次index域,从而要去遍历整个跳表的最底层。大大的糟糕透顶!public Node(int data, int leavel){this.data = data;this.forword = new Node[leavel];//this.index = index;}public String toString(){return "[data="+data+", height="+forword.length+"] -->";}}//因为我知道跳表是一个非常优秀的以空间换时间的数据结构设计,//且其性能在插入、删除甚至要比红黑树高。//所以我会毫不吝啬的挥霍内存private static final int DEFAULT_LEAVEL = 3;//开始标志,(我打算设置其数据项为Integer.MIN_VALUE)private Node header;//结束标志,(我打算设置其数据项为Integer.MAX_VALUE)private Node nil;//当前节点位置private Node current;// 这一变量是为下面的add_tail()方法量身打造的private Random rd = new Random();public SkipList(){//新建header和nilheader = new Node(Integer.MIN_VALUE, DEFAULT_LEAVEL);nil = new Node(Integer.MAX_VALUE, DEFAULT_LEAVEL); //这里把它的高度设为1是为了后面的遍历//把header指向下一个节点,也就是nilfor(int i = DEFAULT_LEAVEL - 1; i >= 0; i --){header.forword[i] = nil;}current = header;}/** * 将指定数组转换成跳表 * @param data */public void addArrayToSkipList(int[] data){//先将data数组进行排序有两种方法://1.用Arrays类的sort方法//2.自己写一个快速排序算法quickSort(data);//System.out.println( Arrays.toString(data));//for(int d : data){//因为数组已经有序//所以选择尾插法add_tail(d);}}/** * 将指定数据添加到跳表 * @param data */public void add(int data){Node preN = find(data);if(preN.data != data){ //找到相同的数据的节点不存入跳表int k = leavel();Node node = new Node(data, k);//找新节点node在跳表中的最终位置的后一个位置节点。注意这里的后一个位置节点是指如下:// node1 --> node2  (node1 就是node2的后一个节点)dealForAdd(preN, node, preN.forword[0], k);}}/** * 如果存在 data, 返回 data 所在的节点,  * 否则返回 data 的前驱节点  * @param data * @return */private Node find(int data){Node current = header;int n = current.forword.length - 1;while(true){  //为什么要while(true)写个死循环呢 ?while(n >= 0 && current.data < data){if(current.forword[n].data < data){current = current.forword[n];}else if(current.forword[n].data > data){n -= 1;}else{return current.forword[n];}}return current;}}/** * 删除节点 * @param data */public void delete(int data){Node del = find(data);if(del.data == data){ //确定找到的节点不是它的前驱节点delForDelete(del);}}private void delForDelete(Node node) {int h = node.forword.length;for(int i = h - 1; i >= 0; i --){Node current = header;while(current.forword[i] != node){current = current.forword[i];}current.forword[i] = node.forword[i];}node = null;}/** * 链尾添加 * @param data */public void add_tail(int data) {Node preN = find(data);if(preN.data != data){int k = leavel();Node node = new Node(data, k);dealForAdd(current, node, nil, k);current = node;}}/** * 添加节点是对链表的相关处理 * @param preNode:待插节点前驱节点 * @param node:待插节点 * @param succNode:待插节点后继节点 * @param k */private void dealForAdd(Node preNode, Node node, Node succNode, int k){ //其实这个方法里的参数 k 有点多余。int l = header.forword.length;int h = preNode.forword.length;if(k <= h){//如果新添加的节点高度不高于相邻的后一个节点高度for(int j = k - 1; j >= 0 ; j --){node.forword[j] = preNode.forword[j];preNode.forword[j] = node;}}else{//if(l < k){ //如果header的高度(forward的长度)比 k 小header.forword = Arrays.copyOf(header.forword, k); //暂时就这么写吧,更好地处理机制没想到nil.forword = Arrays.copyOf(nil.forword, k);for(int i = k - 1; i >= l; i --){header.forword[i] = node;node.forword[i] = nil;}}Node tmp;for(int m = l < k ? l - 1 : k - 1; m >= h; m --){tmp = header;while(tmp.forword[m] != null && tmp.forword[m] != succNode){tmp = tmp.forword[m];}node.forword[m] = tmp.forword[m];tmp.forword[m] = node;}for(int n = h - 1; n >= 0; n --){node.forword[n] = preNode.forword[n];preNode.forword[n] = node;}}}/** * 随机获取高度,(相当于抛硬币连续出现正面的次数) * @return */private int leavel(){int k = 1;while(rd.nextInt(2) == 1){k ++;}return k;}/** * 快速排序 * @param data */private void quickSort(int[] data){quickSortUtil(data, 0, data.length - 1);}private void quickSortUtil(int[] data, int start, int end){if(start < end){//以第一个元素为分界线int base = data[start];int i = start;int j = end + 1;//该轮次while(true){//从左边开始查找直到找到大于base的索引iwhile( i < end && data[++ i] < base);//从右边开始查找直到找到小于base的索引jwhile( j > start && data[-- j] > base);if(i < j){swap(data, i, j);}else{break;}}//将分界值与 j 互换位置。swap(data, start, j);//左递归quickSortUtil(data, start, j - 1);//右递归quickSortUtil(data, j + 1, end);}}private void swap(int[] data, int i, int j){int t = data[i];data[i] = data[j];data[j] = t;}//遍历跳表  限第一层public Map<Integer, List<Node>> lookUp(){Map<Integer, List<Node>> map = new HashMap<Integer, List<Node>>();List<Node> nodes;for(int i = 0; i < header.forword.length; i ++){nodes = new ArrayList<Node>();for(Node current = header; current != null; current = current.forword[i]){nodes.add(current);}map.put(i,nodes);}return map;}public void show(Map<Integer, List<Node>> map){for(int i = map.size() - 1; i >= 0; i --){List<Node> list = map.get(i);StringBuffer sb = new StringBuffer("第"+i+"层:");for(Iterator<Node> it = list.iterator(); it.hasNext();){sb.append(it.next().toString());}System.out.println(sb.substring(0,sb.toString().lastIndexOf("-->")));}}public static void main(String[] args) {SkipList list = new SkipList();int[] data = {4, 8, 16, 10, 14};list.addArrayToSkipList(data);list.add(12);list.add(12);list.add(18);list.show(list.lookUp());System.out.println("在本次跳表中查找15的节点或前驱节点为:" + list.find(15));System.out.println("在本次跳表中查找12的节点或前驱节点为:" + list.find(12) + "\n");list.delete(12);System.out.println("删除节点值为12后的跳表为:");list.show(list.lookUp());}}

某次(注意它是随机的,所以是某次)测试结果为:
第2层:[data=-2147483648, height=3] -->[data=2147483647, height=3] 第1层:[data=-2147483648, height=3] -->[data=8, height=2] -->[data=14, height=2] -->[data=16, height=2] -->[data=2147483647, height=3] 第0层:[data=-2147483648, height=3] -->[data=4, height=1] -->[data=8, height=2] -->[data=10, height=1] -->[data=12, height=1] -->[data=14, height=2] -->[data=16, height=2] -->[data=18, height=1] -->[data=2147483647, height=3] 在本次跳表中查找15的节点或前驱节点为:[data=14, height=2] -->在本次跳表中查找12的节点或前驱节点为:[data=12, height=1] -->删除节点值为12后的跳表为:第2层:[data=-2147483648, height=3] -->[data=2147483647, height=3] 第1层:[data=-2147483648, height=3] -->[data=8, height=2] -->[data=14, height=2] -->[data=16, height=2] -->[data=2147483647, height=3] 第0层:[data=-2147483648, height=3] -->[data=4, height=1] -->[data=8, height=2] -->[data=10, height=1] -->[data=14, height=2] -->[data=16, height=2] -->[data=18, height=1] -->[data=2147483647, height=3] 
由于个人能力有限,不能很好把结果展示给大家,望见谅。
下面是一些学习地址:


http://blog.csdn.net/tsaid/article/details/7010985

http://www.cnblogs.com/xuqiang/archive/2011/05/22/2053516.html


http://www.cnblogs.com/liuhao/archive/2012/07/26/2610218.html


0 0
原创粉丝点击