java实现跳表(SkipList)

JAVA herman 953浏览 0评论
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog,发送下载链接帮助你免费下载!
本博客日IP超过1800,PV 2600 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog,之前的微信号好友位已满,备注:返现
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

前面的《从摔鸡蛋问题讲跳表的原理》中,我简单说了下跳表的相关知识,限于篇幅没有实现代码,只是列举了算法和思路。本文使用java来实现一个跳表算法。

直接上代码,如下:

package com.xttblog.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 //www.xttblog.com
 *
 *  在这里我也从网上查了一些关于跳表的资料,发现了跳表的两种数据结构的设计
 *  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次耗时更少吧。
 *  //www.xttblog.com
 */
public class SkipList {
    private class Node{
        //存储的数据,当然也可以用泛型
        int data;
        //leavel层数组
        Node[] forword;//www.xttblog.com
        //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()方法量身打造的
 //www.xttblog.com
    private Random rd = new Random();
 //www.xttblog.com
    public SkipList(){
        //新建header和nil
        header = new Node(Integer.MIN_VALUE, DEFAULT_LEAVEL);
        nil = new Node(Integer.MAX_VALUE, DEFAULT_LEAVEL); //这里把它的高度设为1是为了后面的遍历
        //把header指向下一个节点,也就是nil
        for(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;
    }
 //www.xttblog.com
    /**
     * 快速排序
     * @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的索引i
                while( i < end && data[++ i] < base);
                //从右边开始查找直到找到小于base的索引j
                while( 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;
    }
 //www.xttblog.com
    //遍历跳表  限第一层
    public Map<integer, node="">> lookUp(){
        Map<integer, node="">> map = new HashMap<integer, 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;
    }
 //www.xttblog.com
    public void show(Map<integer, 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]

以上就是相关的具体实现,如果你有疑问,欢迎留言讨论!

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加QQ1群:135430763(2000人群已满),QQ2群:454796847(已满),QQ3群:187424846(已满)。QQ群进群密码:xttblog,想加微信群的朋友,之前的微信号好友已满,请加博主新的微信号:xttblog,备注:“xttblog”,添加博主微信拉你进群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作可添加助理微信进行沟通!

本文原文出处:业余草: » java实现跳表(SkipList)