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