日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

skiplist跳表的 实现

發布時間:2023/11/27 生活经验 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 skiplist跳表的 实现 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

        • 前言
        • 跳表結構
        • 時間復雜度
        • 空間復雜度
        • 高效的動態插入和刪除
        • 跳表索引的動態更新
        • 總結
        • 詳細實現

前言

rocksdb 的memtable中默認使用跳表數據結構對有序數據進行的管理,為什么呢?

同時redis 也用跳表作為管理自己有序集合的數據結構,為什么他們不選擇用紅黑樹來管理(同樣能夠提供高效的插入,查找,刪除操作,而且各種語言都已經封裝好了很多輪子),就選擇跳表來實現?

今天就來仔細探討一下這個數據結構。

跳表結構

對于一個單鏈表來說,即使鏈表中存儲的數據結構是有序的,想要查找一個元素也需要從頭到尾進行查找,時間復雜度是O(n)。


提高查效率的一種辦法就是建立索引,對鏈表建立一級索引,每兩個節點提取一個索引節點到上一級,把抽取出來的一級叫做索引。如下圖,down就是索引節點指向節點的指針:

此時如果我們想要查找某個節點,比如18。可以先在索引層遍歷,當遍歷到索引層節點值為13時,發現沒有next指針了,此時下降到原始節點層,繼續遍歷。這個時候只需要遍歷一個節點就能訪問到數值為18的節點了。

這樣的話原來要找節點18,需要遍歷8個節點,此時只需要遍歷6個節點了,查找效率提高了。那如果我們再增加一級索引,效率會不會更高呢?還是在第一級索引節點的基礎上再創建一級索引,如下圖:

在查找部分節點的情況下效率能夠更高,因為這里舉例的數據量較小,查看如下數據,有64個原始節點,按照如上的思路建立了五級索引。

此時查找節點62,原始鏈表需要遍歷62個節點,此時只需要遍歷11個節點即可訪問到,在數據量較為龐大的情況下效率提升非常明顯。

時間復雜度

單鏈表中查找一個節點的效率是O(n),那么跳表中查找一個節點的時間復雜度是多少呢?
按照我們上面所說,每兩個原始節點抽取為一個索引節點的思路。

假設現在有n個節點,每兩個節點抽取一個索引節點,那么第一級索引節點的個數:n/2,第二級索引節點:n/4,第三節索引節點:n/8,依次第k級索引節點:n/(2^k)

假設索引有h級,最高級的索引有2個結點。通過上面的公式,我們可以得到n/(2h)=2,從而求得h=log2n-1。如果包含原始鏈表這一層,整個跳表的高度就是log2n。
我們在跳表中查詢某個數據的時候,如果每一層都要遍歷m個結點,那在跳表中查詢一個數據的時間復雜度就是O(m*logn)。

如何確定m的數值是多少呢,按照上面的索引結構,從最頂層的索引層開始遍歷一直到最底層,每一級索引最多需要遍歷3個節點。
證明如下:

  • 假設我們要查找的數據是x,在第k級索引中
  • 遍歷到y結點之后,發現x大于y,小于后面的結點z,所以我們通過y的down指針,從第k級索引下降到第k-1級索 引
  • 在第k-1級索引中,y和z之間只有3個結點(包含y和z),即我們在K-1級索引中最多只需要遍歷3個結點,依次類推,每一級索引都最多只需要遍歷3個結 點。


所以我們可以得到m=3這樣的一個結論,則在跳表中查詢任意一個節點的時間復雜度都為O(logn),效率和二分查找一樣。

但是問題也很明顯,索引節點消耗內存空間,這是以空間換時間的方式來達到優化的目的,接下來我們看看空間的消耗

空間復雜度

假設原始鏈表大小為n,我們前面也說過之上的索引節點的個數依次為:
第一級索引節點的個數:n/2,第二級索引節點:n/4,第三節索引節點:n/8,依次第k級索引節點:n/(2^k),直到剩下兩個索引節點

