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();
	}
}

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