DS高階:跳表

一、skiplist

1.1?skiplist的概念

? ? ? ? skiplist本質上也是一種查找結構,用于解決算法中的查找問題,跟平衡搜索樹和哈希表的價值是一樣的,可以作為key或者key/value的查找模型。skiplist是由William Pugh發明的,最早出現于他在1990年發表的論文《Skip Lists: A Probabilistic Alternative to Balanced Trees》?

? ? ? ? skiplist,顧名思義,首先它是一個list。實際上,它是在有序鏈表的基礎上發展起來的。如果是一個有序的鏈表,查找數據的時間復雜度是O(N)。


1.2?skiplist的優化思路分析

? ? ? ? ? ?假如我們每相鄰兩個節點升高一層,增加一個指針,讓指針指向下下個節點,如下圖b所
示。這樣所有新增加的指針連成了一個新的鏈表,但它包含的節點個數只有原來的一半。由于新增加的指針,我們不再需要與鏈表中每個節點逐個進行比較了,需要比較的節點數大概只有原來的一半。(多層鏈表的啟發思路)以此類推,我們可以在第二層新產生的鏈表上,繼續為每相鄰的兩個節點升高一層,增加一個指針,從而產生第三層鏈表。如下圖c,這樣搜索效率就進一步提高了。

? ? ? ?skiplist正是受這種多層鏈表的想法的啟發而設計出來的。實際上,按照上面生成鏈表的方式,上面每一層鏈表的節點個數,是下面一層的節點個數的一半,這樣查找過程就非常類似二分查找,使得查找的時間復雜度可以降低到O(log n)。但是這個結構在插入刪除數據的時候有很大的問題,插入或者刪除一個節點之后,就會打亂上下相鄰兩層鏈表上節點個數嚴格的2:1的對應關系。如果要維持這種對應關系,就必須把新插入的節點后面的所有節點(也包括新插入的節點)重新進行調整,這會讓時間復雜度重新蛻化成O(n)。

? ? ? ?skiplist的設計為了避免這種問題,做了一個大膽的處理,不再嚴格要求對應比例關系,而是
插入一個節點的時候隨機出一個層數。這樣每次插入和刪除都不需要考慮其他節點的層數
,這樣就好處理多了。

?1.3 隨機出層數的含義

? ? ? ? ?插入節點時隨機出一個層數究竟是什么意思呢???難道直接random任意數就可以了嗎??

答:雖然是隨機,但是也有規則的限制。這里首先要細節分析的是這個隨機層數是怎么來的。一般跳表會設計一個最大層數maxLevel的限制,其次會設置一個多增加一層的概率p。那么計算這個隨機層數的偽代碼如下圖:

?一個節點的平均層數(也即包含的平均指針數目),計算如下:

現在很容易計算出:
當p=1/2時,每個節點所包含的平均指針數目為2;
當p=1/4時,每個節點所包含的平均指針數目為1.33。

時間復雜度:logN

具體的分析可以看下面的文章:Redis內部數據結構詳解(6)——skiplist
?

2、skiplist的模擬實現

力扣有一道設計跳表的題. - 力扣(LeetCode)設計跳表

基本的調表需要實現4個函數:構造函數、搜索、插入、刪除。下面我們來一個個分析。

2.1?skiplist的基本結構

struct SkiplistNode
{int _val;//存儲對應的值vector<SkiplistNode*> _nextV;//存放對應的next指針集合SkiplistNode(const int&val, size_t level = 1) //level表示需要開辟的層數 不傳就是默認開滿:_val(val){_nextV.resize(level, nullptr);}
};class Skiplist
{typedef  SkiplistNode Node;
public:private:Node* _head;//虛擬頭節點const size_t _maxLevel = 32; //用缺省參數去初始化const double _p = 0.25;//用缺省參數去初始化
};

?2.2?skiplist的默認構造

Skiplist()
{srand((unsigned int)time(nullptr));//為了方便后面的隨機取層數,先弄一個隨機種子_head = new Node(-1);//默認開一層,用默認構造初始化
}

? ? ? ? 給虛擬頭節點申請一塊空間,一開始默認就開一層。為了能夠方面后面利用rand函數隨機取層數,所以在這個地方先用了一個時間種子

? ? ? ? ?我們默認開的是一層,因為在數據量小的時候其實我們可以根據插入的情況去調整_head的層數,如果是數據量特別大的話,也可以一次性就把他開到滿

2.3?skiplist的搜索

bool search(int target) 
{//要不斷往下走Node* cur = _head;int level = _head->_nextV.size() - 1;//從后往前去找while (level >= 0){//如果我比你大 我就跳過去->更新cur   //如果我比你小或者你為空 我就往下走 --levelif (cur->_nextV[level] == nullptr || target < cur->_nextV[level]->_val) --level;else if (target > cur->_nextV[level]->_val)  cur = cur->_nextV[level];else return true;}return false; //循環結束都沒有找到,說明找不到。
}

