數據結構——跳表

簡單介紹跳表

? 跳表(Skip List)是一種可以進行對數級別查找的數據結構,它通過在數據中構建多級索引來提高查詢效率。跳表是一種基于鏈表的隨機化數據結構,其本質是由多個鏈表組成,每個鏈表中的元素都是原始鏈表中的元素。

? 我們知道鏈表的查找時間復雜度是O(N),如果這個鏈表數據是有序的,還是O(N),我們如何利用有序這一點,來進行優化呢?接下來就是我們的主角跳表登場。

? 跳表在實際應用中有許多用途,例如作為Redis等數據庫的有序數據結構實現,以及作為平衡樹等數據結構的替代方案。與其他數據結構相比,跳表具有實現簡單、空間復雜度低、查詢效率高等優點。

? skiplist是由William Pugh發明的,最早出現于他在1990年發表的論文《Skip Lists: A
Probabilistic Alternative to Balanced Trees》。
William Pugh開始的優化思路:
1. 假如我們每相鄰兩個節點升高一層,增加一個指針,讓指針指向下下個節點,如下圖b所
示。這樣所有新增加的指針連成了一個新的鏈表,但它包含的節點個數只有原來的一半。由
于新增加的指針,我們不再需要與鏈表中每個節點逐個進行比較了,需要比較的節點數大概
只有原來的一半。
2. 以此類推,我們可以在第二層新產生的鏈表上,繼續為每相鄰的兩個節點升高一層,增加一
個指針,從而產生第三層鏈表。如下圖c,這樣搜索效率就進一步提高了。
3. skiplist正是受這種多層鏈表的想法的啟發而設計出來的。實際上,按照上面生成鏈表的方
式,上面每一層鏈表的節點個數,是下面一層的節點個數的一半,這樣查找過程就非常類似
二分查找,使得查找的時間復雜度可以降低到O(log n)。但是這個結構在插入刪除數據的時
候有很大的問題,插入或者刪除一個節點之后,就會打亂上下相鄰兩層鏈表上節點個數嚴格
的2:1的對應關系。如果要維持這種對應關系,就必須把新插入的節點后面的所有節點(也
包括新插入的節點)重新進行調整,這會讓時間復雜度重新蛻化成O(n)。
理想狀態的跳表大概長這樣:
但是一旦我們進行了插入和刪除操作,就會很難維護這個2:1的關系。因此
4. skiplist的設計為了避免這種問題,做了一個大膽的處理,不再嚴格要求對應比例關系,而是
插入一個節點的時候隨機出一個層數。這樣每次插入和刪除都不需要考慮其他節點的層數,
這樣就好處理多了。細節過程入下圖:

?

注意重點,在這種處理下,我們插入的結點的層數是隨機的!如此一來,插入刪除操作就簡化了很多。

skiplist的效率?

? 首先要分析,這個隨機層數是怎么來的。一般跳表會設置一個最大的層數限制maxLevel。其次會設計一個概率p。這個p就是指 最開始從第一層開始,每多一層的概率為p。

? 最大層數限制很好理解,這個p就是每次插入的時候,由它來決定這個結點有多少層。每多一層其實就是多一個指針。

在Redis中,這兩個參數的取值為

p = 1/4
maxLevel = 32

?

再簡單分析這個p就是:

節點層數至少為1。而大于1的節點層數,滿足一個概率分布。
節點層數恰好等于1的概率為1-p。
節點層數大于等于2的概率為p,而節點層數恰好等于2的概率為p(1-p)。
節點層數大于等于3的概率為p^2,而節點層數恰好等于3的概率為p^2*(1-p)。
節點層數大于等于4的概率為p^3,而節點層數恰好等于4的概率為p^3*(1-p)。
……

平均計算如下:

?

?skiplist的簡單實現

? 其實skiplist只要理解了思想,實現起來還是比較簡單了,我們可以參考力扣上的一道題

1206. 設計跳表 - 力扣(LeetCode)?

?代碼:

