redis中sortset跳錶資料結構
轉自
跳躍列表是在很多應用中有可能替代平衡樹而作為實現方法的一種資料結構。跳躍列表的演算法有同平衡樹一樣的漸進的預期時間邊界,並且更簡單、更快速和使用更少的空間。----by 發明者
像是redis中有序集合就使用到了跳躍表。
場景:商品總數量有幾十萬件,對應資料庫商品表的幾十萬條記錄。需要根據不同欄位正序或者倒敘做精確或全量查詢,而且效能有硬性要求。
如果是按照商品名稱精確查詢還好辦,可以直接從資料庫查出來,最多也就上百條記錄。
如果是沒有商品名稱的全量查詢怎麼辦?總不可能把資料庫裡的所有記錄全查出來吧,而且還要支援不同欄位的排序。
所以,只能提前在記憶體中儲存有序的全量商品集合,每一種排序方式都儲存成獨立的集合,每次請求的時候按照請求的排序種類,返回對應的集合。
- 比如按價格欄位排序的集合:
- 比如按銷量欄位排序的集合:
商品列表是線性的,最容易表達線性結構的自然是陣列和連結串列。可是,無論是陣列還是連結串列,在插入新商品的時候,都會存在效能問題。
按照商品等級排序的集合為例,如果使用陣列,插入新商品的方式如下:
如果要插入一個價格是3的商品,首先要知道這個商品應該插入的位置。使用二分查詢可以最快定位,這一步時間複雜度是O(logN)。
插入過程中,原陣列中所有大於3的商品都要右移,這一步時間複雜度是O(N)。所以總體時間複雜度是O(N)。
如果使用連結串列,插入新商品的方式如下:
如果要插入一個價格是3的商品,首先要知道這個商品應該插入的位置。連結串列無法使用二分查詢,只能和原連結串列中的節點逐一比較大小來確定位置。這一步的時間複雜度是O(N)。
插入的過程倒是很容易,直接改變節點指標的目標,時間複雜度O(1)。因此總體的時間複雜度也是O(N)。
這對於擁有幾十萬商品的集合來說,這兩種方法顯然都太慢了。
因此引入一種不同的資料結構----跳躍表
首先,應該要了解跳躍表的性質;
- 由很多層結構組成;
- 每一層都是一個有序的連結串列,排列順序為由高層到底層,都至少包含兩個連結串列節點,分別是前面的head節點和後面的nil節點;
- 最底層的連結串列包含了所有的元素;
- 如果一個元素出現在某一層的連結串列中,那麼在該層之下的連結串列也全都會出現(上一層的元素是當前層的元素的子集);
- 連結串列中的每個節點都包含兩個指標,一個指向同一層的下一個連結串列節點,另一個指向下一層的同一個連結串列節點;
因此在前文中的商品列表排序中,可以取出若干關鍵節點,在新節點插入時先去比較關鍵節點,逐層下沉,最終在原連結串列中插入。這樣,比較的次數下降了一半,節點數量少的情況下效果不明顯,但在1w甚至10w個節點的連結串列下,效能的提升會非常明顯。
搜尋:
其基本原理就是從最高層的連結串列節點(關 鍵節點)開始,如果比當前節點要大和比當前層的下一個節點要小,那麼則往下找,也就是和當前層的下一層的節點的下一個節點進行比較,以此類推,一直找到最底層的最後一個節點,如果找到則返回,反之則返回空。
1 find(x) 2 { 3 p = top; 4 while (1) { 5 while (p->next->key < x) 6 p = p->next; 7 if (p->down == NULL) 8 return p->next; 9 p = p->down; 10 } 11 }
插入節點 :
既然要插入,首先需要確定插入的層數,這裡有不一樣的方法。1. 看到一些部落格寫的是拋硬幣,只要是正面就累加,直到遇見反面才停止,最後記錄正面的次數並將其作為要新增新元素的層;2. 還有就是一些部落格裡面寫的統計概率,先給定一個概率p,產生一個0到1之間的隨機數,如果這個隨機數小於p,則將高度加1,直到產生的隨機數大於概率p才停止,根據給出的結論,當概率為1/2或者是1/4的時候,整體的效能會比較好(其實當p為1/2的時候,也就是拋硬幣的方法)。
當確定好要插入的層數以後,則需要將元素都插入到從最底層到第k層
拋硬幣法:
新節點和各層索引節點逐一比較,確定原連結串列的插入位置。O(logN)
把索引插入到原連結串列。O(1)
利用拋硬幣的隨機方式,決定新節點是否提升為上一級索引。結果為“正”則提升並繼續拋硬幣,結果為“負”則停止。O(logN)
總體上,跳躍表插入操作的時間複雜度是O(logN),而這種資料結構所佔空間是2N,既空間複雜度是 O(N)。
刪除節點
在各個層中找到包含指定值的節點,然後將節點從連結串列中刪除即可,如果刪除以後只剩下頭尾兩個節點,則刪除這一層。
自上而下,查詢第一次出現節點的索引,並逐層找到每一層對應的節點。O(logN)
刪除每一層查詢到的節點,如果該層只剩下1個節點,刪除整個一層(原連結串列除外)。O(logN)
總體上,跳躍表刪除操作的時間複雜度是O(logN)。
程式碼示例:
1 /*************************** SkipList.java *********************/ 2 3 import java.util.Random; 4 5 public class SkipList<T extends Comparable<? super T>> { 6 private int maxLevel; 7 private SkipListNode<T>[] root; 8 private int[] powers; 9 private Random rd = new Random(); 10 SkipList() { 11 this(4); 12 } 13 SkipList(int i) { 14 maxLevel = i; 15 root = new SkipListNode[maxLevel]; 16 powers = new int[maxLevel]; 17 for (int j = 0; j < maxLevel; j++) 18 root[j] = null; 19 choosePowers(); 20 } 21 public boolean isEmpty() { 22 return root[0] == null; 23 } 24 public void choosePowers() { 25 powers[maxLevel-1] = (2 << (maxLevel-1)) - 1; // 2^maxLevel - 1 26 for (int i = maxLevel - 2, j = 0; i >= 0; i--, j++) 27 powers[i] = powers[i+1] - (2 << j); // 2^(j+1) 28 } 29 public int chooseLevel() { 30 int i, r = Math.abs(rd.nextInt()) % powers[maxLevel-1] + 1; 31 for (i = 1; i < maxLevel; i++) 32 if (r < powers[i]) 33 return i-1; // return a level < the highest level; 34 return i-1; // return the highest level; 35 } 36 // make sure (with isEmpty()) that search() is called for a nonempty list; 37 public T search(T key) { 38 int lvl; 39 SkipListNode<T> prev, curr; // find the highest nonnull 40 for (lvl = maxLevel-1; lvl >= 0 && root[lvl] == null; lvl--); // level; 41 prev = curr = root[lvl]; 42 while (true) { 43 if (key.equals(curr.key)) // success if equal; 44 return curr.key; 45 else if (key.compareTo(curr.key) < 0) { // if smaller, go down, 46 if (lvl == 0) // if possible 47 return null; 48 else if (curr == root[lvl]) // by one level 49 curr = root[--lvl]; // starting from the 50 else curr = prev.next[--lvl]; // predecessor which 51 } // can be the root; 52 else { // if greater, 53 prev = curr; // go to the next 54 if (curr.next[lvl] != null) // non-null node 55 curr = curr.next[lvl]; // on the same level 56 else { // or to a list on a lower level; 57 for (lvl--; lvl >= 0 && curr.next[lvl] == null; lvl--); 58 if (lvl >= 0) 59 curr = curr.next[lvl]; 60 else return null; 61 } 62 } 63 } 64 } 65 public void insert(T key) { 66 SkipListNode<T>[] curr = new SkipListNode[maxLevel]; 67 SkipListNode<T>[] prev = new SkipListNode[maxLevel]; 68 SkipListNode<T> newNode; 69 int lvl, i; 70 curr[maxLevel-1] = root[maxLevel-1]; 71 prev[maxLevel-1] = null; 72 for (lvl = maxLevel - 1; lvl >= 0; lvl--) { 73 while (curr[lvl] != null && curr[lvl].key.compareTo(key) < 0) { 74 prev[lvl] = curr[lvl]; // go to the next 75 curr[lvl] = curr[lvl].next[lvl]; // if smaller; 76 } 77 if (curr[lvl] != null && key.equals(curr[lvl].key)) // don't 78 return; // include duplicates; 79 if (lvl > 0) // go one level down 80 if (prev[lvl] == null) { // if not the lowest 81 curr[lvl-1] = root[lvl-1]; // level, using a link 82 prev[lvl-1] = null; // either from the root 83 } 84 else { // or from the predecessor; 85 curr[lvl-1] = prev[lvl].next[lvl-1]; 86 prev[lvl-1] = prev[lvl]; 87 } 88 } 89 lvl = chooseLevel(); // generate randomly level 90 newNode = new SkipListNode<T>(key,lvl+1); // for newNode; 91 for (i = 0; i <= lvl; i++) { // initialize next fields of 92 newNode.next[i] = curr[i]; // newNode and reset to newNode 93 if (prev[i] == null) // either fields of the root 94 root[i] = newNode; // or next fields of newNode's 95 else prev[i].next[i] = newNode; // predecessors; 96 } 97 } 98 }
有的時候跳躍表常用來代替紅黑樹,因為跳躍表在維持結構平衡時成本較低,完全靠隨機,而紅黑樹這一類查詢樹每一次新增刪除後都需要rebalance。