我們要從高層一直找到底層,所以要從_nextV的后面開始找。

1、如果你為空,或者我比你小,那就得往下走 ->--level

2、如果我比你大,就可以直接跳到你的位置->更新cur=cur->_nextV[level]

3、如果找到了就返回true,如果循環結束了都找不到,那就返回false

2.4 找到prevV指針數組

為什么要單獨去封裝這個函數呢?

? ? ? ? ?因為不管是插入,還是刪除,我們都需要去找前驅節點的集合,這樣才能去改變連接關系,所以為了提高代碼的復用性,封裝這樣的一個函數,去找到待插入位置或者是待刪除位置的前驅節點集合。

vector<Node*> FindPrevNode(int num) //幫助我們找到前驅指針集合
{//最終我們要返回待插入位置或者是待刪除位置的前驅指針集合  一開始的時候默認是head、Node* cur = _head;int level = _head->_nextV.size() - 1;vector<Node*> prevV(level+1, _head);while (level >= 0){if (cur->_nextV[level] == nullptr || num < cur->_nextV[level]->_val){//更新level的層的前一個節點 往下跳之前保存前驅節點prevV[level] = cur;--level;}else//(num >= cur->_nextV[level]->_val)  cur = cur->_nextV[level];}return prevV;
}

? ? ? ? 當我們需要往后面跳之前,保存當前的cur進去prevV數組中,這樣我們返回的數組就是待插入節點對應的前驅節點集合了!

2.5 隨機層數的生成函數

? ? ? 我們在插入節點之前,要隨機生成一個層數,所以要先實現一個生成層數的函數

2.5.1 C語言rand( )版本

size_t RandomLevel() //C語言版本
{size_t level = 1;//初始的層數while (rand() <= RAND_MAX * _p && level < _maxLevel)  ++level; //RAND_MAX是隨機數的最大值return level;
}

2.5.2 C++11隨機數庫

	size_t RandomLevel() //需要的時候去搜 C++11的隨機數庫即可  頭文件chrono和random{//類似隨機數種子,但是只用一次是最好的 所以設置成staic 這樣就只會調用一次了static std::default_random_engine generator(std::chrono::system_clock::now().time_since_epoch().count());//now.time_since_epoch().count()是一個時間戳 類似隨機數種子static std::uniform_real_distribution<double> distribution(0.0, 1.0);size_t level = 1;while (distribution(generator) <= _p && level < _maxLevel)  ++level;return level;}

? ? ? ? ?std::chrono::system_clock::now().time_since_epoch().count() 類似一個時間戳,相當于是隨機種子,但是由于只需要初始化一次,所以我們將他變成static變量,這樣就只要初始化一次即可!

? ? ? ? 關于C++11的random庫用法,還是比較復雜的,大家可以參考一些相關的文章。

2.6?skiplist的增加

void add(int num)  //插入節點
{vector<Node*> prevV = FindPrevNode(num); //右值引用size_t n = RandomLevel(); //表示需要開多少層//如果n超過了_head的最大層數,那么就要調整一下if (n > _head->_nextV.size()){_head->_nextV.resize(n, nullptr); prevV.resize(n, _head);//不夠的地方也要更新過去}Node* newnode = new Node(num, n);//申請對應的新節點  然后根據prevV數組去建立連接for (size_t i = 0; i < n; ++i) //連接前后節點,首先要先連后面的 再連前面的{newnode->_nextV[i] = prevV[i]->_nextV[i];prevV[i]->_nextV[i] = newnode;}
}

? ? ? ? ?一個很關鍵的地方就是,我們隨機生成了一個層數后,有可能我們的_head的層數都沒這個多,所以我們必須利用resize去初始化一下,否則會出現越界訪問。?

? ? ? ? ?中間插入的邏輯就類似鏈表的指定位置插入,先讓自己的后繼指向前驅的后繼,然后再讓前驅指向自己,必須按照這個順序,否則會丟失節點

2.7?skiplist的刪除

bool erase(int num) 
{//首先 有可能沒有這個數 所以要看看是不是真的沒有vector<Node*> prevV = FindPrevNode(num);if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num)  return false;//有的話,就要去刪除然后重新連接Node* del = prevV[0]->_nextV[0];//我們需要刪除的節點,但是在刪除前要調整一下連接的關系for (size_t i = 0; i < del->_nextV.size(); ++i)  prevV[i]->_nextV[i] = del->_nextV[i];delete del;// 如果刪除最高層節點,把頭節點的層數也降一下int i = _head->_nextV.size() - 1;while (i >= 0){if (_head->_nextV[i] == nullptr)  --i;else  break;}_head->_nextV.resize(i + 1);return true;
}

