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
