19. 跳表(Skip List)

1 跳表
  1. 一个有序链表搜索、添加、删除的平均时间复杂度是多少?
    1. 数组查找的时间复杂度可以达到O(logn)是因为数组支持随机访问,对于有序的数组,可以通过二分查找,达到O(logn)的效率
      在这里插入图片描述
  2. 能否利用二分搜索优化有序链表,将搜索、添加、删除的平均时间复杂度降低至 O(logn)?
    1. 链表没有像数组那样的高效随机访问(O(1) 时间复杂度),所以不能像有序数组那样直接进行二分搜索优化
  3. 那有没有其他办法让有序链表搜索、添加、删除的平均时间复杂度降低至 O(logn)?
    1. 使用跳表(SkipList)
  4. 跳表,又叫做跳跃表、跳跃列表,在有序链表的基础上增加了“跳跃”的功能。设计的初衷是为了取代平衡树(比如红黑树),跳表类似TreeMap,都存放key-value
  5. Redis中 的 SortedSet、LevelDB 中的 MemTable 都用到了跳表
  6. 对比平衡树
    1. 跳表的实现和维护会更加简单
    2. 跳表的搜索、删除、添加的平均时间复杂度是 O(logn)
  7. 使用跳表优化链表
    在这里插入图片描述
  8. 跳表的搜索
    1. 从顶层链表的首元素开始,从左往右搜索,直至找到一个大于或等于目标的元素,或者到达当前层链表的尾部
    2. 如果该元素等于目标元素,则表明该元素已被找到
    3. 如果该元素大于目标元素或已到达链表的尾部,则退回到当前层的前一个元素,然后转入下一层进行搜索
  9. 跳表的添加、删除
    1. 本质就是在搜索的基础上,建立一个数组,存放要添加或删除的节点的所有层的前驱节点
    2. 新添加节点的层数,是随机以一定规则获得的
    3. 最后还需要考虑层数的更新
      在这里插入图片描述
  10. 跳表的层数
    1. 跳表是按层构造的,底层是一个普通的有序链表,高层相当于是低层的“快速通道”
    2. 在第 i 层中的元素按某个固定的概率 p(通常为 ½ 或 ¼ )出现在第 i + 1层中,产生越高的层数,概率越低
      1. 元素层数恰好等于 1 的概率为 1 – p
      2. 元素层数大于等于 2 的概率为 p,而元素层数恰好等于 2 的概率为 p * (1 – p)
      3. 元素层数大于等于 3 的概率为 p^2,而元素层数恰好等于 3 的概率为 (p ^2) * (1 – p)
      4. 将这些概率加和,除以层数,得到一个元素的平均层数是 1 / (1 – p)
        1. 当 p = ½ 时,每个元素所包含的平均指针数量是 2
        2. 当 p = ¼ 时,每个元素所包含的平均指针数量是 1.33
        3. 跳表中的指针数指的就是nexts数组中元素个数,而红黑树每个节点上指针至少是三个,next、parent、right,因此当p=1/4可以发现,其空间复杂度要好于红黑树
  11. 跳表的复杂度分析
    1. 每一层的元素数量
      1. 第 1 层链表固定有 n 个元素(最底层,一定是和总元素个数相同)
      2. 第 2 层链表平均有 n * p 个元素
      3. 第 3 层链表平均有 n * p^2 个元素
      4. 第 k 层链表平均有 n * p^k 个元素
    2. 跳表最高有 log (1/p) (n)层,搜索时,每一层链表的预期查找步数最多是 1/p
      1. 所以,如果p是1/4,那么总的查找步数是最高层数*每层最多步数,即log4(n)/4,
      2. 因此时间复杂度为O(logn)
  12. SkipList
package com.mj;

import java.util.Comparator;

@SuppressWarnings("unchecked")
public class SkipList<K, V> {
	private static final int MAX_LEVEL = 32;
	private static final double P = 0.25;
	private int size;
	private Comparator<K> comparator;
	//记录有效层数,方便搜索的时,从有效的最高层数开始搜索,例如从first.next[3]开始搜索,注意层数是从1开始的不是从0,所以循环时,都从first.nexts[level-1]开始
	private int level;
	/**
	 * 不存放任何K-V,如果存放就没法走不同的层了
	 */
	private Node<K, V> first;
	
	public SkipList(Comparator<K> comparator) {
		this.comparator = comparator;
		//设置其最高层为MAX_LEVEL,redis是32层,节点中最高就是32层
		//虽然有32层,但实际上可能最后用到的就几层,也就是可能只first.next[0]到first.next[3]有值,其他都为null
		first = new Node<>(null, null, MAX_LEVEL);
	}
	
	public SkipList() {
		this(null);
	}
	
	public int size() {
		return size;
	}
	
	public boolean isEmpty() {
		return size == 0;
	}
	