? ? ?有可能我們找不到這個數,這個時候就沒什么可以刪的了。

? ? ? 在刪除這個節點之前,我們要先記錄這個節點,然后去改變被刪除節點的連接關系,類似鏈表的指定位置刪除。

? ? ? 如果我們刪除的恰好是最高層的節點,這個時候可以整體對頭結點的層數降個高度,這樣就提高了查找效率。

三、skiplist跟平衡搜索樹和哈希表的對比

1. skiplist相比平衡搜索樹(AVL樹和紅黑樹)對比,都可以做到遍歷數據有序,時間復雜度也差不多。但是skiplist在平衡樹面前優勢明顯。

skiplist的優勢是:

a、skiplist實現簡單,容易控制。平衡樹增刪查改遍歷都更復雜。

b、skiplist的額外空間消耗更低。平衡樹節點存儲每個值有三叉鏈,平衡因子/顏色等消耗skiplist中p=1/2時,每個節點所包含的平均指針數目為2;skiplist中p=1/4時,每個節點所包含的平均指針數目為1.33;


2. skiplist相比哈希表而言,就沒有那么大的優勢了:

哈希表的優勢如下:

a、哈希表平均時間復雜度是O(1),比skiplist快。

b、哈希表空間消耗略多一點。

skiplist優勢如下:

a、遍歷數據有序

b、skiplist空間消耗略小一點,哈希表存在鏈接指針和表空間消耗。

c、哈希表擴容有性能損耗。

d、哈希表再極端場景下哈希沖突高,效率下降厲害,需要紅黑樹補足接力。
?

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/13091.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/13091.shtml
英文地址,請注明出處:http://en.pswp.cn/web/13091.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Python學習之路 | Python基礎語法(一)

數據類型 Python3 中常見的數據類型有&#xff1a; Number&#xff08;數字&#xff09;String&#xff08;字符串&#xff09;bool&#xff08;布爾類型&#xff09;List&#xff08;列表&#xff09;Tuple&#xff08;元組&#xff09;Set&#xff08;集合&#xff09;Dict…

鴻蒙HDC命令行工具:模擬操作

模擬操作 uinput用于輸入模擬操作&#xff0c;其命令幫助手冊為&#xff1a; > hdc shell uinput --help Usage: uinput <option> <command> <arg>... The option are: -M --mouse //模擬鼠標操作 commands for mouse: -m <dx> <d…

【Image captioning】基于檢測模型網格特征提取——以Sydeny為例

【Image captioning】基于檢測模型網格特征提取——以Sydeny為例 今天,我們將重點探討如何利用Faster R-CNN檢測模型來提取Sydeny數據集的網格特征。具體而言,這一過程涉及通過Faster R-CNN模型對圖像進行分析,進而抽取出關鍵區域的特征信息,這些特征在網格結構中被系統地…

1金融風控相關業務介紹

金融風控相關業務介紹 學習目標 知道常見信貸風險知道機器學習風控模型的優勢知道信貸領域常用術語含義1 信貸&風控介紹 信貸業務,就是貸款業務,是商業銀行和互聯網金融公司最重要的資產業務和主要贏利手段 通過放款收回本金和利息,扣除成本后獲得利潤。貸款平臺預測有…

java中什么是方法的返回值?方法有哪幾種類型?

在Java中&#xff0c;方法的返回值是指方法執行后返回給調用者的結果。返回值可以是任何數據類型&#xff0c;包括基本數據類型&#xff08;如int、float&#xff09;和引用數據類型&#xff08;如String、對象&#xff09;。返回值的主要作用是將方法執行的結果傳遞給調用該方…

springboot集成dubbo實現微服務系統

目錄 1.說明 2.示例 3.總結 1.說明 dubbo官網&#xff1a;https://cn.dubbo.apache.org/zh-cn/ Apache Dubbo 是一款 RPC 服務開發框架&#xff0c;用于解決微服務架構下的服務治理與通信問題&#xff0c;支持多種語言&#xff0c;官方提供了 Java、Golang 等多語言 SDK 實…

什么是Vue.js? Vue.js簡介

什么是Vue.js? Vue.js簡介 Vue.js是一種用于構建用戶界面的前端框架。它是目前非常流行的JavaScript框架之一&#xff0c;被廣泛應用于單頁應用和響應式網頁開發。 Vue.js具有以下特點和優勢&#xff1a; 輕量級&#xff1a; Vue.js的文件體積很小&#xff0c;加載速度快&…

代碼隨想錄--鏈表--反轉鏈表

題目 題意&#xff1a;反轉一個單鏈表。 示例: 輸入: 1->2->3->4->5->NULL 輸出: 5->4->3->2->1->NULL 思路 如果再定義一個新的鏈表&#xff0c;實現鏈表元素的反轉&#xff0c;其實這是對內存空間的浪費。 其實只需要改變鏈表的next指針的…

