zoukankan      html  css  js  c++  java
  • 数据结构

    1.结构

    单链表和向量实现难度低,维护容易却查找效率低,AVL树、红黑树等高级数据结构效率高但较为复杂,难以维护。

    SkipList的实现较红黑树简单得多,而且同时保证了查询和维护等操作平均仅需O(logn)时间。

    不难看出,跳表的结构大致可以被描述为:带索引的有序的链表

    底层的链表存储了所有数据,而上层的链表则作为索引存在。

    普通单链表节点的指针域中仅有一个next指向后继元素,双向链表中增加了一个前驱。

    而在跳表中,一个节点中可以有4个指针,分别指向前驱、后继、上邻、下邻。

    2.SkipListNode

    根据跳表的定义很容易设计出这样一个类

    key:关键码

    value:数据域

    pre,next,up,down:分别指向前后上下的节点

    public class SkipListNode<T> {
    
        public int key;
    
        public T value;
    
        /** 前驱、后继 */
        public SkipListNode<T> pre, next;
    
        /** 上邻、下邻 */
        public SkipListNode<T> up, down;
    
        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;
        }
    
        @Override
        public boolean equals(Object object) {
            if (this == object) {
                return true;
            }
            if (object == null) {
                return false;
            }
            if (!(object instanceof SkipListNode<?>)) {
                return false;
            }
            SkipListNode<T> skipListNode;
            try {
                skipListNode = (SkipListNode<T>) object;
            } catch (ClassCastException ex) {
                return false;
            }
            return (skipListNode.getKey() == key) && (skipListNode.getValue() == value);
        }
    
        @Override
        public String toString() {
            return "key-value:" + key + "," + value;
        }
    }

    3.SkipList

    包含了几个重要的操作:查找(get),插入(put),删除(remove),加层(addEmptyLevel)

    查找(get)

    最重要的方法,其他的几个方法都是以查找为基础的。

    实际进行查找操作的方法是findNode。

    查找的过程很清晰,从顶层的头节点开始顺序查找,一旦遇到下一个关键码大于目标关键码(确定了目标在下层的区间),则转入下层查找。

    目标关键码在跳表中不一定存在,则此时findNode返回的将是小于且距离目标关键码的值最近的节点

        /**
         * 如果传入的key值在跳跃表中不存在,则findNode返回跳跃表中key值小于key,并且key值相差最小的底层节点
         * 所以不能用此方法来代替get
         * @param key key
         * @return key值相差最小的底层节点
         */
        public SkipListNode<T> findNode(int key) {
            SkipListNode<T> p = head;
            while (true) {
                while (p.next.key != SkipListNode.TAIL_KEY && p.next.key <= key) {
                    p = p.next;
                }
                //转入下层
                if (p.down != null) {
                    p = p.down;
                } else {
                    break;
                }
            }
            return p;
        }

    因为存在查找失败的情况,所以要给findNode加一层判断,当findNode返回的节点确实等于目标关键码而非最近小于时,才将该节点返回。

        public SkipListNode<T> get(int key) {
            SkipListNode<T> p = findNode(key);
            if (p.key == key) {
                return p;
            }
            return null;
        }

    加层(addEmptyLevel)

    这个没什么好说的,将各个变量初始化,listLevel++

        public void addEmptyLevel() {
            SkipListNode<T> p1 = new SkipListNode<T>(SkipListNode.HEAD_KEY, null);
            SkipListNode<T> p2 = new SkipListNode<T>(SkipListNode.TAIL_KEY, null);
            p1.next = p2;
            p1.down = head;
            p2.pre = p1;
            p2.down = tail;
            head.up = p1;
            tail.up = p2;
            head = p1;
            tail = p2;
            listLevel++;
        }

    插入(put)

    实际的插入操作是由insertNode方法完成的。

    单纯的插入操作其实很简单,insertNode要求传入待插入节点的前驱。

        /**
         * 单层插入
         * @param p 前驱
         * @param q 待插入的节点
         */
        private void insertNode(SkipListNode<T> p, SkipListNode<T> q) {
            q.next = p.next;
            q.pre = p;
            p.next.pre = q;
            p.next = q;
        }

    首先在跳表中findNode,由上可知,findNode的返回值分两种情况:第一种,当key已存在,关键码不能重复,所以终止插入操作;第二种,当key不存在,此时findNode返回最近小于key的节点,正好将这个节点作为待插入节点的前驱,调用insertNode方法即可。

    插入操作进行到这里已经完成了,但插入新节点后,还涉及到比较复杂的升层问题。

    首先考虑SkipList的空间复杂度,SkipList是基于有序链表实现的,不难看出,为了加快查找速度,SkipList为有序链表建立了索引,即以空间换时间,牺牲了一定的空间效率换取了更高的查找效率,这算得上是比较常用的手段。但是,SkipList的层数并非越多越好,索引也不是越密集越好,如果没有一定的策略,在浪费了空间的同时,时间的效率也无法得到保证。

    网上主流的随机策略是升层的概率逐层递减为1/2,根据数学归纳法计算数学期望,总体的消耗量将仅为O(n)。

    (证明见《数据结构与算法分析(C++语言版)第3版》 邓俊辉 P254)

    在jdk实现的ConcurrentSkipListMap中,随机策略有所不同。

        /**
         * 插入
         * put方法有一些需要注意的步骤:
         * 1.如果put的key值在跳跃表中存在,则进行修改操作;
         * 2.如果put的key值在跳跃表中不存在,则需要进行新增节点的操作,并且需要由random随机数决定新加入的节点的高度(最大level);
         * 3.当新添加的节点高度达到跳跃表的最大level,需要添加一个空白层(除了-oo和+oo没有别的节点)
         * @param key key
         * @param value value
         */
        public void put(int key, T value) {
            SkipListNode<T> p = findNode(key);
            //如果已经存在,则终止插入操作
            if (p.key == key) {
                p.value = value;
                return;
            }
    
            SkipListNode<T> q = new SkipListNode<>(key, value);
            insertNode(p, q);
    
            //随机决定是否升层
            int currentLevel = 0;
            while (random.nextDouble() > PROBABILITY) {
                if (currentLevel >= listLevel) {
                    addEmptyLevel();
                }
                while (p.up == null) {
                    System.out.println(p);
                    p = p.pre;
                }
                p = p.up;
                SkipListNode<T> z = new SkipListNode<>(key, null);
                insertNode(p, z);
                z.down = q;
                q.up = z;
                //把指针移到上一层
                q = z;
                currentLevel++;
            }
            size++;
        }

    删除(remove)

    删除操作本质上和普通链表的删除没有什么区别,唯一的不同是要注意其他层的节点要一并删除。

        /**
         * 首先查找到包含key值的节点,将节点从链表中移除,接着如果有更高level的节点,则重复这个操作即可。
         * @param key key
         * @return T(删除的数据)
         */
        public T remove(int key) {
            SkipListNode<T> p = get(key);
            if (p == null) {
                return null;
            }
            T oldV = p.value;
            SkipListNode<T> q;
            while (p != null) {
                q = p.next;
                q.pre = p.pre;
                p.pre.next = q;
                p = p.up;
            }
            return oldV;
        }

    全部代码

    public class SkipList<T> {
    
        private SkipListNode<T> head, tail;
    
        private int size;
        private int listLevel;
    
        private final Random random;
        private static final double PROBABILITY = 0.5;
    
        public SkipList() {
            head = new SkipListNode<T>(SkipListNode.HEAD_KEY, null);
            tail = new SkipListNode<>(SkipListNode.TAIL_KEY, null);
            head.next = tail;
            tail.pre = head;
            size = 0;
            listLevel = 0;
            random = new Random();
        }
    
        /**
         * 根据key查找
         * @param key key
         * @return SkipListNode<T>
         */
        public SkipListNode<T> get(int key) {
            SkipListNode<T> p = findNode(key);
            if (p.key == key) {
                return p;
            }
            return null;
        }
    
        /**
         * 首先查找到包含key值的节点,将节点从链表中移除,接着如果有更高level的节点,则repeat这个操作即可。
         * @param key key
         * @return T(删除的数据)
         */
        public T remove(int key) {
            SkipListNode<T> p = get(key);
            if (p == null) {
                return null;
            }
            T oldV = p.value;
            SkipListNode<T> q;
            while (p != null) {
                q = p.next;
                q.pre = p.pre;
                p.pre.next = q;
                p = p.up;
            }
            return oldV;
        }
    
        /**
         * 插入
         * put方法有一些需要注意的步骤:
         * 1.如果put的key值在跳跃表中存在,则进行修改操作;
         * 2.如果put的key值在跳跃表中不存在,则需要进行新增节点的操作,并且需要由random随机数决定新加入的节点的高度(最大level);
         * 3.当新添加的节点高度达到跳跃表的最大level,需要添加一个空白层(除了-oo和+oo没有别的节点)
         * @param key key
         * @param value value
         */
        public void put(int key, T value) {
            SkipListNode<T> p = findNode(key);
            //如果已经存在,则终止插入操作
            if (p.key == key) {
                p.value = value;
                return;
            }
    
            SkipListNode<T> q = new SkipListNode<>(key, value);
            insertNode(p, q);
    
            //随机决定是否升层
            int currentLevel = 0;
            while (random.nextDouble() > PROBABILITY) {
                if (currentLevel >= listLevel) {
                    addEmptyLevel();
                }
                while (p.up == null) {
                    System.out.println(p);
                    p = p.pre;
                }
                p = p.up;
                SkipListNode<T> z = new SkipListNode<>(key, null);
                insertNode(p, z);
                z.down = q;
                q.up = z;
                //把指针移到上一层
                q = z;
                currentLevel++;
            }
            size++;
        }
    
        /**
         * 如果传入的key值在跳跃表中不存在,则findNode返回跳跃表中key值小于key,并且key值相差最小的底层节点;
         * 所以不能用此方法来代替get
         * @param key key
         * @return key值相差最小的底层节点
         */
        public SkipListNode<T> findNode(int key) {
            SkipListNode<T> p = head;
            while (true) {
                while (p.next.key != SkipListNode.TAIL_KEY && p.next.key <= key) {
                    p = p.next;
                }
                //转入下层
                if (p.down != null) {
                    p = p.down;
                } else {
                    break;
                }
            }
            return p;
        }
    
        public boolean isEmpty() {
            return size == 0;
        }
    
        public int size() {
            return size;
        }
    
        /**
         * 添加新的一层
         */
        public void addEmptyLevel() {
            SkipListNode<T> p1 = new SkipListNode<T>(SkipListNode.HEAD_KEY, null);
            SkipListNode<T> p2 = new SkipListNode<T>(SkipListNode.TAIL_KEY, null);
            p1.next = p2;
            p1.down = head;
            p2.pre = p1;
            p2.down = tail;
            head.up = p1;
            tail.up = p2;
            head = p1;
            tail = p2;
            listLevel++;
        }
    
        /**
         * 单层插入
         * @param p 前驱
         * @param q 待插入的节点
         */
        private void insertNode(SkipListNode<T> p, SkipListNode<T> q) {
            q.next = p.next;
            q.pre = p;
            p.next.pre = q;
            p.next = q;
        }
    
        public int getLevel() {
            return listLevel;
        }
    }
    SkipList.java

    4.java.util.concurrent.ConcurrentSkipListMap

    ConcurrentSkipListMap是线程安全的有序的哈希表,适用于高并发的场景。
    ConcurrentSkipListMap和TreeMap,它们虽然都是有序的哈希表。但是,第一,它们的线程安全机制不同,TreeMap是非线程安全的,而ConcurrentSkipListMap是线程安全的。第二,ConcurrentSkipListMap是通过跳表实现的,而TreeMap是通过红黑树实现的。

    ConcurrentSkipListMap内部实现的跳表主要有三个方法:doGet(查找),doPut(插入),doRemove(删除)

        /**
         * Gets value for key. Almost the same as findNode, but returns
         * the found value (to avoid retries during re-reads)
         *
         * @param key the key
         * @return the value, or null if absent
         */
        private V doGet(Object key) {
            if (key == null)
                throw new NullPointerException();
            Comparator<? super K> cmp = comparator;
            outer: for (;;) {
                for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                    Object v; int c;
                    if (n == null)
                        break outer;
                    Node<K,V> f = n.next;
                    if (n != b.next)                // inconsistent read
                        break;
                    if ((v = n.value) == null) {    // n is deleted
                        n.helpDelete(b, f);
                        break;
                    }
                    if (b.value == null || v == n)  // b is deleted
                        break;
                    if ((c = cpr(cmp, key, n.key)) == 0) {
                        @SuppressWarnings("unchecked") V vv = (V)v;
                        return vv;
                    }
                    if (c < 0)
                        break outer;
                    b = n;
                    n = f;
                }
            }
            return null;
        }
    
        /* ---------------- Insertion -------------- */
    
        /**
         * Main insertion method.  Adds element if not present, or
         * replaces value if present and onlyIfAbsent is false.
         * @param key the key
         * @param value the value that must be associated with key
         * @param onlyIfAbsent if should not insert if already present
         * @return the old value, or null if newly inserted
         */
        private V doPut(K key, V value, boolean onlyIfAbsent) {
            Node<K,V> z;             // added node
            if (key == null)
                throw new NullPointerException();
            Comparator<? super K> cmp = comparator;
            outer: for (;;) {
                for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                    if (n != null) {
                        Object v; int c;
                        Node<K,V> f = n.next;
                        if (n != b.next)               // inconsistent read
                            break;
                        if ((v = n.value) == null) {   // n is deleted
                            n.helpDelete(b, f);
                            break;
                        }
                        if (b.value == null || v == n) // b is deleted
                            break;
                        if ((c = cpr(cmp, key, n.key)) > 0) {
                            b = n;
                            n = f;
                            continue;
                        }
                        if (c == 0) {
                            if (onlyIfAbsent || n.casValue(v, value)) {
                                @SuppressWarnings("unchecked") V vv = (V)v;
                                return vv;
                            }
                            break; // restart if lost race to replace value
                        }
                        // else c < 0; fall through
                    }
    
                    z = new Node<K,V>(key, value, n);
                    if (!b.casNext(n, z))
                        break;         // restart if lost race to append to b
                    break outer;
                }
            }
    
            int rnd = ThreadLocalRandom.nextSecondarySeed();
            if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
                int level = 1, max;
                while (((rnd >>>= 1) & 1) != 0)
                    ++level;
                Index<K,V> idx = null;
                HeadIndex<K,V> h = head;
                if (level <= (max = h.level)) {
                    for (int i = 1; i <= level; ++i)
                        idx = new Index<K,V>(z, idx, null);
                }
                else { // try to grow by one level
                    level = max + 1; // hold in array and later pick the one to use
                    @SuppressWarnings("unchecked")Index<K,V>[] idxs =
                        (Index<K,V>[])new Index<?,?>[level+1];
                    for (int i = 1; i <= level; ++i)
                        idxs[i] = idx = new Index<K,V>(z, idx, null);
                    for (;;) {
                        h = head;
                        int oldLevel = h.level;
                        if (level <= oldLevel) // lost race to add level
                            break;
                        HeadIndex<K,V> newh = h;
                        Node<K,V> oldbase = h.node;
                        for (int j = oldLevel+1; j <= level; ++j)
                            newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                        if (casHead(h, newh)) {
                            h = newh;
                            idx = idxs[level = oldLevel];
                            break;
                        }
                    }
                }
                // find insertion points and splice in
                splice: for (int insertionLevel = level;;) {
                    int j = h.level;
                    for (Index<K,V> q = h, r = q.right, t = idx;;) {
                        if (q == null || t == null)
                            break splice;
                        if (r != null) {
                            Node<K,V> n = r.node;
                            // compare before deletion check avoids needing recheck
                            int c = cpr(cmp, key, n.key);
                            if (n.value == null) {
                                if (!q.unlink(r))
                                    break;
                                r = q.right;
                                continue;
                            }
                            if (c > 0) {
                                q = r;
                                r = r.right;
                                continue;
                            }
                        }
    
                        if (j == insertionLevel) {
                            if (!q.link(r, t))
                                break; // restart
                            if (t.node.value == null) {
                                findNode(key);
                                break splice;
                            }
                            if (--insertionLevel == 0)
                                break splice;
                        }
    
                        if (--j >= insertionLevel && j < level)
                            t = t.down;
                        q = q.down;
                        r = q.right;
                    }
                }
            }
            return null;
        }
    
        /* ---------------- Deletion -------------- */
    
        /**
         * Main deletion method. Locates node, nulls value, appends a
         * deletion marker, unlinks predecessor, removes associated index
         * nodes, and possibly reduces head index level.
         *
         * Index nodes are cleared out simply by calling findPredecessor.
         * which unlinks indexes to deleted nodes found along path to key,
         * which will include the indexes to this node.  This is done
         * unconditionally. We can't check beforehand whether there are
         * index nodes because it might be the case that some or all
         * indexes hadn't been inserted yet for this node during initial
         * search for it, and we'd like to ensure lack of garbage
         * retention, so must call to be sure.
         *
         * @param key the key
         * @param value if non-null, the value that must be
         * associated with key
         * @return the node, or null if not found
         */
        final V doRemove(Object key, Object value) {
            if (key == null)
                throw new NullPointerException();
            Comparator<? super K> cmp = comparator;
            outer: for (;;) {
                for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
                    Object v; int c;
                    if (n == null)
                        break outer;
                    Node<K,V> f = n.next;
                    if (n != b.next)                    // inconsistent read
                        break;
                    if ((v = n.value) == null) {        // n is deleted
                        n.helpDelete(b, f);
                        break;
                    }
                    if (b.value == null || v == n)      // b is deleted
                        break;
                    if ((c = cpr(cmp, key, n.key)) < 0)
                        break outer;
                    if (c > 0) {
                        b = n;
                        n = f;
                        continue;
                    }
                    if (value != null && !value.equals(v))
                        break outer;
                    if (!n.casValue(v, null))
                        break;
                    if (!n.appendMarker(f) || !b.casNext(n, f))
                        findNode(key);                  // retry via findNode
                    else {
                        findPredecessor(key, cmp);      // clean index
                        if (head.right == null)
                            tryReduceLevel();
                    }
                    @SuppressWarnings("unchecked") V vv = (V)v;
                    return vv;
                }
            }
            return null;
        }
    java.util.concurrent.ConcurrentSkipListMap

    实现方法大致相同,值得注意的是其采用的随机策略。

    (rnd & 0x80000001) == 0

    0x80000001展开为二进制为10000000000000000000000000000001,即只有最高位和最低位为1

    排除了负数(负数最高位为1)和奇数(奇数最低位为1),即只有随机数rnd为正偶数的情况下,才会考虑升层。

    升层的次数等于rnd倒数第二位开始的1的位数

            int rnd = ThreadLocalRandom.nextSecondarySeed();
            if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
                int level = 1, max;
                while (((rnd >>>= 1) & 1) != 0)
                    ++level;
                ...
            }

    5.ZSET与SkipList

    Redis提供的五种数据结构之一的ZSET(有序集合),其底层由跳表实现(zskiplist)

    server.h(Redis由C语言实现)

    #define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements */
    #define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */
    /* ZSETs use a specialized version of Skiplists */
    typedef struct zskiplistNode {
        sds ele;
        double score;
        struct zskiplistNode *backward;
        struct zskiplistLevel {
            struct zskiplistNode *forward;
            unsigned long span;
        } level[];
    } zskiplistNode;
    
    typedef struct zskiplist {
        struct zskiplistNode *header, *tail;
        unsigned long length;
        int level;
    } zskiplist;
    
    typedef struct zset {
        dict *dict;
        zskiplist *zsl;
    } zset;

    zskiplist是限高的,其随机升层的策略如下

      /* Returns a random level for the new skiplist node we are going to create.
       * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
       * (both inclusive), with a powerlaw-alike distribution where higher
       * levels are less likely to be returned. */
      int zslRandomLevel(void) {
          int level = 1;
          while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
              level += 1;
          return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
      }

    参考资料:《数据结构与算法分析(C++语言版)第3版》 邓俊辉

          Redis源码阅读

         Java并发集合(二)-ConcurrentSkipListMap分析和使用

          死磕 java集合之ConcurrentSkipListMap源码分析

         skiplist(跳表)的原理及JAVA实现

  • 相关阅读:
    计算机网络 chapter 6 应用层
    计算机网络 chapter 4 网络层
    计算机网络 chapter 2 物理层
    计算机网络 chapter3数据链路层
    计算机网络 chapter 1 概述
    文章
    进程池线程池 协程
    MySQL
    同步锁 死锁与递归锁 信号量 线程queue event事件
    GIL全局解释器
  • 原文地址:https://www.cnblogs.com/CofJus/p/13602564.html
Copyright © 2011-2022 走看看