让链表的元素查询接近线性时间【跳跃表】

让链表的元素查询接近线性时间【跳跃表】

无意中读到一篇文章AVL树,红黑树,B树,B+树,Trie树,对于目前使用的高级的数据结构进行了一些总结,以前学习数据结构的时候多少都有过一些的接触,大概的意思还是能听懂,下面有一个部分场景使用跳表来代替红黑树???跳表 为啥 redis 使用跳表(skiplist)而不是使用 red-black?–(如果单纯比较性能,跳跃表和红黑树可以说相差不大,但是加上并发的环境就不一样了,如果要更新数据,跳跃表需要更新的部分就比较少,锁的东西也就比较少,所以不同线程争锁的代价就相对少了,而红黑树有个平衡的过程,牵涉到大量的节点,争锁的代价也就相对较高了。性能也就不如前者了。)

AVL树:

平衡二叉树,一般是用平衡因子差值决定并通过旋转来实现,左右子树树高差不超过1,那么和红黑树比较它是严格的平衡二叉树,平衡条件非常严格(树高差只有1),只要插入或删除不满足上面的条件就要通过旋转来保持平衡。由于旋转是非常耗费时间的。我们可以推出AVL树适合用于插入删除次数比较少,但查找多的情况。

红黑树:(TreeMap)

平衡二叉树,通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍,因而是近似平衡的。所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。用于搜索时,插入删除次数多的情况下我们就用红黑树来取代AVL。(现在部分场景使用跳表来替换红黑树,可搜索“为啥 redis 使用跳表(skiplist)而不是使用 red-black?”)
应用:

著名的linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块
epoll在内核中的实现,
Java的TreeMap实现

B树(数据库系统)

漫画算法:什么是 B 树
它们特点是一样的,是多路查找树,一般用于数据库系统中,为什么,因为它们分支多层数少呗,都知道磁盘IO是非常耗时的,而像大量数据存储在磁盘中所以我们要有效的减少磁盘IO次数避免磁盘频繁的查找。

B+树(数据库索引和文件系统)

是B树的变种树,有n棵子树的节点中含有n个关键字,每个关键字不保存数据,只用来索引,数据都保存在叶子节点。是为文件系统而生的。
我们知道要获取磁盘上数据,必须先通过磁盘移动臂移动到数据所在的柱面,然后找到指定盘面,接着旋转盘面找到数据所在的磁道,最后对数据进行读写。磁盘IO代价主要花费在查找所需的柱面上,树的深度过大会造成磁盘IO频繁读写。根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,B树可以有多个子女,从几十到上千,可以降低树的高度。

Trie树:(搜索引擎搜索)

又名单词查找树,一种树形结构,常用来操作字符串。它是不同字符串的相同前缀只保存一份。相对直接保存字符串肯定是节省空间的,但是它保存大量字符串时会很耗费内存(是内存)。类似的有前缀树(prefix tree),后缀树(suffix tree),radix tree(patricia tree, compact prefix tree),crit-bit tree(解决耗费内存问题),以及前面说的double array trie。简单的补充下我了解应用前缀树:字符串快速检索,字符串排序,最长公共前缀,自动匹配前缀显示后缀。后缀树:查找字符串s1在s2中,字符串s1在s2中出现的次数,字符串s1,s2最长公共部分,最长回文串。radix tree:linux内核,nginx。

要了解跳跃表

感觉大学好像没有接触过这样的数据结构,查看了很多的参考的资料,其实发现这个玩意其实就是一个有序的多级链表的结构,针对题目让链表的元素查询接近线性时间,这样相对于查询链表的速度将提高了一个数量级,而且代码也不是非常的复杂。
有很多有趣的博客介绍跳跃表:
漫画算法:什么是跳跃表?
跳跃表-原理及Java实现
java 实现跳跃表
一个简单的跳跃表(Skip List)的Java实现
JAVA SkipList 跳表 的原理和使用例子
【算法导论33】跳跃表(Skip list)原理与java实现 这里面还有个算法的视频连接http://open.163.com/movie/2010/12/7/S/M6UTT5U0I_M6V2TTJ7S.html 专门讲解跳跃表,博客中还有关于JDK中实现的ConcurrentSkipListMap等跳跃表的使用选型进行了介绍
Java中的跳跃表
Java API中提供了支持并发操作的跳跃表ConcurrentSkipListSet和ConcurrentSkipListMap。下面摘录Java多线程(四)之ConcurrentSkipListMap深入分析中的一些结论。

有序的情况下:
在非多线程的情况下,应当尽量使用TreeMap(红黑树实现)。
对于并发性相对较低的并行程序可以使用Collections.synchronizedSortedMap将TreeMap进行包装,也可以提供较好的效率。但是对于高并发程序,应当使用ConcurrentSkipListMap

无序情况下:
并发程度低,数据量大时,ConcurrentHashMap 存取远大于ConcurrentSkipListMap。
数据量一定,并发程度高时,ConcurrentSkipListMap比ConcurrentHashMap效率更高。

仔细的看看上面推荐的博客,对于性能实现原理等的分析的非常的不错,对于这样的数据结构,我们还是有必要了解的,其实就是一种变种的链表(多级链表,分别在上下左右四个方向都有指向的指针),对于让链表的元素查询接近线性的时间,这个是一个不错的方案。所谓的跳跃表,实际的意思就是对于查询的时候可以适当的跳跃过部分的链表数据达到快速查询的目的详细介绍

参考的实现跳跃表

http://blog.csdn.net/brillianteagle/article/details/52206261

public class SkipListNode {
    public int key;
    public T value;
    public SkipListNode up, down, left, right; // 上下左右 四个指针

