歡迎關注公衆號《後臺開發探索之旅》。
有序集合
日常開發經常需要對數據進行排序,針對不同的場景,採用特定的排序方法,比如:
(1)數組排序:將數值存入數組中,對數組進行冒泡、快排等排序方法,得到一個有序數組
(2)二叉搜索:構造二叉平衡樹,從根結點開始向左向右搜索,每次查找規模減半,最終找到目標節點
(3)最小根堆:同樣將數值存入數組中,對數組進行堆排序,保持最小值始終在堆頂,從而得到最小值
(4)有序鏈表:將數值存入鏈表中,保持鏈表有序,類似於數組
每一類數據結構都有其適合的場景,存在一些缺點,如下:
(1)數組排序:更新複雜度高,若插入首結點,後面所有元素都需要後移一位
(2)二叉搜索:只適合查找和更新,不能獲取排序列表
(3)最小根堆:只適合獲取最小值
(4)有序鏈表:查找複雜度高,每次都需要從頭遍歷
理想的有序集合是:
(1)查找、插入、刪除的複雜度都小於O(N),即不需要遍歷所有結點
(2)可以獲取排序列表,以及某個結點的排名
(3)可以根據數值查找排名,也可以根據排名查找數值
跳錶(skiplist)就可以滿足這些苛刻的要求,付出的代價是增加一倍的空間消耗,典型的空間換時間。
跳錶原理
跳錶實際上是在有序鏈表的基礎上實現的,比如有數值1~10,用有序鏈表表示就是:
1,2,3,4,5,6,7,8,9,10
有序鏈表的缺點是每次都要從頭遍歷,比如查找9,要從1開始查找,那麼能不能跳着查找,比如1,3,5,7,9這種方式呢,增加1層鏈表:
1,3,5,7,10
1,2,3,4,5,6,7,8,9,10
先從第2層開始查找,順序爲1,3,5,7,後面是10,不能繼續向右了,進入下一層,從7接着向右,找到8,9
如果數據量特別大,有幾百萬條數據,還是很慢,能不能再快點,可以的,增加層數到20層,每次從20層開始,本層查找完畢,進入下一層繼續查找。
當跳錶設置爲25層時,無論數據量有多大,查找次數穩定在50次以內,性能非常優秀。
redis的跳錶實現
redis基於自身功能需求,擴展了跳錶的功能。每個元素有name和score兩個屬性,name是元素唯一標識符,根據score排序。支持以下操作:
(1)插入name和score,name不能相同,score可以相同; 先根據score排序,若score相同,根據name排序
(2)根據name,刪除元素,獲取元素score,獲取元素rank排名
(3)獲取指定score區間的元素排序列表; 比如傳入1和5,返回1,2,3,4,5
(4)獲取指定rank區間的元素排序列表; 比如獲取前10名的元素列表
golang實現
考慮到redis跳錶的強大,用golang實現了其功能,項目地址爲:
https://github.com/throne-developer/skiplist
測試代碼:
func TestSkipListSimple(t *testing.T) {
sl := New()
sl.Insert("A", 1)
sl.Insert("B", 2)
sl.Insert("C", 3)
sl.Insert("D1", 6)
sl.Insert("D2", 6)
sl.Insert("D3", 6)
if elem := sl.Find("D1"); elem != nil {
fmt.Println("Find D1, score=", elem.Score())
}
if rank, ok := sl.GetRank("D1"); ok {
fmt.Println("D1 rank is ", rank)
}
if score, ok := sl.GetScore("D1"); ok {
fmt.Println("D1 score is ", score)
}
if elem := sl.FindByRank(5); elem != nil {
fmt.Println("FindByRank 5, name=", elem.Name())
}
for elem := sl.FindGreaterOrEqual(3); elem != nil; elem = elem.Next() {
fmt.Println("FindGreaterOrEqual 3, name=", elem.Name())
}
sl.Delete("A")
}
在此接口基礎上做簡單的封裝,即可實現redis的zset命令,下一篇將介紹golang版本的skiplist內部實現機制。