范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

比红黑树更快的跳表到底是什么数据结构?如何实现?

  前言
  在头条创作了一个月左右的时间,收获了50+粉丝,很是开心,我会把数据结构与算法的文章更新到底,第一次看我文章的同仁如果觉得不错的话就关注一下我哦,你的支持就是我创作的动力。
  时间复杂度和空间复杂度详解(初学者请点击):传送门
  一、什么是跳表
  我们在之前介绍了十分优秀的二分查找算法,但是它只能作用于有序数组上,查找起来比较方便,但是数组中插入和删除元素是比较麻烦的;那么有没有办法让二分查找也能作用于有序链表上,从而达到查找、插入和删除元素都十分的快速呢?
  对于普通的有序列表来说,当然不能实现我们的目标,如下查找的时间复杂度为O(n);
  原始有序单链表
  我们可以基于原始链表建立一个 索引层,比如每两个节点提取一个节点到索引层:
  带一级索引的跳表
  如此,两种数据结构我们查找元素16的比较次数分别为10次和8次,确实能提高查询速度;我们更近一步,再次建立第二级索引:
  带二级索引的跳表 带二级索引的跳表
  此时查找元素16比较的次数只需要比较7次即可;
  如果在大数据量的有序链表中,我们建立很多层索引,使得最高层索引只有两个节点,那么就实现了类似二分查找的算法思想,此时这种数据结构就被成为跳表。 二、跳表性能分析2.1 时间复杂度
  假设有序链表总节点个数为n,我们建立索引时每两个节点提取一个索引节点;那么第一级索引共有n/2个节点;
  第k级索引共有(n/2^k)个节点,假设k为最高级索引,为2个节点,那么2=(n/2^k),n=2(k+1),k=logn-1,如果原始链表也算进去的话,k=logn正好是整个数据结构的高度。
  假设每一层需要遍历m个节点,那么时间复杂度就可以表示为O(mlogn),但是推断m是常量级别的,因此可以忽略,那么整个查找过程时间复杂度就是O(logn),竟然和二分查找是一样的高效!2.2 空间复杂度
  第一级索引需要n/2个节点,第二级需要n/2^2个节点,依次类推,第k级索引需要的节点个数为n/2^k,这正好是一个等比数列,等比数列求和的结果是n-2,所以空间复杂度为O(n),这是一个以空间换时间的策略;
  我们可以每3个节点或者每4个节点往上提取一个索引节点,如此可以适当减少需要的额外空间,但是空间复杂度仍然为O(n);如果有序链表存储的是大对象,那么索引节点中无需存放整个大对象,只要存储对象的指针即可,所以此时空间复杂度就显得不那么重要了;2.3 跳表的插入和删除
  跳表因为是顺序链表,所以真正插入和删除的时间复杂度都是O(1),但是找到需要插入节点的位置或者找到待删除的节点时间复杂度为O(logn);
  跳表在删除的时候,除了需要删除原始有序链表中的节点,还需要同步删除k级索引中的全部该索引节点;
  跳表在插入元素式,极端情况下会导致两个索引节点中存在大量的原始节点,时间效率极有可能会退化为单链表的O(n),所以需要动态地平衡和更新k级索引节点; 三、跳表使用场景
  Redis在存储有序集合的时候就用到了跳表+散列表的数据结构,跳表和红黑树相比,插入、删除、查找的时间复杂度相同,但是跳表在按照区间查找时明显具有效率优势,而且跳表实现起来比红黑树要简单易懂,不容易出错。
  红黑树等常用数据结构在程序语言中都是封装好的,我们想用的话直接拿来用即可,比如HashMap,但是跳表却没有对应的封装好的数据结构,想用的话开发者必须自己去实现。 四、代码实现跳表Skiplist以及优化
  代码来源于极客时间《数据结构和算法之美》
  github地址:https://github.com/wangzheng0822/algo(数据结构和算法必知必会的50个代码实现) 4.1 作者王争给出的跳表实现方式/**  * 跳表的一种实现方法。  * 跳表中存储的是正整数,并且存储的是不重复的。  *  */ public class SkipList {    private static final float SKIPLIST_P = 0.5f;   private static final int MAX_LEVEL = 16;    private int levelCount = 1;    private Node head = new Node();  // 带头链表    public Node find(int value) {     Node p = head;     for (int i = levelCount - 1; i >= 0; --i) {       while (p.forwards[i] != null && p.forwards[i].data < value) {         p = p.forwards[i];       }     }      if (p.forwards[0] != null && p.forwards[0].data == value) {       return p.forwards[0];     } else {       return null;     }   }    public void insert(int value) {     int level = randomLevel();     Node newNode = new Node();     newNode.data = value;     newNode.maxLevel = level;     Node update[] = new Node[level];     for (int i = 0; i < level; ++i) {       update[i] = head;     }      // record every level largest value which smaller than insert value in update[]     Node p = head;     for (int i = level - 1; i >= 0; --i) {       while (p.forwards[i] != null && p.forwards[i].data < value) {         p = p.forwards[i];       }       update[i] = p;// use update save node in search path     }      // in search path node next node become new node forwords(next)     for (int i = 0; i < level; ++i) {       newNode.forwards[i] = update[i].forwards[i];       update[i].forwards[i] = newNode;     }      // update node hight     if (levelCount < level) levelCount = level;   }    public void delete(int value) {     Node[] update = new Node[levelCount];     Node p = head;     for (int i = levelCount - 1; i >= 0; --i) {       while (p.forwards[i] != null && p.forwards[i].data < value) {         p = p.forwards[i];       }       update[i] = p;     }      if (p.forwards[0] != null && p.forwards[0].data == value) {       for (int i = levelCount - 1; i >= 0; --i) {         if (update[i].forwards[i] != null && update[i].forwards[i].data == value) {           update[i].forwards[i] = update[i].forwards[i].forwards[i];         }       }     }      while (levelCount>1&&head.forwards[levelCount]==null){       levelCount--;     }    }    // 理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。   // 因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。   // 该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且 :   //        50%的概率返回 1   //        25%的概率返回 2   //      12.5%的概率返回 3 ...   private int randomLevel() {     int level = 1;      while (Math.random() < SKIPLIST_P && level < MAX_LEVEL)       level += 1;     return level;   }    public void printAll() {     Node p = head;     while (p.forwards[0] != null) {       System.out.print(p.forwards[0] + " ");       p = p.forwards[0];     }     System.out.println();   }    public class Node {     private int data = -1;     private Node forwards[] = new Node[MAX_LEVEL];     private int maxLevel = 0;      @Override     public String toString() {       StringBuilder builder = new StringBuilder();       builder.append("{ data: ");       builder.append(data);       builder.append("; levels: ");       builder.append(maxLevel);       builder.append(" }");        return builder.toString();     }   }  }4.2 作者ldb基于王争的代码给出的优化import java.util.Random;  /**  * 1,跳表的一种实现方法,用于练习。跳表中存储的是正整数,并且存储的是不重复的。  * 2,本类是参考作者zheng ,自己学习,优化了添加方法  * 3,看完这个,我觉得再看ConcurrentSkipListMap 源码,会有很大收获  */ public class SkipList2 {      private static final int MAX_LEVEL = 16;     private int levelCount = 1;      /**      * 带头链表      */     private Node head = new Node(MAX_LEVEL);     private Random r = new Random();      public Node find(int value) {         Node p = head;         // 从最大层开始查找,找到前一节点,通过--i,移动到下层再开始查找         for (int i = levelCount - 1; i >= 0; --i) {             while (p.forwards[i] != null && p.forwards[i].data < value) {                 // 找到前一节点                 p = p.forwards[i];             }         }          if (p.forwards[0] != null && p.forwards[0].data == value) {             return p.forwards[0];         } else {             return null;         }     }      /**      * 优化了作者zheng的插入方法      *      * @param value 值      */     public void insert(int value) {         int level = head.forwards[0] == null ? 1 : randomLevel();         // 每次只增加一层,如果条件满足         if (level > levelCount) {             level = ++levelCount;         }         Node newNode = new Node(level);         newNode.data = value;         Node update[] = new Node[level];         for (int i = 0; i < level; ++i) {             update[i] = head;         }          Node p = head;         // 从最大层开始查找,找到前一节点,通过--i,移动到下层再开始查找         for (int i = levelCount - 1; i >= 0; --i) {             while (p.forwards[i] != null && p.forwards[i].data < value) {                 // 找到前一节点                 p = p.forwards[i];             }             // levelCount 会 > level,所以加上判断             if (level > i) {                 update[i] = p;             }          }         for (int i = 0; i < level; ++i) {             newNode.forwards[i] = update[i].forwards[i];             update[i].forwards[i] = newNode;         }      }      /**      * 优化了作者zheng的插入方法2      *      * @param value 值      */     public void insert2(int value) {         int level = head.forwards[0] == null ? 1 : randomLevel();         // 每次只增加一层,如果条件满足         if (level > levelCount) {             level = ++levelCount;         }         Node newNode = new Node(level);         newNode.data = value;         Node p = head;         // 从最大层开始查找,找到前一节点,通过--i,移动到下层再开始查找         for (int i = levelCount - 1; i >= 0; --i) {             while (p.forwards[i] != null && p.forwards[i].data < value) {                 // 找到前一节点                 p = p.forwards[i];             }             // levelCount 会 > level,所以加上判断             if (level > i) {                 if (p.forwards[i] == null) {                     p.forwards[i] = newNode;                 } else {                     Node next = p.forwards[i];                     p.forwards[i] = newNode;                     newNode.forwards[i] = next;                 }             }          }      }      /**      * 作者zheng的插入方法,未优化前,优化后参见上面insert()      *      * @param value      * @param level 0 表示随机层数,不为0,表示指定层数,指定层数      *              可以让每次打印结果不变动,这里是为了便于学习理解      */     public void insert(int value, int level) {         // 随机一个层数         if (level == 0) {             level = randomLevel();         }         // 创建新节点         Node newNode = new Node(level);         newNode.data = value;         // 表示从最大层到低层,都要有节点数据         newNode.maxLevel = level;         // 记录要更新的层数,表示新节点要更新到哪几层         Node update[] = new Node[level];         for (int i = 0; i < level; ++i) {             update[i] = head;         }          /**          *          * 1,说明:层是从下到上的,这里最下层编号是0,最上层编号是15          * 2,这里没有从已有数据最大层(编号最大)开始找,(而是随机层的最大层)导致有些问题。          *    如果数据量为1亿,随机level=1 ,那么插入时间复杂度为O(n)          */         Node p = head;         for (int i = level - 1; i >= 0; --i) {             while (p.forwards[i] != null && p.forwards[i].data < value) {                 p = p.forwards[i];             }             // 这里update[i]表示当前层节点的前一节点,因为要找到前一节点,才好插入数据             update[i] = p;         }          // 将每一层节点和后面节点关联         for (int i = 0; i < level; ++i) {             // 记录当前层节点后面节点指针             newNode.forwards[i] = update[i].forwards[i];             // 前一个节点的指针,指向当前节点             update[i].forwards[i] = newNode;         }          // 更新层高         if (levelCount < level) levelCount = level;     }      public void delete(int value) {         Node[] update = new Node[levelCount];         Node p = head;         for (int i = levelCount - 1; i >= 0; --i) {             while (p.forwards[i] != null && p.forwards[i].data < value) {                 p = p.forwards[i];             }             update[i] = p;         }          if (p.forwards[0] != null && p.forwards[0].data == value) {             for (int i = levelCount - 1; i >= 0; --i) {                 if (update[i].forwards[i] != null && update[i].forwards[i].data == value) {                     update[i].forwards[i] = update[i].forwards[i].forwards[i];                 }             }         }     }      /**      * 随机 level 次,如果是奇数层数 +1,防止伪随机      *      * @return      */     private int randomLevel() {         int level = 1;         for (int i = 1; i < MAX_LEVEL; ++i) {             if (r.nextInt() % 2 == 1) {                 level++;             }         }         return level;     }      /**      * 打印每个节点数据和最大层数      */     public void printAll() {         Node p = head;         while (p.forwards[0] != null) {             System.out.print(p.forwards[0] + " ");             p = p.forwards[0];         }         System.out.println();     }      /**      * 打印所有数据      */     public void printAll_beautiful() {         Node p = head;         Node[] c = p.forwards;         Node[] d = c;         int maxLevel = c.length;         for (int i = maxLevel - 1; i >= 0; i--) {             do {                 System.out.print((d[i] != null ? d[i].data : null) + ":" + i + "-------");             } while (d[i] != null && (d = d[i].forwards)[i] != null);             System.out.println();             d = c;         }     }      /**      * 跳表的节点,每个节点记录了当前节点数据和所在层数数据      */     public class Node {         private int data = -1;         /**          * 表示当前节点位置的下一个节点所有层的数据,从上层切换到下层,就是数组下标-1,          * forwards[3]表示当前节点在第三层的下一个节点。          */         private Node forwards[];          /**          * 这个值其实可以不用,看优化insert()          */         private int maxLevel = 0;          public Node(int level) {             forwards = new Node[level];         }          @Override         public String toString() {             StringBuilder builder = new StringBuilder();             builder.append("{ data: ");             builder.append(data);             builder.append("; levels: ");             builder.append(maxLevel);             builder.append(" }");             return builder.toString();         }     }      public static void main(String[] args) {         SkipList2 list = new SkipList2();         list.insert(1, 3);         list.insert(2, 3);         list.insert(3, 2);         list.insert(4, 4);         list.insert(5, 10);         list.insert(6, 4);         list.insert(8, 5);         list.insert(7, 4);         list.printAll_beautiful();         list.printAll();         /**          * 结果如下:          * null:15-------          * null:14-------          * null:13-------          * null:12-------          * null:11-------          * null:10-------          * 5:9-------          * 5:8-------          * 5:7-------          * 5:6-------          * 5:5-------          * 5:4-------                              * 8:4-------          * 4:3-------5:3-------6:3-------7:3-------8:3-------          * 1:2-------2:2-------          4:2-------5:2-------6:2-------7:2-------8:2-------          * 1:1-------2:1-------3:1-------4:1-------5:1-------6:1-------7:1-------8:1-------          * 1:0-------2:0-------3:0-------4:0-------5:0-------6:0-------7:0-------8:0-------          * { data: 1; levels: 3 } { data: 2; levels: 3 } { data: 3; levels: 2 } { data: 4; levels: 4 }          * { data: 5; levels: 10 } { data: 6; levels: 4 } { data: 7; levels: 4 } { data: 8; levels: 5 }          */         // 优化后insert()          SkipList2 list2 = new SkipList2();         list2.insert2(1);         list2.insert2(2);         list2.insert2(6);         list2.insert2(7);         list2.insert2(8);         list2.insert2(3);         list2.insert2(4);         list2.insert2(5);         System.out.println();         list2.printAll_beautiful();       }  }