	public V get(K key) {
		keyCheck(key);
		
		// first.nexts[3] == 21节点
		// first.nexts[2] == 9节点
		// first.nexts[1] == 6节点
		// first.nexts[0] == 3节点
		
		// key = 30
		// level = 4
		
		Node<K, V> node = first;
		for (int i = level - 1; i >= 0; i--) {
			int cmp = -1;
			//先从最上层找,一旦找到的值比要找的值大了,就退到下一层去找
			//node.nexts[i] = null意味着,本层已经遍历完了,这种情况应该退到下一层重新找
			while (node.nexts[i] != null 
					&& (cmp = compare(key, node.nexts[i].key)) > 0) {
				node = node.nexts[i];
			}
			// node.nexts[i].key >= key
			if (cmp == 0) return node.nexts[i].value;
		}
		return null;
	}
	
	public V put(K key, V value) {
		keyCheck(key);
		
		Node<K, V> node = first;
		//用于存放是从哪个节点位置退到下一层继续查找,该跳表有效层数有多少层,说明最多会向下多少次,因此该数组就存多少个元素
		Node<K, V>[] prevs = new Node[level];
		for (int i = level - 1; i >= 0; i--) {
			int cmp = -1;
			while (node.nexts[i] != null 
					&& (cmp = compare(key, node.nexts[i].key)) > 0) {
				node = node.nexts[i];
			}
			if (cmp == 0) { // 节点是存在的
				V oldV = node.nexts[i].value;
				node.nexts[i].value = value;
				return oldV;
			}
			//存放在哪个节点位置向下找
			prevs[i] = node;
		}
		
		//到下面代码,一定说明了该节点没找到,那么此时的node就一定是要添加节点的、在第0层的,前驱节点
		// 随机获取新节点的层数
		int newLevel = randomLevel();
		// 添加新节点
		Node<K, V> newNode = new Node<>(key, value, newLevel);
		// 设置前驱和后继
		for (int i = 0; i < newLevel; i++) {
			//如果新增的节点随机出来的层数,比现有层数高,需要连接其高出的几层与first相连
			if (i >= level) {
				first.nexts[i] = newNode;
			} else {
				//将新加入的节点(17)后面的线连上
				newNode.nexts[i] = prevs[i].nexts[i];
				//将新加入的节点前面的线连上
				prevs[i].nexts[i] = newNode;
			}
		}
		
		// 节点数量增加
		size++;
		
		// 计算跳表的最终层数
		level = Math.max(level, newLevel);
		
		return null;
	}
	
	public V remove(K key) {
		keyCheck(key);
		
		Node<K, V> node = first;
		Node<K, V>[] prevs = new Node[level];
		boolean exist = false;
		for (int i = level - 1; i >= 0; i--) {
			int cmp = -1;
			while (node.nexts[i] != null 
					&& (cmp = compare(key, node.nexts[i].key)) > 0) {
				node = node.nexts[i];
			}
			//删除相等时的处理,相等的时候,也不应该停止循环,直到找到要删除元素的所有的、不同层的前驱节点
			prevs[i] = node;
			//需要记录是否存在,只要有一次找到了,就认为要删除节点存在。如果最终都不存在就没法删除
			if (cmp == 0) exist = true;
		}
		if (!exist) return null;
		
		// 需要被删除的节点,该节点一定是其第0层的前驱节点的后继节点
		Node<K, V> removedNode = node.nexts[0];
		
		// 数量减少
		size--;
		
		// 设置后继,被删除的节点的后继数组有几个元素,就说明该节点原来有几层,循环将每一层删除
		for (int i = 0; i < removedNode.nexts.length; i++) {
			prevs[i].nexts[i] = removedNode.nexts[i];
		}
		
		// 更新跳表的层数,利用的原理就是,更新后的跳表,first指向的nexts[n],如果有null
		//就说明原来在某一层,只有被删除节点一个元素,因此节点的删除,导致了层数的降低,此时应该层数-1
		int newLevel = level;
		while (--newLevel >= 0 && first.nexts[newLevel] == null) {
			level = newLevel;
		}
		
		return removedNode.value;
	}
	
	private int randomLevel() {
		int level = 1;
		//小于0.25,才增加,最高不允许超过定义的MAX_LEVEL,所以其实新节点的层数越高、概率越小,这就保证了不同层的元素个数肯定不同
		while (Math.random() < P && level < MAX_LEVEL) {
			level++;
		}
		return level;
	}
	
	private void keyCheck(K key) {
		if (key == null) {
			throw new IllegalArgumentException("key must not be null.");
		}
	}
	
	private int compare(K k1, K k2) {
		return comparator != null 
				? comparator.compare(k1, k2)
				: ((Comparable<K>)k1).compareTo(k2);
	}
	
	private static class Node<K, V> {
		K key;
		V value;
		//存放Node不同层连接的节点
		Node<K, V>[] nexts;
//		Node<K, V> right;
//		Node<K, V> down;
//		Node<K, V> top;
//		Node<K, V> left;
		public Node(K key, V value, int level) {
			this.key = key;
			this.value = value;
			nexts = new Node[level];
		}
		@Override
		public String toString() {
			return key + ":" + value + "_" + nexts.length;
		}
	}
	//拼接每一层所有节点
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("一共" + level + "层").append("\n");
		for (int i = level - 1; i >= 0; i--) {
			Node<K, V> node = first;
			while (node.nexts[i] != null) {
				sb.append(node.nexts[i]);
				sb.append(" ");
				node = node.nexts[i];
			}
			//换行
			sb.append("\n");
		}
		return sb.toString();
	}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章