1. 程式人生 > 其它 >Java基礎 - 跳錶(SkipList)

Java基礎 - 跳錶(SkipList)

Java基礎 - 跳錶(SkipList)

跳錶(skiplist)是一個非常優秀的資料結構,實現簡單,插入、刪除、查詢的複雜度均為O(logN)。LevelDB的核心資料結構是用跳錶實現的,redis的sorted set資料結構也是有跳錶實現的。

跳錶同時是平衡樹的一種替代的資料結構,但是和紅黑樹不相同的是,跳錶對於樹的平衡的實現是基於一種隨機化的演算法的,這樣也就是說跳錶的插入和刪除的工作是比較簡單的。下面來研究一下跳錶的核心思想:

下面給出一個完整的跳錶的圖示:

首先從考慮一個有序表開始:


從該有序表中搜索元素 < 23, 43, 59 > ,需要比較的次數分別為 < 2, 4, 6 >,總共比較的次數為 2 + 4 + 6 = 12 次。有沒有優化的

演算法嗎? 連結串列是有序的,但不能使用二分查詢。類似二叉搜尋樹,我們把一些節點提取出來,作為索引。得到如下結構:

這裡我們把 < 14, 34, 50, 72 > 提取出來作為一級索引,這樣搜尋的時候就可以減少比較次數了。我們還可以再從一級索引提取一些元素出來,作為二級索引,變成如下結構:

這裡元素不多,體現不出優勢,如果元素足夠多,這種索引結構就能體現出優勢來了。

跳錶

下面的結構是就是跳錶:

其中 -1 表示 INT_MIN, 連結串列的最小值,1 表示 INT_MAX,連結串列的最大值。

跳錶具有如下性質:

(1) 由很多層結構組成

(2) 每一層都是一個有序的連結串列

(3) 最底層(Level 1)的連結串列包含所有元素

(4) 如果一個元素出現在 Level i 的連結串列中,則它在 Level i 之下的連結串列也都會出現。

(5) 每個節點包含兩個指標,一個指向同一連結串列中的下一個元素,一個指向下面一層的元素。

跳錶的搜尋

例子:查詢元素 117

(1) 比較 21, 比 21 大,往後面找

(2) 比較 37, 比 37大,比連結串列最大值小,從 37 的下面一層開始找

(3) 比較 71, 比 71 大,比連結串列最大值小,從 71 的下面一層開始找

(4) 比較 85, 比 85 大,從後面找

(5) 比較 117, 等於 117, 找到了節點。

跳錶的插入:

先確定該元素要佔據的層數 K(採用丟硬幣的方式,這完全是隨機的)然後在 Level 1 ... Level K 各個層的連結串列都插入元素。

例子:插入 119, K = 2

如果 K 大於連結串列的層數,則要新增新的層。

例子:插入 119, K = 4


跳錶的刪除

在各個層中找到包含 x 的節點,使用標準的 delete from list 方法刪除該節點。

例子:刪除 71



丟硬幣決定 K

插入元素的時候,元素所佔有的層數完全是隨機的。相當與做一次丟硬幣的實驗,如果遇到正面,繼續丟,遇到反面,則停止,用實驗中丟硬幣的次數 K 作為元素佔有的層數。

下面是程式碼實現:
package com.yc.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 wb
 *
 *	在這裡我也從網上查了一些關於跳錶的資料,發現了跳錶的兩種資料結構的設計
 *	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次耗時更少吧。
 *
 */
public class SkipList {
	private class Node{
		//儲存的資料,當然也可以用泛型
		int data;
		//leavel層陣列
		Node[] forword;

		//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()方法量身打造的

	private Random rd = new Random();

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

	/**
	 * 快速排序
	 * @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;
	}

	//遍歷跳錶  限第一層
	public Map<integer, node="">> lookUp(){
		Map<integer, node="">> map = new HashMap<integer, node="">>();
		List nodes;
		for(int i = 0; i < header.forword.length; i ++){
			nodes = new ArrayList();
			for(Node current = header; current != null; current = current.forword[i]){
				nodes.add(current);
			}
			map.put(i,nodes);
		}
		return map;
	}

	public void show(Map<integer, node="">> map){
		for(int i = map.size() - 1; i >= 0; i --){
			List list = map.get(i);
			StringBuffer sb = new StringBuffer("第"+i+"層:");
			for(Iterator 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] 
由於個人能力有限,不能很好把結果展示給大家,望見諒。
原創連結:https://www.2cto.com/kf/201612/579219.html