跳躍表
跳躍表的引入
無論是數組還是鏈表在插入新數據的時候,都會存在性能問題。排好序的數據,如果使用數組,插入新數據的方式如下:
如果要插入數據3,首先要知道這個數據應該插入的位置。使用二分查找可以最快定位,這一步時間復雜度是O(logN)。插入過程中,原數組中所有大於3的商品都要右移,這一步時間復雜度是O(N)。所以總體時間復雜度是O(N)。
如果使用鏈表,插入新的數據方式如下:
如果要插入數據3,首先要知道這個商品應該插入的位置。鏈表無法使用二分查找,智能和原鏈表中的節點逐一比較大小來確定位置。這一步的時間復雜度是O(N)。插入的過程倒是很容易,直接改變節點指針的目標,時間復雜度O(1)。因此總體的時間復雜度也是O(N)。
跳躍表原理
基本概念
如果對於有幾十萬的數據集合來說,這兩種方法顯然都太慢了。為此可以引入跳躍表(skiplist),跳躍表是一種基於有序鏈表的擴展,簡稱跳表。利用類似索引的思想,提取出鏈表中的部分關鍵節點。比如給定一個長度是7的有序鏈表,節點值依次是1->2->3->4->5->6->7->8。那么我們可以取出所有值為奇數的節點作為關鍵點。
此時如果插入一個值是4的新節點,不再需要和原節點8,7,6,5,3逐一比較,只需要比較關鍵節點7,5,3。
確定了新節點在關鍵點中的位置(3和5之間),就可以回到原鏈表,迅速定位到對應的位置插入(同樣是3和5之間)。
現在節點數目少,優化效果還不明顯,如果鏈表中1w甚至10w個節點,比較次數就整整減少了一半。這樣做雖然增加50%的額外的空間,但是性能提高了一倍。不過可以進一步思考,既然已經提取出了一層關鍵節點作為索引,那我們可以從索引中進一步提取,提出一層索引的索引。
當節點足夠多的時候,我們不止能提出兩層索引,還可以向更高層次提取,保證每一層是上一層節點數的一半。至於提取的極限,則是同一層只有兩個節點的時候,因為一個節點沒有比較的意義。這樣的多層鏈表結構就是所謂的跳躍表。
插入節點
有一個問題需要注意,當大量的新節點通過逐層比較,最終插入到原鏈表之后,上層的索引節點會漸漸變得不夠用。這時候需要從新節點當中選取一部分提到上一層,可是究竟應該提拔誰、忽略誰?關於這一點,跳躍表的設計者采用了一種有趣的辦法:拋硬幣。也就是隨機決定新節點是否提拔,每向上提拔一層的幾率是50%。仍然借用剛才的例子,假如值為9新節點插入原鏈表
使用拋硬幣的方法的原因是因為跳躍表刪除和添加的節點是不可預測道德,很難用一種有效的算法來保證跳躍表的索引部分始終均勻。隨機拋硬幣的方法雖然不能保證索引絕對均勻分布,卻可以讓大體趨於均勻。總結一下,跳躍表插入節點的流程有以下幾步:
-
新節點和各層索引節點逐一比較,確定原鏈表的插入位置。O(logN)
-
把索引插入到原鏈表。O(1)
-
利用拋硬幣的隨機方式,決定新節點是否提升為上一級索引。結果為"正"則提升並繼續拋硬幣,結果為"負"則停止。O(logN)
總體上,跳躍表插入操作的時間復雜度是O(logN),而這種數據結構所占空間是2N,即空間復雜度是O(N)
刪除節點
刪除操作比較簡單,只要在索引層找到要刪除的節點,然后順藤摸瓜,刪除每一層的相同節點即可。如果某一層索引在刪除后只剩下一個節點,那么整個一層就可以干掉了。還用原來的例子,如果要刪除的節點值是5
總結一下跳躍表刪除節點的步驟:
-
自上而下,查找第一次出現節點的索引,並逐層找到每一層對應的節點。O(logN)
-
刪除每一層查找到的節點,如果該層只剩下1個節點,刪除整個一層(原鏈表除外)。O(logN)
總體上,跳躍表刪除操作的時間復雜度是O(logN)。
跳躍表java實現
定義節點
public class SkipListNode implements Comparable { private int value; private SkipListNode next = null; private SkipListNode downNext = null; @Override protected void finalize() throws Throwable { super.finalize(); System.out.printf("123"); } public int getValue() { return value; } public void setValue(int value) { this.value = value; } public SkipListNode getNext() { return next; } public void setNext(SkipListNode next) { this.next = next; } public SkipListNode getDownNext() { return downNext; } public void setDownNext(SkipListNode downNext) { this.downNext = downNext; } @Override public int compareTo(Object o) { return this.value > ((SkipListNode)o).value ? 1 : -1; } }
插入、刪除操作類
import java.util.Random; public class SkipList { public int level = 0; public SkipListNode top = null; public SkipList() { this(4); } //跳躍表的初始化 public SkipList(int level) { this.level = level; SkipListNode skipListNode = null; SkipListNode temp = top; SkipListNode tempDown = null; SkipListNode tempNextDown = null; int tempLevel = level; while(tempLevel -- != 0){ skipListNode = createNode(Integer.MIN_VALUE); temp = skipListNode; skipListNode = createNode(Integer.MAX_VALUE); temp.setNext(skipListNode); temp.setDownNext(tempDown); temp.getNext().setDownNext(tempNextDown); tempDown = temp; tempNextDown = temp.getNext(); } top = temp; } //隨機產生數k,k層下的都需要將值插入 public int randomLevel(){ int k = 1; while(new Random().nextInt()%2 == 0){ k ++; } return k > level ? level : k; } //查找 public SkipListNode find(int value){ SkipListNode node = top; while(true){ while(node.getNext().getValue() < value){ node = node.getNext(); } if(node.getDownNext() == null){ //返回要查找的節點的前一個節點 return node; } node = node.getDownNext(); } } //刪除一個節點 public boolean delete(int value){ int tempLevel = level; SkipListNode skipListNode = top; SkipListNode temp = skipListNode; boolean flag = false; while(tempLevel -- != 0){ while(temp.getNext().getValue() < value){ temp = temp.getNext(); } if(temp.getNext().getValue() == value){ temp.setNext(temp.getNext().getNext()); flag = true; } temp = skipListNode.getDownNext(); } return flag; } //插入一個節點 public void insert(int value){ SkipListNode skipListNode = null; int k = randomLevel(); SkipListNode temp = top; int tempLevel = level; SkipListNode tempNode = null; //當在第n行插入后,在第n - 1行插入時需要將第n行backTempNode的DownNext域指向第n - 1的域 SkipListNode backTempNode = null; int flag = 1; while(tempLevel-- != k){ temp = temp.getDownNext(); } tempLevel++; tempNode = temp; //小於k層的都需要進行插入 while(tempLevel-- != 0){ //在第k層尋找要插入的位置 while(tempNode.getNext().getValue() < value){ tempNode = tempNode.getNext(); } skipListNode = createNode(value); //如果是頂層 if(flag != 1){ backTempNode.setDownNext(skipListNode); } backTempNode = skipListNode; skipListNode.setNext(tempNode.getNext()); tempNode.setNext(skipListNode); flag = 0; tempNode = tempNode.getDownNext(); } } //創建一個節點 private SkipListNode createNode(int value){ SkipListNode node = new SkipListNode(); node.setValue(value); return node; } }
測試類
import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; public class SkipListTest { public static void main(String[] args) { long startTime = 0; long endTime = 0; SkipList skipList = new SkipList(12); Set<Integer> set = new HashSet(); List<Integer> list = new LinkedList<>(); //測試跳躍表性能 startTime = System.currentTimeMillis(); for (int i = 1; i < 1000; i++) { skipList.insert(i); } endTime = System.currentTimeMillis(); System.out.printf("createSkipList:%d\n", endTime - startTime); startTime = System.currentTimeMillis(); System.out.printf("find(555):%d\n", skipList.find(55555).getNext().getValue()); endTime = System.currentTimeMillis(); System.out.printf("skipListFindTime:%d\n\n", endTime - startTime); //測試LinkedList性能 startTime = System.currentTimeMillis(); for (int i = 1; i < 100; i++) { list.add(i); } endTime = System.currentTimeMillis(); System.out.printf("createList:%d\n", endTime - startTime); startTime = System.currentTimeMillis(); System.out.printf("find(555):%b\n", list.contains(55555)); endTime = System.currentTimeMillis(); System.out.printf("linkedListFindTime:%d\n\n", endTime - startTime); //測試hashSet性能 startTime = System.currentTimeMillis(); for (int i = 1; i < 100000; i++) { set.add(i); } endTime = System.currentTimeMillis(); System.out.printf("createSet:%d\n", endTime - startTime); startTime = System.currentTimeMillis(); System.out.printf("find(555):%b\n", set.contains(55555)); endTime = System.currentTimeMillis(); System.out.printf("hashSetFindTime:%d\n", endTime - startTime); } }
Redis中的有序集合
Redis使用跳躍表作為有序集合鍵的的底層實現,如果一個有序集合包含的元素數量比較多,又或者有序集合中元素的成員是比較長的字符串時Redis就會使用跳躍表來作為有序集合鍵的底層實現。Redis只在兩個地方用到了跳躍表,一個是實現有序集合鍵,另一個是在集群節點中用作內部數據結構。
Redis為什么要使用跳躍表而不是紅黑樹?
其中,插入、刪除、查找以及迭代輸出有序序列這幾個操作,紅黑樹也可以完成,時間復雜度跟跳表是一樣的。但是,按照區間來查找數據這個操作,紅黑樹的效率沒有跳表高。對於按照區間查找數據這個操作,跳表可以做到 O(logn) 的時間復雜度定位區間的起點,然后在原始鏈表中順序往后遍歷就可以了。這樣做非常高效。當然,Redis 之所以用跳表來實現有序集合,還有其他原因,比如,跳表更容易代碼實現。雖然跳表的實現也不簡單,但比起紅黑樹來說還是好懂、好寫多了,而簡單就意味着可讀性好,不容易出錯。還有,跳表更加靈活,它可以通過改變索引構建策略,有效平衡執行效率和內存消耗。不過,跳表也不能完全替代紅黑樹。因為紅黑樹比跳表的出現要早一些,很多編程語言中的 Map 類型都是通過紅黑樹來實現的。我們做業務開發的時候,直接拿來用就可以了,不用費勁自己去實現一個紅黑樹,但是跳表並沒有一個現成的實現,所以在開發中,如果你想使用跳表,必須要自己實現。
基本命令
zadd key score element #如果元素存在,則改變它的分數
> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
實戰
排行榜功能在很多應用都是普遍存在的,例如音樂排行榜、電影排行榜、文章排行榜、熱門視頻等等。類似這種場景就可以使用有序集合來實現。
可以使用zadd去添加元素和初始分數,然后使用zincrby實現分數的更新,使用zrem將一些元素刪除榜外,使用zrangebyscore獲取一定范圍分數的榜單等等。
那么這里最核心的就是分數具體代表什么,例如最新榜單可以使用timeStamp作為分數,銷售量可以使用saleCount,關注量使用followCount。然后使用相關的API進行業務操作,也可以對多個集合進行匯總根據一定的規則作為類似綜合排序的結果。