    public static final int HEAD_KEY = Integer.MIN_VALUE; // 负无穷
    public static final int  TAIL_KEY = Integer.MAX_VALUE; // 正无穷
    public SkipListNode(int k,T v) {
        key = k;
        value = v;
    }
    public int getKey() {
        return key;
    }
    public void setKey(int key) {
        this.key = key;
    }
    public T getValue() {
        return value;
    }
    public void setValue(T value) {
        this.value = value;
    }
    public boolean equals(Object o) {
        if (this==o) {
            return true;
        }
        if (o==null) {
            return false;
        }
        if (!(o instanceof SkipListNode)) {
            return false;
        }
        SkipListNode ent;
        try {
            ent = (SkipListNode)  o; // 检测类型
        } catch (ClassCastException ex) {
            return false;
        }
        return (ent.getKey() == key) && (ent.getValue() == value);
    }
    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return "key-value:"+key+"-"+value;
    }
}
/**
 * http://blog.csdn.net/brillianteagle/article/details/52206261
 */
public class SkipList<T> {
    private SkipListNode head, tail;
    private int nodes;//节点总数
    private int listLevel;//层数
    private Random random;// 用于投掷硬币
    private static final double PROBABILITY = 0.5;//向上提升一个的概率

    public SkipList() {
        random = new Random();
        clear();
    }

    /**
     * 清空跳跃表
     */
    public void clear() {
        head = new SkipListNode(SkipListNode.HEAD_KEY, null);
        tail = new SkipListNode(SkipListNode.TAIL_KEY, null);
        horizontalLink(head, tail);
        listLevel = 0;
        nodes = 0;
    }

    public boolean isEmpty() {
        return nodes == 0;
    }

    public int size() {
        return nodes;
    }

    /**
     * 在最下面一层,找到要插入的位置前面的那个key
     */
    private SkipListNode findNode(int key) {
        SkipListNode p = head;
        while (true) {
            while (p.right.key != SkipListNode.TAIL_KEY && p.right.key <= key) {
                p = p.right;
            }
            if (p.down != null) {
                p = p.down;
            } else {
                break;
            }

        }
        return p;
    }

    /**
     * 查找是否存储key,存在则返回该节点,否则返回null
     */
    public SkipListNode search(int key) {
        SkipListNode p = findNode(key);
        if (key == p.getKey()) {
            return p;
        } else {
            return null;
        }
    }

    /**
     * 向跳跃表中添加key-value
     */
    public void put(int k, T v) {
        SkipListNode p = findNode(k);
        //如果key值相同,替换原来的vaule即可结束
        if (k == p.getKey()) {
            p.value = v;
            return;
        }
        SkipListNode q = new SkipListNode(k, v);
        backLink(p, q);
        int currentLevel = 0;//当前所在的层级是0
        //抛硬币
        while (random.nextDouble() < PROBABILITY) {
            //如果超出了高度,需要重新建一个顶层
            if (currentLevel >= listLevel) {
                listLevel++;
                SkipListNode p1 = new SkipListNode(SkipListNode.HEAD_KEY, null);
                SkipListNode p2 = new SkipListNode(SkipListNode.TAIL_KEY, null);
                horizontalLink(p1, p2);
                vertiacallLink(p1, head);
                vertiacallLink(p2, tail);
                head = p1;
                tail = p2;
            }
            //将p移动到上一层
            while (p.up == null) {
                p = p.left;
            }
            p = p.up;

            SkipListNode e = new SkipListNode(k, null);//只保存key就ok
            backLink(p, e);//将e插入到p的后面
            vertiacallLink(e, q);//将e和q上下连接
            q = e;
            currentLevel++;
        }
        nodes++;//层数递增
    }

    //node1后面插入node2
    private void backLink(SkipListNode node1, SkipListNode node2) {
        node2.left = node1;
        node2.right = node1.right;
        node1.right.left = node2;
        node1.right = node2;
    }

    /**
     * 水平双向连接
     */
    private void horizontalLink(SkipListNode node1, SkipListNode node2) {
        node1.right = node2;
        node2.left = node1;
    }

    /**
     * 垂直双向连接
     */
    private void vertiacallLink(SkipListNode node1, SkipListNode node2) {
        node1.down = node2;
        node2.up = node1;
    }

    /**
     * 打印出原始数据
     */
    @Override
    public String toString() {
        if (isEmpty()) {
            return "跳跃表为空!";
        }
        StringBuilder builder = new StringBuilder();
        SkipListNode p = head;
        while (p.down != null) {
            p = p.down;
        }

        while (p.left != null) {
            p = p.left;
        }
        if (p.right != null) {
            p = p.right;
        }
        // 查找最下面的数据就可以了,其他的层级主要是为了构造结构
        while (p.right != null) {
            builder.append(p);
            builder.append("\n");
            p = p.right;
        }

        return builder.toString();
    }

}
public class Test {
    public static void main(String[] args) {
        SkipList list=new SkipList();
        System.out.println(list);
        list.put(2, "a");
        list.put(1, "b");
        list.put(3, "c");
        list.put(1, "d");//测试同一个key值
        System.out.println(list);
        System.out.println(list.size());
    }
}

这个实现了增删查改一个简单的跳跃表(Skip List)的Java实现

总结

这样的数据结构的变种很厉害,平时要多注意结累理解这样的数据结构的处理方式,真正需要使用的时候可以有选择性的去使用。回答问题:让链表的元素查询接近线性时间-跳跃表 使用空间来换取时间。

你可能感兴趣的:(算法,跳跃表)