#pragma once#include <iostream>
#include <vector>
#include <time.h>
#include <random>
#include <chrono>using namespace std;struct SkiplistNode
{int _val;vector<SkiplistNode*> _nextV;  // 層數也就是指針,用數組存起來SkiplistNode(int val, int level):_val(val), _nextV(level, nullptr){}
};class Skiplist
{typedef SkiplistNode Node;
public:Skiplist(){srand(time(0));// 頭結點的層數設置為1_head = new SkiplistNode(-1, 1);}bool search(int target){Node* cur = _head;int level = _head->_nextV.size() - 1;while (level >= 0){// 目標值比下一個結點的值要大的話,就往右走// 如果下一個結點是空(尾),或者目標值比下一個結點要小,就向下走if (cur->_nextV[level] && cur->_nextV[level]->_val < target){// 往右走cur = cur->_nextV[level];}else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val > target){// 往下走--level;}else{// 找到了return true;}}return false;}vector<Node*> FindPrevNode(int num){Node* cur = _head;int level = _head->_nextV.size() - 1;// 記錄 被改動的位置的每一層的前一個結點指針vector<Node*> prevV(level + 1, _head);while (level >= 0){// 同理 比下一個結點大就往右走// 下一個結點是空,或者目標值比下一個結點要小,就往下走if (cur->_nextV[level] && cur->_nextV[level]->_val < num){cur = cur->_nextV[level]; // 向右走}else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val >= num) {// 注意 這里num等于也是要進來的// 更新level層的前一個prevV[level] = cur;--level; // 向下走}}return prevV;}void add(int num){vector<Node*> prevV = FindPrevNode(num);int n = RandomLevel();Node* newnode = new Node(num, n);// 注意:如果n超過了當前的最大層,那么就要相應的提高_head的層數if (n > _head->_nextV.size()){_head->_nextV.resize(n, nullptr);prevV.resize(n, _head);}// 鏈接前后結點for (size_t i = 0; i < n; ++i){// 注意順序,先設置好目標結點的指針指向newnode->_nextV[i] = prevV[i]->_nextV[i];prevV[i]->_nextV[i] = newnode;}}bool erase(int num){vector<Node*> prevV = FindPrevNode(num);// 如果第一層的下一個不是val,則val不在表中if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != num){return false;}else{Node* del = prevV[0]->_nextV[0];// 先將del結點的前后結點鏈接起來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;elsebreak;}_head->_nextV.resize(i + 1);return true;}return false;}int RandomLevel(){size_t level = 1;// rand() 的取值范圍在 [0,RAND_MAX] 之間// 這里就轉換成了 如果區間在 [0,_p]就加一層while (rand() <= RAND_MAX * _p && level < _maxLevel){++level;}return level;}
private:Node* _head;size_t _maxLevel = 32;double _p = 0.25;
};void Test()
{Skiplist sl;//int a[] = { 5, 2, 3, 8, 9, 6, 5, 2, 3, 8, 9, 6, 5, 2, 3, 8, 9, 6 };int a[] = { 1, 2, 3, 4 };for (auto e : a){sl.add(e);}/*int x;cin >> x;sl.erase(x);*/sl.erase(1);//cout << sl.erase(0) << " " << sl.erase(1) << endl;
}

? 跳表稍復雜一點的地方就是插入和刪除了。因為我們還要需要找到目標結點的每一層前一個結點,將它們放入數組中,然后才能處理好目標結點的前后結點之間的關系。

? 另外就是它的查找邏輯,設cur來遍歷結點,那么cur的移動就有兩種情況,一種是目標值大于cur下一個結點的值的話,cur就向右走;另一種就是小于,那么cur就向下走(--level)。再然后就是等于了。

跳表跟平衡搜索樹及哈希表的對比

跟平衡搜索樹?

? 二者都可以做到遍歷數據有序,并且時間復雜度都差不多。

? 跳表跟平衡搜索樹(AVL樹和RB樹)的優勢就是:

1. 跳表實現簡單,且容易控制。不像AVL樹和RB樹非常復雜,跳表這里我們刪除操作都很容易就實現了。

2.跳表的空間消耗相對較低,不像平衡搜索樹,不僅每個結點都有三叉鏈(指針),而且還要存平衡因子或者顏色。當跳表中的p = 1/2時,每個結點所包含的指針個數為2;p = 1/4時,每個結點所包含的指針個數為1.33。

因此,跟平衡搜索樹比起來,還有是有優勢的。

跟哈希表

跳表跟哈希表比起來,各有優缺點。

跳表的優點:

1.空間消耗還是略低哈希表。哈希表存在鏈表指針和表空間的消耗。

2.跳表遍歷數據能有序。

3.哈希表擴容時有性能損耗。跳表就沒有。

4.在極端場景下,哈希表哈希沖突高,效率下降的厲害,還需要紅黑樹來接力,增加了算法復雜度。

哈希表的優點:

時間復雜度是O(1),比跳表要快。

所以這樣看來,跳表跟哈希表比起來,有些還是有優勢的,但是沒有跟平衡搜索樹比起來那么大。

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

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

相關文章

圖論 - Trie樹(字符串統計、最大異或對)

文章目錄 前言Part 1&#xff1a;Trie字符串統計1.題目描述輸入格式輸出格式數據范圍輸入樣例輸出樣例 2.算法 Part 2&#xff1a;最大異或對1.題目描述輸入格式輸出格式數據范圍輸入樣例輸出樣例 2.算法 前言 本篇博客將介紹Trie樹的常見應用&#xff0c;包括&#xff1a;Trie…

C++ 使用 nlohmann::json存儲json文件

C 使用 nlohmann::json存儲json文件 nlohmann::json 概述JSON 存儲的示例以追加的方式存儲json文件 nlohmann::json 概述 nlohmann::json 是 C 中一個流行的 JSON 庫&#xff0c;由 Niels Lohmann 開發。它提供了一個簡單而強大的 API&#xff0c;用于解析、構建、操作和序列化…

電子電氣架構——車載以太網協議棧

電子電氣架構——車載以太網協議棧 我是穿拖鞋的漢子&#xff0c;魔都中堅持長期主義的汽車電子工程師。 老規矩&#xff0c;分享一段喜歡的文字&#xff0c;避免自己成為高知識低文化的工程師&#xff1a; 沒有人關注你。也無需有人關注你。你必須承認自己的價值&#xff0c…

MySQL入門------數據庫與SQL概述

目錄 前言 一、數據庫相關概念 二、數據模型 1.關系型數據庫&#xff08;RDBMS&#xff09; 三、MySQL數據庫 1.下載和安裝 2.配置環境變量 四、SQL 1.SQL通用語法 2.SQL分類 前言 從本期開始&#xff0c;我們開始學習數據庫的相關理論和實踐知識&#xff0c;從入門…

jupyter 用pyecharts進行數據分析

一、jupyter和pyecharts下載和打開 因為我是用的pycharm&#xff0c;所以我直接在pycharm項目終端中下載pip install jupyter,pip install pyecharts 在你下載的項目路徑中輸入jupyter notebook 之后會進入頁面 Jupyter 具體使用參考這個鏈接&#xff1a;Jupyter Notebook基本…

Pygame教程01:初識pygame游戲模塊

Pygame是一個用于創建基本的2D游戲和圖形應用程序。它提供了一套豐富的工具&#xff0c;讓開發者能夠輕松地創建游戲和其他圖形應用程序。Pygame 支持許多功能&#xff0c;包括圖像和聲音處理、事件處理、碰撞檢測、字體渲染等。 Pygame 是在 SDL&#xff08;Simple DirectMed…

常用設計模式詳解

設計模式 1.UML圖 統一建模語言是用來設計軟件的可視化建模語言。定義了用例圖、類圖、對象圖、狀態圖、活動圖、時序圖、協作圖、構件圖、部署圖等 9 種圖。 1.1類圖 1.1.1類的表示方式 在UML類圖中&#xff0c;類使用包含類名、屬性(field) 和方法(method) 且帶有分割線…

基本正則表達式

基本正則表達式 正則命令功能&#xff3e;尖角號&#xff0c;用于模式的最左側&#xff0c;如“^oldbpy"&#xff0c;匹配以oldboy單詞開頭的行$美元符&#xff0c;用于模式的最右側&#xff0c;如"oldboy$"&#xff0c;表示以oldboy單詞結尾的行^$組合符&…

Java基于springboot的廚藝交流平臺的設計與實現代碼

摘 要 使用舊方法對廚藝交流信息進行系統化管理已經不再讓人們信賴了&#xff0c;把現在的網絡信息技術運用在廚藝交流信息的管理上面可以解決許多信息管理上面的難題&#xff0c;比如處理數據時間很長&#xff0c;數據存在錯誤不能及時糾正等問題。 這次開發的廚藝交流平臺功…

如何優雅的刪除undo表空間

前言 因磁盤空間不足&#xff0c;需要將undo表空間遷移到其它的存儲空間 本文介紹如何優雅的刪除undo表空間&#xff0c;并在新的存儲空間中創建新的undo表空間 詳細操作步驟如下&#xff1a; 1、查看默認undo表空間 SQL>show parameter undo NAME …

Redis的主從搭建

1.準備兩臺機器&#xff0c;安裝好redis 2.修改從服務器的redis配置 slaveof <masterip> <masterport>兩個參數 masterip 主的ip 主的端口號 masterport 3. 啟動redis 1.先啟動主機redis 2.再啟用從機redis 主機redis日志打印 從機redis 日志打印

【python】1.python3.12.2和pycharm社區版的安裝指南

歡迎來CILMY23的博客喔&#xff0c;本篇為【python】1.python3.12.2和pycharm社區版的安裝指南&#xff0c;感謝觀看&#xff0c;支持的可以給個一鍵三連&#xff0c;點贊關注收藏。 目錄 一、python3.12.2的下載與安裝 1.1下載 1.2安裝 二、pycharm的安裝 2.1下載安裝 2…

Bootstrap的使用

目錄 js的引入&#xff1a; 1.行內式 2.嵌入式 3.外鏈式 Bootstrap:的引入 注意事項&#xff1a; 條件注釋語句&#xff1a; 柵格系統&#xff1a; 列嵌套&#xff1a; 列偏移&#xff1a; 列排序&#xff1a; 響應式工具&#xff1a; Bootstrap的字體圖標的使用&a…

2024最新算法:河馬優化算法(Hippopotamus optimization algorithm,HO)求解23個基準函數,提供MATLAB代碼

一、河馬優化算法 河馬優化算法&#xff08;Hippopotamus optimization algorithm&#xff0c;HO&#xff09;由Amiri等人于2024年提出&#xff0c;該算法模擬了河馬在河流或池塘中的位置更新、針對捕食者的防御策略以及規避方法。河馬優化算法的靈感來自河馬生活中觀察到的三…

【金三銀四】Mysgl優化了解?什么情況下會導致SQL索引失效?如何寫出高效SQL與優化慢SQL

Mysgl優化 MySQL 優化是指對 MySQL 數據庫的配置、表設計、查詢語句等進行針對性的優化&#xff0c;以提高數據庫的性能和效率。這包括但不限于合理設計數據庫表結構、編寫高效的 SQL 查詢語句、創建合適的索引以及調整數據庫服務器的參數等。 當MySQL單表記錄數過大時&#xf…

【測試工具】Fiddler

1.Fiddler簡介 Fiddler是位于客戶端和服務器端的HTTP代理&#xff0c;能夠記錄客戶端和服務器之間的所有 HTTP請求&#xff0c;是web調試的利器。既然是代理&#xff0c;也就是說&#xff1a;客戶端的所有請求都要先經過Fiddler&#xff0c;然后轉發到相應的服務器&#xff0c…

【應用多元統計分析】--數據矩陣及R語言表示

在多元分析中&#xff0c;數據通常以矩陣的形式出現&#xff0c;下面結合R語言介紹基本的矩陣運算。主要包括&#xff1a;創建矩陣向量&#xff0c;矩陣加減、乘積&#xff0c;矩陣的逆&#xff0c;行列式的值&#xff0c;特征值與特征向量&#xff0c;QR分解&#xff0c;奇異值…

微前端-乾坤《》

微前端 一個應用&#xff0c;當不斷迭代的時候&#xff0c;功能會越來越多&#xff0c;代碼量隨著也會變得越來越大。進而代碼之間的耦合性會變高&#xff0c;這樣導致開發和維護很糟心&#xff0c;動一發而牽全身。于是有了微前端來解這個問題&#xff0c;按功能可以將這個應…

day02-JavaScript-Vue

文章目錄 1 JavaScript1.1 介紹 1.2 引入方式1.3 基礎語法1.3.1 書寫語法1.3.2 變量1.3.3 數據類型和運算符 1.4 函數1.4.1 第一種定義格式1.4.2 第二種定義格式 1.5 JavaScript對象1.5.1 基本對象1.5.1.1 Array對象語法格式特點屬性和方法 1.5.1.2 String對象語法格式屬性和方…

17.來自Sora的奪舍妄想——享元模式詳解

OpenAI 的 Sora 模型面世之后&#xff0c;可以說人類抵御AI的最后陣地也淪陷了。 在此之前&#xff0c;人們面對AI交互式對話&#xff0c;AI制圖&#xff0c;AI建模之類的奇跡時&#xff0c;還可以略微放肆的說&#xff1a;“的確很神奇&#xff0c;這畢竟還是比人類世界低了一…