這幾級索引節點的總和:n/2 + n/4 + n/8 +… 8 + 4 +2 = n -2
可以看出跳表的空間復雜度是O(n)。也就是說,如果將包含n個結點的單鏈表構造成跳表,我們需要額外再用 接近n個結點的存儲空間。那我們有沒有辦法降低索引占用的內存空間呢?

之前我們是每兩個節點抽取一個索引節點,同樣我們可以每三個節點抽取一個索引節點,示意圖如下:

依次總的索引節點的個數為:n/3 + n/9 + n/27 +… + 9 + 3 = n /2
雖然還是O(n)的空間復雜度,但是整體比上面的抽取方式少占用一般的空間。且實際開發過程中,原始鏈表中存儲的大都是數據量龐大的數據,索引節點僅僅存儲一些關鍵數據以及指針,基本的空間消耗并不會很大,可以忽略不計得。

高效的動態插入和刪除

插入數據和查找數據的時間復雜度一樣,單鏈表的插入性能消耗O(n)在查找插入位置上,而真正的插入只需要O(1)的時間。同樣,跳表的插入也是消耗在查找的時間復雜度上O(logn)。

刪除的時候,我們在找到原始鏈表中的節點之后,如果該節點還出現在索引節點之中,我們除了要刪除原始鏈表中的節點,還需要刪除索引層中的節點。

跳表索引的動態更新

當我們不停地往跳表中插入數據時,如果我們不更新索引,就有可能出現某2個索引結點之間數據非常多的情況。極端情況下,跳表還會退化成單鏈表。
如下這種情況:

紅黑樹、AVL樹這樣平衡二叉樹,它們是通過左右旋的方式保持左右子樹的大小平衡(如果不了解也沒關系,我們后面會講),而跳表是通 過隨機函數來維護前面提到的“平衡性”。

過程如下:

  • 通過一個隨機函數,來決定將這個結點插入到哪幾級索引中,比如隨機函數生成了K
  • 查找當前節點要插入的原始節點的位置
  • 基于該位置,從原始鏈表層向上,每層建立一個指向該節點的down指針,直到第K層
    如下節點6 插入該跳表,并且隨機函數生成的K=2,即對6創建索引節點直到第二層

總結

綜上描述,我們了解了跳表的查找,插入,刪除,更新的過程,為什么rocksdb和redis都想要使用跳表作為自己的有序集合的管理結構呢?

像redis和rocksdb 都提供以下核心的數據操作:

  • 插入一個數據
  • 刪除一個數據
  • 查找一個數據
  • 查找一個區間數據[52,100]
  • 不斷輸出一個有序序列

以上插入,查找,刪除,迭代輸出的操作跳表和紅黑樹的效率接近,但是range查找則紅黑樹沒有跳表高
在區間查找的時候,跳表只需要找到區間的第一個元素即可順序遍歷即可(元素是有序的),但是紅黑樹每一個元素都需要相同的復雜度。

但是跳表并沒有紅黑樹的接口通用,很多語言都提供紅黑樹的實現接口,跳表還需要自己實現。

