您好,欢迎来到三六零分类信息网!老站,搜索引擎当天收录,欢迎发信息
免费发信息
三六零分类信息网 > 滨州分类信息网,免费分类信息发布

跳跃表---用简单的方式实现有序集合

2020/5/28 10:59:34发布190次查看

一、简介
有序集合通常采用红黑树实现,但是红黑树结构复杂,涉及到节点的旋转等操作,而且删除节点也会变得很复杂。在著名的nosql数据库redis中,采用跳表的方式代替红黑树实现了有序集合
从有序链表入手
一个简单的链表
class node{
   node next;
   int val;

其结构如图:
由于链表的顺序结构,从链表中查找一个值必须
遍历整个链表,时间复杂度为o(n),例如我们向查找7,即node4,需要4次查找
再加几个指针,更快的查找
如何避免每次查找数据都从表头按顺序遍历?我们可以设想,如果node1有一个直接指向node3,那么我们对7的查找就只需要3次
最终的结构,跳跃表
我们将原有的next指针变更为一个指针数组,这样就允许一个节点有多个节点指向后面的节点,注意这里每一个节点的next[0]指针始终指向null,原因在稍后说明。这个新的结构就是跳跃表了,跳跃表中的操作始终从head节点的最高指针开始
例如查找7:
跳跃表节结构代码为:
/** * 跳跃表 * 查找,插入,删除 都为 o(logn) * 空间复杂度为o(n) */public class skiplist {    //头节点    private node head;    //记录节点leve的最大值    private int maxlevel;    //节点数量    private int size;    //抛硬币实验的概率    private static final double probability = 0.5;    //节点代码    private static class node{        int val;        //相比链表,这里next指针为一组,而不是一个        arraylist<node> nextnodes = null;//arraylist与c++中vector相似        public node(int val){            this.val = val;            nextnodes = new arraylist<>();        }    }    public skiplist(){        this.head = new node(integer.min_value);        //初始化跳表的0层始终为null        this.head.nextnodes.add(null);//add()与c++中push_back()相似        this.maxlevel = 0;        this.size = 0;    }    //添加    public void add(int val){    }    //查找    public boolean contains(int val){    }    //删除    public void remove(int val){    }}
那么还有最后一个问题,如何决定每一个节点next数组的大小才能保证olog(n)的时间复杂度?答案是建立每个节点时,都进行抛硬币实验,如果硬币是反面,next数组就“增高”,直到抛出正面的硬币,用代码实现就是:
//确定新节点的层数int level = 1;//next指针数组的大小用level表示while(math.random() < probability){    level ++;}二、实现
查找
结合两个例子分析查找相关的代码
查找7:
再例如查找5:
public boolean contains(int val){        if(size == 0){            return false;        }        //level表示当前遍历到的层数        //每次查找都从头节点的最高层开始        int level = maxlevel;        node curr = head;        while(level > 0){            if(curr.nextnodes.get(level) != null){                //同一层下个节点值小于目标值,向右遍历                if(curr.nextnodes.get(level).val < val){                    curr = curr.nextnodes.get(level);                //同一层下个节点值大于目标值,向下遍历(层数减一)                }else if(curr.nextnodes.get(level).val > val){                    level --;                }else if(curr.nextnodes.get(level).val == val){                    return true;                }            //本层遍历到了末尾(指向null),向下遍历(层数减一)            }else{                level --;            }        }        return false;    }添加
添加过程相对复杂,大概分为一下几个步骤:
进行抛硬币实验,确定新节点的层数,如果层数比头节点层数大,则还需要加高头节点
从头节点最高层开始,寻找新节点最高层插入的位置
层数降低,因为新节点每一层都需要与前后节点相连
public void add(int val){        if(contains(val)){            return;        }        //1. 确定新节点的层数        int level = 1;        while(math.random() < probability){            level ++;        }        //如果新节点的层数大于最高层数,先将头节点增高        if(level > maxlevel){            int increment = level - maxlevel;            while(increment-- > 0){                head.nextnodes.add(null);            }            maxlevel = level;        }        //创建新节点        node newnode = new node(val);        //2. 寻找新节点最高层的插入位置        node curr = findinsertionoftoplevel(val, level);        while (level > 0){            //新节点与后边的节点连接            if(curr.nextnodes.get(level) != null){                newnode.nextnodes.add(0, curr.nextnodes.get(level));            }else{                newnode.nextnodes.add(0, null);            }            //当前节点的next指向新节点            curr.nextnodes.set(level, newnode);            //3. 层数降低,新节点的每层都要与前后节点相连            level --;            //寻找下一层需要连接的地方            while(curr.nextnodes.get(level) != null && curr.nextnodes.get(level).val < val){                curr = curr.nextnodes.get(level);            }        }        //最后,保证节点的0层始终为null        newnode.nextnodes.add(0, null);        size ++;    }    private node findinsertionoftoplevel(int newval, int level){        int currlevel = maxlevel;        node curr = head;        while(currlevel >= level){            //尝试向右寻找            if(curr.nextnodes.get(currlevel) != null && curr.nextnodes.get(currlevel).val < newval){                curr = curr.nextnodes.get(currlevel);            }else{                //向下寻找                currlevel --;            }        }        return curr;    }删除
删除就是插入的逆过程,分为两个步骤:
从最高层开始,寻找需要删除的节点
找到要删除的节点的前驱节点,断开被删节点每一层与前后节点连接的指针
public void remove(int val){        if(contains(val)){            //一定存在,每次都从head的最高层开始遍历            node curr = head;            int level = maxlevel;            //1. 寻找要删除的节点            while (level > 0){                if(curr.nextnodes.get(level) != null){                    //向右寻找                    if(curr.nextnodes.get(level).val < val) {                        curr = curr.nextnodes.get(level);                    }else if(curr.nextnodes.get(level).val == val){ //找到了                        curr = curr.nextnodes.get(level);                        break;                    }else{                        //本层没有找到,到下一层找                        level --;                    }                }else{                    //本层没有找到,到下一层找                    level --;                }            }            //2. 寻找要删除的节点的前驱节点,每一层都要断开与被删除节点的连接            while(level > 0){                node pre = head;                //向右寻找                while(pre.nextnodes.get(level).val != val){                    pre = pre.nextnodes.get(level);                }                pre.nextnodes.set(level, curr.nextnodes.get(level));                level --;            }            size --;        }    }遍历
我们注意到,跳表的节点至少为一层,next[1]指针始终指向比它大的下一个节点,所以遍历跳跃表和遍历链表一样简单,如图:
代码与遍历链表相同,这里不在赘述。
同时,还可以结合查找的相关代码,轻松找出比某个值大的所有节点
三、双向跳跃表
还记得始终指向null的next[0]指针吗?如果上述实现的跳跃表的基础上,将每一个next[0]指针指向前驱节点,并添加一个尾节点,就是双向跳表了,方便做反向遍历,例如找出比某个值小的所有节点
注意尾节点始终只有第0层
双向跳跃表实现与跳跃表基本类似,只是增加了反向指针,具体实现见双向跳跃表
作者:好吃懒做贪玩东
编辑:西瓜媛

滨州分类信息网,免费分类信息发布

VIP推荐

免费发布信息,免费发布B2B信息网站平台 - 三六零分类信息网 沪ICP备09012988号-2
企业名录