雅尼斯挂帅印而去,北京篮球的问题不会因此而解决被吉林淘汰后,雅尼斯还是很平静地开完发布会,面对记者的激烈提问,还不形声色。想不到的是新闻发布会一结束,雅尼斯就用一纸书信,宣告结束将近5年北京执教生涯。雅尼斯宣布自己承担球队失利CBA中职篮北京首钢主教练雅尼斯辞职有四人最有可能接过帅印第一位是解立彬,这个呼声最高,解指导一直就是北京的助理教练,而且每到了北京首钢教练员缺失的关键时刻,解立彬都会挺身而出,而到了北京首钢需要他做出牺牲,他立刻就会毫不犹豫的下课腾出位北京队重建计划!3老将必须淘汰,神投手两场17中3,方硕最可惜进入4月份之后,CBA季后赛点燃了球迷的热情,一般来说首轮附加赛只是开胃菜,出现冷门的几率不大,但是北京首钢被吉林队横扫,还是让人大跌眼镜。最为要命的是,这次溃败意味着北京队夏天要CBA常规赛大奖全部出炉!引发出3大争议,超级外援发文吐槽正值CBA季后赛来临之际,CBA公司正式公布CBA20212022赛季常规赛的各项大奖,从各项大奖的得主来看,广厦队无疑成为了最大的赢家,孙铭徽和胡金秋进入第一阵容,胡金秋收获MV中国足球正式确认!恭喜武磊,恭喜李霄鹏,他们这回可以安心了北京时间4月5日,中国足球传来最新消息,据国内权威媒体足球报报道,中国足协透露目前还没有归化球员主动申请退出中国国籍,在李霄鹏还需要归化球员的情况下,足协确认归化球员没有退出中国籍NBA这些看起来很不可思议的照片,真的都没有被P过NBA有些照片看起来不可思议,那么这些照片,大概率是被P过。现在P图又不是什么技术,一部手机就能搞定,因此NBAP图真不少。但是有些不可思议的照片,那是真实存在,一点都没被P过。比刘翔我老婆美,苏炳添我老婆更美,看到马龙老婆美过热巴刘翔我老婆美,苏炳添我老婆更美,看到马龙老婆美过热巴。情人眼里出西施,对于这句话可能很多网友都深有体会吧!毕竟在相爱的两个人眼中,爱人拥有时最帅气和最漂亮的,没有任何人能够和他们比郭艾伦与林书豪谁更强?徐雨犀利点评不给颜面,网友一致赞同郭艾伦与林书豪谁更强?徐雨犀利点评不给颜面,网友一致赞同亲情提示亲爱的读者,如何能每天能读到这样的体育资讯呢?点击右上角的关注按钮即可,您的关注将是我创作的最大动力!CBA季后赛继惨烈!被曼城德比4国家队比赛日结束之后,本周末英超联赛将重燃战火。在英超积分榜上,曼城仍压制着强势的利物浦高居第一位,而且已经领先同城死敌曼联多达20分!本赛季两回合曼市德比,曼城双杀曼联,第二回合两主力缺阵马刺仍轻取开拓者坐稳西部第十!凯登约翰逊28分NBA常规赛4月4日继续进行,本场比赛穆雷和博尔特尔缺阵。最终,马刺以11392战胜开拓者,开拓者遭遇7连败。而由于湖人早上输给掘金,马刺将与湖人的胜场拉开到2场,马刺坐稳了西部第今日CBA血性是灵魂,有它东北虎就能再品鸭味,鲁深之战在于智今日京吉再战!在上一轮的较量中,是单外援的吉林东北虎击败了四外援的北京鸭队。而且吉林队仅凭7人轮换,就对首钢队保持了整场的压制。其中琼斯姜宇星姜伟泽三人的上场时间都是超过了40分钟
生吃最健康营养?不是所有的蔬菜都适合近年来,越来越多的轻食沙拉受到了许多健康人士的推崇。生吃蔬菜被认为可以更好地获取蔬菜中所含的营养成分,没有经过加工的蔬菜除了能保留本身的原汁原味,还更符合自然风味。图片来源视觉中国单程最高收200块!燃油附加费五连涨快讯(王潇雨摄影)文王潇雨时隔一个月,连续调升征收标准的国内航线燃油附加费迎来又一次涨价,而这已经是自今年2月以来连续第五次上涨。在线旅游服务商同程旅行方面在6月30日透露,已经接到多王者新赛季到底是MMR匹配机制,还是ELO匹配机制王者S28新赛季也更新了好几天了,不知道大家现在是什么段位了,有木有上星耀,甚至是已经冲上王者了。反正小编打了20多局排位,现在还是一颗星没上,甚至是掉到了钻石三4颗星。赛季初定位寂然在线失落,打野段位直掉,原来是没有搞清这个机制目前王者荣耀新赛季已经更新有几天了,不知道各位小伙伴们有没有上分呢?这个赛季和上个赛季在巅峰赛机制上,还是有相似的地方的,只要玩家们上个赛季达到了荣耀王者的段位,便可直接参与巅峰赛8大健康食物公布,海参未入榜?中老年人可以多吃8大健康食物公布,海参未入榜?中老年人要多吃,好吃且营养。日常生活中的饮食,对于人们的健康起着非常关键的作用。尤其是一些中老年人消化能力下降,更应该多加注意。老年人年纪大了,在锻炼食物嘌呤排行榜!2张图教你怎么吃!远离尿酸炸弹护肾保平安如今生活条件好了,人们顿顿吃香喝辣天天大鱼大肉,因此在三高之外还有了第四高高尿酸。据统计,我国高尿酸血症的总体患病率为13。3,患病人数约为1。77亿,而且这个人群还有越来越庞大的肾炎忌口的食物有哪些?肾炎患者饮食需要忌口,这样才有利于康复。具体肾炎不能吃什么呢?下面小编列举肾炎不能吃的几种食物,日常要不吃或少吃哦!1。高盐分饮食慢性肾炎的水肿和血容量钠盐的关系极大,故必须限制食杏坛杯兄弟游记(中学)己亥七月,下旬伊始,兄弟五人,结伴而游。晨遇中雨,勿延行程,自驾爱车,踏上旅途。驱车三时,一路高速,雨声相伴,笑声起伏。终点峄山,巳时抵达,老天相惜,云多雨住。山峙邹城,道家之所,朝开暮落,木槿花只为盛夏绽放6月29日,郯城县马头镇玉带社区绿化带里的木槿花开了。盛开的木槿花红白相间,绚丽多姿,淡雅素洁,炎炎夏日里给人们带来一丝丝清凉。木槿花每天随着太阳升起开放,随着太阳落山而闭合,坚持高考完宅家里?不!要出去看世界德天瀑布布被评为中国最美六大瀑布之一,瀑布宽120米,分为三级,垂直高度60多米,是亚洲第一世界第二大的跨国瀑布,与越南板约瀑布相连,雨季两瀑布融为一体,全宽208米,气势磅礴。瀑管理好便秘,首先应该学会吃应该如何正确的去管理便秘呢?我们建议采用一个非药物干预的方法。管理包括四个方面膳食营养干预生活行为管理运动指导肠道微生态管理。首先是膳食营养干预,关键要均衡膳食结构合理,有良好的膳