詳細實現

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <string>
#include <cstring>
#include <random>
#include <ctime>
using namespace std;/*** 跳表的一種實現方法。* 跳表中存儲的是正整數,并且存儲的是不重復的。* *  跳表結構:* *  第K級           1           9*  第K-1級         1     5     9*  第K-2級         1  3  5  7  9*  ...             ....*  第0級(原始鏈表)  1  2  3  4  5  6  7  8  9*/const int MAX_LEVEL = 16;/*** @brief 節點
*/
class CNode
{
public:CNode();~CNode();std::string toString();/*** @brief 獲取索引鏈表*/CNode** GetIdxList();/*** @brief 設置數據*/void SetData(int v);/*** @brief 獲取數據*/int GetData();/*** @brief 設置最大索引級別*/void SetLevel(int l);
private:/**當前節點的值*/int m_data;/** * 當前節點的每個等級的下一個節點.* 第2級 N1 N2* 第1級 N1 N2* 如果N1是本節點,則 m_lpForwards[x] 保存的是N2* * [0] 就是原始鏈表.*/CNode* m_lpForwards[MAX_LEVEL];/**當前節點的所在的最大索引級別*/int m_iMaxLevel;
};/*** @brief 跳表
*/
class CSkipList
{
public:CSkipList();~CSkipList();/*** @brief 查找指定的值的節點* @param v 正整數*/CNode* Find(int v);/*** @brief 插入指定的值* @param v 正整數*/void Insert(int v);/*** @brief 刪除指定的值的節點* @param v 正整數*/int Delete(int v);void PrintAll();/*** @brief 打印跳表結構* @param l 等于-1時打印所有級別的結構 >=0時打印指定級別的結構*/void PrintAll(int l);/*** @brief 插入節點時,得到插入K級的隨機函數* @return K*/int RandomLevel();private:int levelCount;/*** 鏈表* 帶頭/哨所(節點)*/CNode* m_lpHead;
};int main()
{CSkipList skipList;/// 插入原始值for(int i=1; i< 50; i++){if((i%3) == 0){skipList.Insert(i);}}for(int i=1; i< 50; i++){if((i%3) == 1){skipList.Insert(i);}}skipList.PrintAll();std::cout<<std::endl;// 打印所有等級結構skipList.PrintAll(-1);// 查找std::cout<<std::endl;CNode* lpNode = skipList.Find(27);if(NULL != lpNode){std::cout<<"查找值為27的節點,找到該節點,節點值:"<<lpNode->GetData()<<std::endl;}else{std::cout<<"查找值為27的節點,未找到該節點"<<std::endl;}/// 刪除std::cout<<std::endl;int ret = skipList.Delete(46);if(0 == ret){std::cout<<"查找值為46的節點,找到該節點,并刪除成功"<<std::endl;}else{std::cout<<"查找值為46的節點,找到該節點,刪除失敗"<<std::endl;}std::cout<<std::endl;//打印所有等級結構skipList.PrintAll(-1);std::cin.ignore();return 0;
}CNode::CNode()
{m_data = -1;m_iMaxLevel = 0;for(int i=0; i<MAX_LEVEL; i++){m_lpForwards[i] = NULL;}
}
CNode::~CNode()
{}
CNode** CNode::GetIdxList()
{return m_lpForwards;
}void CNode::SetData(int v)
{m_data = v;
}
int CNode::GetData()
{return m_data;
}
void CNode::SetLevel(int l)
{m_iMaxLevel = l;
}
std::string CNode::toString()
{char tmp[32];std::string ret;ret.append("{ data: ");sprintf(tmp, "%d", m_data);ret.append(tmp);ret.append("; levels: ");sprintf(tmp, "%d", m_iMaxLevel);ret.append(tmp);ret.append(" }");return ret;
}CSkipList::CSkipList()
{levelCount = 1;m_lpHead = new CNode();
}
CSkipList::~CSkipList()
{}
CNode* CSkipList::Find(int v)
{CNode* lpNode = m_lpHead;/*** 從 最大級索引鏈表開始查找.* K -> k-1 -> k-2 ...->0*/for(int i=levelCount-1; i>=0; --i){/*** 查找小于v的節點(lpNode).*/while((NULL != lpNode->GetIdxList()[i]) && (lpNode->GetIdxList()[i]->GetData() < v)){lpNode = lpNode->GetIdxList()[i];}}/*** lpNode 是小于v的節點, lpNode的下一個節點就等于或大于v的節點*/if((NULL != lpNode->GetIdxList()[0]) && (lpNode->GetIdxList()[0]->GetData() == v)){return lpNode->GetIdxList()[0];}return NULL;
}
void CSkipList::Insert(int v)
{/// 新節點CNode* lpNewNode = new CNode();if(NULL == lpNewNode){return;}/*** 新節點最大分布在的索引鏈表的上限* 如果返回 3,則 新的節點會在索引1、2、3上的鏈表都存在*/int level = RandomLevel();lpNewNode->SetData(v);lpNewNode->SetLevel(level);/*** 臨時索引鏈表* 主要是得到新的節點在每個索引鏈表上的位置*/CNode *lpUpdateNode[level];for(int i=0; i<level; i++){/// 每個索引鏈表的頭節點lpUpdateNode[i] =m_lpHead;}CNode* lpFind = m_lpHead;for(int i= level-1; i >= 0; --i){/*** 查找位置*   eg.  第1級  1  7  10*   如果插入的是 6*   lpFind->GetIdxList()[i]->GetData() : 表示節點lpFind在第1級索引的下一個節點的數據*   當 "lpFind->GetIdxList()[i]->GetData() < v"不成立的時候,*   新節點就要插入到 lpFind節點的后面, lpFind->GetIdxList()[i] 節點的前面*   即在這里 lpFind就是1  lpFind->GetIdxList()[i] 就是7*/while((NULL != lpFind->GetIdxList()[i]) && (lpFind->GetIdxList()[i]->GetData() < v)){lpFind = lpFind->GetIdxList()[i];}/// lpFind 是新節點在 第i級索引鏈表的后一個節點lpUpdateNode[i] = lpFind;}for(int i=0; i<level; ++i){/*** 重新設置鏈表指針位置*   eg  第1級索引 1  7  10*      插入6.*      lpUpdateNode[i] 節點是1; lpUpdateNode[i]->GetIdxList()[i]節點是7*  *  這2句代碼就是 把6放在 1和7之間*/lpNewNode->GetIdxList()[i] = lpUpdateNode[i]->GetIdxList()[i];lpUpdateNode[i]->GetIdxList()[i] = lpNewNode;}if(levelCount < level){levelCount = level;}
}
int CSkipList::Delete(int v)
{int ret = -1;CNode *lpUpdateNode[levelCount];CNode *lpFind = m_lpHead;for(int i=levelCount-1; i>= 0; --i){/*** 查找小于v的節點(lpFind).*/while((NULL != lpFind->GetIdxList()[i]) && (lpFind->GetIdxList()[i]->GetData() < v)){lpFind = lpFind->GetIdxList()[i];}lpUpdateNode[i] = lpFind;}/*** lpFind 是小于v的節點, lpFind的下一個節點就等于或大于v的節點*/if((NULL != lpFind->GetIdxList()[0]) && (lpFind->GetIdxList()[0]->GetData() == v)){for(int i=levelCount-1; i>=0; --i){if((NULL != lpUpdateNode[i]->GetIdxList()[i]) && (v == lpUpdateNode[i]->GetIdxList()[i]->GetData())){lpUpdateNode[i]->GetIdxList()[i] = lpUpdateNode[i]->GetIdxList()[i]->GetIdxList()[i];ret = 0;}}}return ret;
}
void CSkipList::PrintAll()
{CNode* lpNode = m_lpHead;while(NULL != lpNode->GetIdxList()[0]){std::cout<<lpNode->GetIdxList()[0]->toString().data()<<std::endl;lpNode = lpNode->GetIdxList()[0];}
}
void CSkipList::PrintAll(int l)
{for(int i=MAX_LEVEL-1; i>=0;--i){CNode* lpNode = m_lpHead;std::cout<<"第"<<i<<"級:"<<std::endl;if((l < 0) || ((l >= 0) && (l == i))){while(NULL != lpNode->GetIdxList()[i]){std::cout<<lpNode->GetIdxList()[i]->GetData()<<" ";lpNode = lpNode->GetIdxList()[i];}std::cout<<std::endl;if(l >= 0){break;}}}
}
int GetRandom()
{static int _count = 1;std::default_random_engine generator(time(0) + _count);std::uniform_int_distribution<int> distribution(1,99999/*0x7FFFFFFF*/);int dice_roll = distribution(generator);_count += 100;return dice_roll;
}
int CSkipList::RandomLevel()
{int level = 1;for(int i=1; i<MAX_LEVEL; ++i){if(1 == (GetRandom()%3)){level++;}}return level;
}

總結

以上是生活随笔為你收集整理的skiplist跳表的 实现的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。