GPU學習記一下線程分組相關

在compute的時候&#xff0c;是要dispatch一個數量的代表分了多少塊任務集&#xff0c;dispatch的塊內部也是有一個數量的&#xff0c;那么這些值怎么取的呢 內部&#xff0c;N卡32 外面dispatch的數量就是all/32 然后細說這個值 這有一個叫core的東西&#xff0c;就是相當于th…

嵌入式學習-PWM輸出比較

簡介 PWM技術 輸出比較框圖介紹 定時器部分 比較器控制部分 輸出控制部分 相關寄存器

(5.4–5.10)投融資周報|共38筆公開投融資事件,基礎設施領跑,游戲融資活躍

5月4日至5月10日期間&#xff0c;加密市場共發生38筆投融資事件&#xff0c;其中基礎設施18筆、游戲5 筆、其他4 筆、DeFi 3筆、Depin 3 筆、CeFi 2筆、NFT2筆、 RWA1筆。 本周千萬美金以上融資有5筆&#xff1a; 加密貨幣交易公司Arbelos完成了一輪2800 萬美元的種子輪融資&…

智慧園區EasyCVR視頻智能管理方案:構建高效安全園區新視界

一、背景分析 園區作為城市的基本單元&#xff0c;是最重要的人口和產業聚集區。根據行業市場調研&#xff0c;90%以上城市居民工作與生活在園區進行&#xff0c;80%以上的GDP和90%以上的創新在園區內產生&#xff0c;可以說“城市&#xff0c;除了馬路都是園區”。 園區形態…

C++ static_cast學習

static_cast可實現&#xff0c; 1 基本類型之間的轉換 2 void指針轉換為任意基本類型的指針 3 用于有繼承關系的子類與父類之間的指針或引用的轉換 用于基本類型轉化時&#xff0c;會損失精度類似于C語言的強制轉化&#xff1b; 下面先看一下void指針的轉換&#xff1b; …

手動實現Promise

// 定義異步調用的主類&#xff0c;名為 MyPromise class MyPromise {// 執行器接收 resolve 和 reject 方法來改變 promise 的狀態constructor(executor) {// 初始化狀態為 "pending"this.state "pending";// 初始化值為 undefinedthis.value undefined…

鏡像抑制和鏡像衰減有什么不同

在很多無線產品接收機手冊中&#xff0c;我們會看到兩個參數&#xff0c;一個是鏡像抑制&#xff08;Image Rejection&#xff09;&#xff0c;另一個是鏡像衰減&#xff08;Image Attention&#xff09;&#xff0c;但這兩者究竟有什么不同&#xff0c;一直比較疑惑&#xff0…

AI學習指南線性代數篇-奇異值分解

AI學習指南線性代數篇-奇異值分解 一、概述 在人工智能領域&#xff0c;線性代數是一項非常重要的基礎知識&#xff0c;而奇異值分解&#xff08;Singular Value Decomposition, SVD&#xff09;作為線性代數中的一種重要工具&#xff0c;被廣泛應用于機器學習、數據科學等領…

理解Spring的IOC核心:為何它成為開發中的關鍵要素?

Spring框架采用的IOC&#xff08;依賴注入&#xff09;技術&#xff0c;是一種創新的設計思路&#xff0c;它授權程序開發人員將組件實例化及生命周期管理的職責轉交給框架自身處理。在這一機制下&#xff0c;Spring框架負責協調并裝配應用程序中的各個組件&#xff0c;從而實現…

以太坊Layer 2開發商StarkWare

文章目錄 以太坊Layer 2開發商StarkWare相關新聞StarkWare是什么團隊介紹StarkEx 和 StarkNet參考以太坊Layer 2開發商StarkWare 相關新聞 據The Block 2021年11月16日消息,使用ZK-rollups技術的以太坊第2層開發商StarkWare在C輪融資中籌集了5000萬美元,其估值已達20億美元…

三路輸出小功率開關電源【MATLAB/simulink】

擬選用一種DC-DC變換器拓撲使用1700 V SiC MOSFET或IGBT設計三相功率系 統的高頻開關直流輔助電源&#xff0c;它可用于太陽能逆變器、工業開關電源、電動汽車充電器、 電機驅動裝置等領域。&#xff08;建議采用單端反激式電路拓撲&#xff0c;開關頻率為80kHz) 電路基本參數&…

【Unity學習筆記】第十七 Quaternion 中 LookRotation、Lerp、Slerp、RotateTowards等方法辨析與驗證

轉載請注明出處: https://blog.csdn.net/weixin_44013533/article/details/138909256 作者&#xff1a;CSDN|Ringleader| 目錄 Quaternion API 速覽FromToRotation在Transform中的應用LookRotation 中upwards取Vector3.up和 transform.up的區別旋轉時如何保持Y軸不變&#xff…