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