自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Go 語言下的 Redis 跳表設(shè)計與實現(xiàn)

系統(tǒng) Redis
本文將詳細介紹如何使用 Go 語言從零開始實現(xiàn)一個類似于 Redis 的跳表。我們將探討跳表的基本原理、設(shè)計思路以及具體的實現(xiàn)方法。

在現(xiàn)代高性能數(shù)據(jù)庫和緩存系統(tǒng)中,跳表(Skip List)作為一種高效的有序數(shù)據(jù)結(jié)構(gòu),被廣泛應(yīng)用于快速查找、插入和刪除操作。Redis 是一個開源的鍵值對存儲系統(tǒng),它支持多種數(shù)據(jù)類型,并以其出色的性能而聞名。其中,Redis 使用了跳表來實現(xiàn)有序集合(Sorted Set),以保證其高效的數(shù)據(jù)處理能力。

本文將詳細介紹如何使用 Go 語言從零開始實現(xiàn)一個類似于 Redis 的跳表。我們將探討跳表的基本原理、設(shè)計思路以及具體的實現(xiàn)方法。通過本篇文章的學(xué)習(xí),你不僅能夠了解跳表的工作機制,還能夠在實際項目中應(yīng)用這一強大的數(shù)據(jù)結(jié)構(gòu)。

定義基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)

redis中跳表通過score標(biāo)識元素的大小,通過redis obj維護節(jié)點的信息,與此同時為了保證查詢的高效,它會為每個節(jié)點維護一份隨機高度的索引記錄當(dāng)前節(jié)點的某個前驅(qū)節(jié)點:

對應(yīng)我們給出節(jié)點的代碼實現(xiàn):

/*
*
跳表節(jié)點的定義
*/
type zskiplistNode struct {
 //記錄元素的redis指針
 obj *robj
 //記錄當(dāng)前元素的數(shù)值,代表當(dāng)前元素的優(yōu)先級
 score float64
 //指向當(dāng)前元素的前驅(qū)節(jié)點,即小于當(dāng)前節(jié)點的元素
 backward *zskiplistNode
 //用一個zskiplistLevel數(shù)組維護本屆點各層索引信息
 level    []zskiplistLevel
}

zskiplistLevel的代碼實現(xiàn)比較簡單,通過forward 記錄本層索引的前驅(qū)節(jié)點,并用span維護當(dāng)前節(jié)點需要跨幾步才能走到該前驅(qū)節(jié)點:

type zskiplistLevel struct {
 //記錄本層索引的前驅(qū)節(jié)點的指針
 forward *zskiplistNode
 //標(biāo)識節(jié)點的本層索引需要跨幾步才能到達該節(jié)點
 span    int64
}

通過上述概念構(gòu)成無數(shù)個節(jié)點即稱為跳表,如下圖所示,各個節(jié)點都用一個level數(shù)組記錄本層索引到前驅(qū)節(jié)點的地址和跨度,而跳表也用一個header和tail指針維護跳表的頭尾節(jié)點:

對應(yīng)的跳表結(jié)構(gòu)體的代碼如下所示:

type zskiplist struct {
 //指向跳表的頭節(jié)點
 header *zskiplistNode
 //指向跳表的尾節(jié)點
 tail *zskiplistNode
 //維護跳表的長度
 length int64
 //維護跳表當(dāng)前索引的最高高度
 level int
}

實現(xiàn)初始化方法

對應(yīng)的我們也給出跳表的初始化代碼,大體邏輯是初始化跳表之后,初始化一個全空的索引和維護跳表的各種初始化信息,對應(yīng)的筆者也對此代碼做了詳盡的注釋,讀者可自行參閱:

func zslCreate() *zskiplist {
 var j int
 //初始化跳表結(jié)構(gòu)體
 zsl := new(zskiplist)
 //索引默認高度為1
 zsl.level = 1
 //跳表元素初始化為0
 zsl.length = 0
 //初始化一個頭節(jié)點socre為0,元素為空
 zsl.header = zslCreateNode(ZSKIPLIST_MAXLEVEL, 0, nil)

 /**
 基于跳表最大高度32初始化頭節(jié)點的索引,
 使得前驅(qū)指針指向null 跨度也設(shè)置為0
 */
 for j = 0; j < ZSKIPLIST_MAXLEVEL; j++ {
  zsl.header.level[j].forward = nil
  zsl.header.level[j].span = 0
 }
 //頭節(jié)點的前驅(qū)節(jié)點指向null,代表頭節(jié)點之前沒有任何元素
 zsl.header.backward = nil
 //初始化尾節(jié)點
 zsl.tail = nil
 return zsl
}

跳表插入操作

插入新節(jié)點時,本質(zhì)上就是通過各層索引找到小于插入節(jié)點x的score的最大值,并記錄到update數(shù)組中,同時將頭節(jié)點跨到update數(shù)組元素的跨度值記錄到rank數(shù)組中,如下圖所示,假如我們插入節(jié)點1.5,那么對應(yīng)各層索引的在update和rank兩個數(shù)組中維護的信息是:

  • level2級中update記錄header節(jié)點,所以跨度為0。
  • level1級中update記錄的是節(jié)點1,跨度為1。

然后基于此信息將x插入:

對應(yīng)的代碼和上述圖解邏輯一致,對應(yīng)的實現(xiàn)細節(jié)筆者都做好了標(biāo)注:

func zslInsert(zsl *zskiplist, score float64, obj *robj) *zskiplistNode {
 //創(chuàng)建一個update數(shù)組,記錄插入節(jié)點每層索引中小于該score的最大值
 update := make([]*zskiplistNode, ZSKIPLIST_MAXLEVEL)
 //記錄各層索引走到小于score最大節(jié)點的跨區(qū)
 rank := make([]int64, ZSKIPLIST_MAXLEVEL)
 //x指向跳表走節(jié)點
 x := zsl.header
 var i int
 //從跳表當(dāng)前最高層索引開始,查找每層小于當(dāng)前score的節(jié)點的最大值節(jié)點
 for i = zsl.level - 1; i >= 0; i-- {
  //如果當(dāng)前索引是最高層索引,那么rank從0開始算
  if i == zsl.level-1 {
   rank[i] = 0
  } else { //反之本層索引直接從上一層的跨度開始往后查找
   rank[i] = rank[i+1]
  }
  /**
  如果前驅(qū)節(jié)點不為空,且符合以下條件,則指針前移:
  1. 節(jié)點小于當(dāng)前插入節(jié)點的score
  2. 節(jié)點score一致,且元素值小于或者等于當(dāng)前score
  */
  if x.level[i].forward != nil &&
   (x.level[i].forward.score < score || (x.level[i].forward.score == score && x.level[i].forward.obj.String() < obj.String())) {
   //記錄本層索引前移跨度
   rank[i] += x.level[i].span
   //索引指針先前移動
   x = x.level[i].forward

  }
  //記錄本層小于當(dāng)前score的最大節(jié)點
  update[i] = x
 }
 //隨機生成新插入節(jié)點的索引高度
 level := zslRandomLevel()
 /**
 如果大于當(dāng)前索引高度,則進行初始化,將這些高層索引的update數(shù)組都指向header節(jié)點,跨度設(shè)置為跳表中的元素數(shù)
 意為這些高層索引小于插入節(jié)點的最大值就是header
 */
 if level > zsl.level {
  for i := zsl.level; i < level; i++ {
   rank[i] = 0
   update[i] = zsl.header
   update[i].level[i].span = zsl.length
  }
  //更新一下跳表索引的高度
  zsl.level = level
 }
 //基于入?yún)⑸梢粋€節(jié)點
 x = zslCreateNode(level, score, obj)
 //從底層到當(dāng)前最高層索引處理節(jié)點關(guān)系
 for i = 0; i < level; i++ {
  //將小于當(dāng)前節(jié)點的最大節(jié)點的forward指向插入節(jié)點x,同時x指向這個節(jié)點的前向節(jié)點
  x.level[i].forward = update[i].level[i].forward
  update[i].level[i].forward = x
  //維護x和update所指向節(jié)點之間的跨度信息
  x.level[i].span = update[i].level[i].span - (rank[0] - rank[i])
  update[i].level[i].span = rank[0] - rank[i] + 1
 }
 /**
 考慮到當(dāng)前插入節(jié)點生成的level小于當(dāng)前跳表最高level的情況
 該邏輯會將這些區(qū)間的update索引中的元素到其前方節(jié)點的跨度+1,即代表這些層級索引雖然沒有指向x節(jié)點,
 但因為x節(jié)點插入的緣故跨度要加1
 */
 for i = level; i < zsl.level; i++ {
  update[i].level[i].span++
 }
 //如果1級索引是header,則x后繼節(jié)點不指向該節(jié)點,反之指向
 if update[0] == zsl.header {
  x.backward = nil
 } else {
  x.backward = update[0]
 }
 //如果x前向節(jié)點不為空,則讓前向節(jié)點指向x
 if x.level[0].forward != nil {
  x.level[0].forward.backward = x
 } else {//反之說明x是尾節(jié)點,tail指針指向它
  zsl.tail = x
 }
 //維護跳表長度信息
 zsl.length++
 return x
}

跳表查詢操作

有了插入操作的基礎(chǔ)后,查詢操作實現(xiàn)也比較容易了,即從頭節(jié)點的最高索引開始不斷向前找,如果沒有則往下一級索引前向找,找到后返回經(jīng)過的跨度即可。

如下圖,我們希望查找元素2,直接從頭節(jié)點的2級索引開始看,就是元素2比對一致,返回跨度2,即跨2步就能到達:

對應(yīng)代碼如下,和筆者說明一致,這里筆者也做了詳盡的標(biāo)注提供參考:

func zslGetRank(zsl *zskiplist, score float64, obj *robj) int64 {
 var rank int64
 //從索引最高節(jié)點開始進行查找
 x := zsl.header
 for i := zsl.level - 1; i >= 0; i-- {
  //如果前向節(jié)點不為空且score小于查找節(jié)點,或者score相等,但是元素字符序比值小于或者等于則前移,同時用rank記錄跨度
  for x.level[i].forward != nil &&
   (x.level[i].forward.score < score || (x.level[i].forward.score == score && x.level[i].forward.obj.String() <= obj.String())) {
   rank += x.level[i].span
   x = x.level[i].forward
  }
  //上述循環(huán)結(jié)束,比對一直,則返回經(jīng)過的跨度
  if x.obj != nil && x.obj.String() == obj.String() {
   return rank
  }
 }
 return 0
}

跳表刪除操作

刪除操作本質(zhì)上也是找到要刪除節(jié)點索引的前后節(jié)點,然后將這些節(jié)點關(guān)聯(lián),并修改其之間跨度,如下圖我們要刪除1.5節(jié)點,對應(yīng)各層查找結(jié)果為:

  • 3級索引找到頭節(jié)點,因為前方不是1.5的節(jié)點索引,直接跨度減1即。
  • 2級索引找到頭節(jié)點,前方就是1.5的索引,刪除掉后跨度改為header索引到1.5+1.5到前向節(jié)點跨度減去1,這里的減去1代表刪除了節(jié)點1.5的跨步。
  • 1級索引同2級索引,不多做贅述。

對應(yīng)的代碼示例如下,整體邏輯和筆者描述基本一致,先通過update找到刪除節(jié)點x的前一個元素,然后調(diào)用zslDeleteNode進行刪除:

func zslDelete(zsl *zskiplist, score float64, obj *robj) int64 {
 update := make([]*zskiplistNode, ZSKIPLIST_MAXLEVEL)
 //找到每層索引要刪除節(jié)點的前一個節(jié)點
 x := zsl.header
 for i := zsl.level - 1; i >= 0; i-- {
  for x.level[i].forward != nil &&
   (x.level[i].forward.score < score || (x.level[i].forward.score == score && x.level[i].forward.obj.String() < obj.String())) {
   x = x.level[i].forward
  }
  update[i] = x
 }
 //查看1級索引前面是否就是要刪除的節(jié)點,如果是則直接調(diào)用zslDeleteNode刪除節(jié)點,并斷掉前后節(jié)點關(guān)系
 x = x.level[0].forward
 if x != nil && x.obj.String() == obj.String() {
  zslDeleteNode(zsl, x, update)
  return 1
 }
 return 0
}

對應(yīng)zslDeleteNode細節(jié)就如筆者上圖所講解的步驟,讀者可參考注釋進行閱讀:

func zslDeleteNode(zsl *zskiplist, x *zskiplistNode, update []*zskiplistNode) {

 var i int
 for i = 0; i < zsl.level; i++ {
  /*
  如果索引前方就是刪除節(jié)點,當(dāng)前節(jié)點span為:
  當(dāng)前節(jié)點到x +x到x前向節(jié)點 -1
   */
  if update[i].level[i].forward == x {
   update[i].level[i].span += x.level[i].span - 1
   update[i].level[i].forward = x.level[i].forward
  } else {
   //反之說明該節(jié)點前方不是x的索引,直接減去x的跨步1即
   update[i].level[i].span -= 1
  }
 }
 //維護刪除后的節(jié)點前后關(guān)系
 if x.level[0].forward != nil {
  x.level[0].forward.backward = x.backward
 } else {
  zsl.tail = x.backward
 }
 //將全空層的索引刪除
 for zsl.level > 1 && zsl.header.level[zsl.level-1].forward == nil {
  zsl.level--
 }
 //維護跳表節(jié)點信息
 zsl.length--

小結(jié)

通過本文的詳細講解,我們從零開始使用 Go 語言實現(xiàn)了一個類似于 Redis 的跳表。我們首先介紹了跳表的基本原理和設(shè)計思路,然后逐步實現(xiàn)了跳表的各種核心操作,包括插入、查找和刪除。最后,我們對跳表的性能進行了分析,并探討了其在 Redis 有序集合和其他場景中的應(yīng)用。

責(zé)任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關(guān)推薦

2025-01-06 08:10:00

Redis跳表索引

2020-12-28 07:33:21

SkipListJava跳表

2021-09-30 09:21:28

Go語言并發(fā)編程

2017-06-27 09:43:43

Python機器學(xué)習(xí)

2023-05-17 00:15:11

TCCXA模式

2025-03-20 09:54:47

2025-02-25 09:29:34

2021-07-30 07:28:15

WorkerPoolGo語言

2021-04-07 09:02:49

Go 語言變量與常量

2021-04-13 07:58:42

Go語言函數(shù)

2023-03-27 00:20:48

2022-05-09 10:36:05

PythonPyScript開發(fā)者

2021-04-20 09:00:48

Go 語言結(jié)構(gòu)體type

2025-03-27 00:45:00

2023-03-21 07:57:37

Go語言設(shè)計模式

2021-10-20 07:18:51

Go語言設(shè)計

2021-07-09 11:59:25

Redis有序集合

2024-11-04 06:00:00

redis雙向鏈表

2015-12-21 14:56:12

Go語言Http網(wǎng)絡(luò)協(xié)議

2022-05-19 14:14:26

go語言限流算法
點贊
收藏

51CTO技術(shù)棧公眾號