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

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

高级数据结构与算法 | 跳跃表(Skip List)

發(fā)布時(shí)間:2024/4/11 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 高级数据结构与算法 | 跳跃表(Skip List) 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

文章目錄

  • 區(qū)間查詢時(shí)鏈表與順序表的局限
  • 跳表=鏈表+索引
  • 跳表的原理
    • 晉升
    • 插入
    • 刪除
  • 跳表的實(shí)現(xiàn)
  • 跳表VS紅黑樹(shù)


區(qū)間查詢時(shí)鏈表與順序表的局限

假設(shè)有這樣一個(gè)情景, 此時(shí)需要設(shè)計(jì)一個(gè)拍賣系統(tǒng),對(duì)于商品的展示需要支持按照價(jià)格、銷量、好評(píng)、拍賣人編號(hào)等方式進(jìn)行排序,并且還需要支持按照名字的精確查詢以及不需要名字的全量查詢。

拍賣行商品的列表是線性的,那么首選的數(shù)據(jù)結(jié)構(gòu)應(yīng)該就是線性結(jié)構(gòu)中的鏈表和順序表。

假設(shè)此時(shí)是一個(gè)按照價(jià)格進(jìn)行排序的集合

如果此時(shí)使用的是一個(gè)順序表,當(dāng)有商品插入時(shí)首先就要確認(rèn)其插入的位置,因?yàn)轫樞虮碇С窒聵?biāo)隨機(jī)訪問(wèn),所以可以通過(guò)二分查找以O(shè)(logN)的效率來(lái)找到數(shù)據(jù)的位置。但是因?yàn)轫樞虮硎强臻g上的順序結(jié)構(gòu),當(dāng)有數(shù)據(jù)插入時(shí)就需要將數(shù)據(jù)往后挪動(dòng),此時(shí)插入的效率就為O(N),對(duì)于拍賣行動(dòng)輒百萬(wàn)的商品,這個(gè)效率顯然不行。

那如果是鏈表呢?因?yàn)槠涫沁壿嬌系木€性結(jié)構(gòu),所以其可以在O(1)的時(shí)間內(nèi)完成插入和刪除,但是又由于其不支持下標(biāo)的隨機(jī)訪問(wèn),所以它沒(méi)有辦法使用二分查找,導(dǎo)致了確認(rèn)位置就需要花費(fèi)O(n)的時(shí)間,這顯然也是不行的。

當(dāng)然也有人會(huì)想到使用哈希或者平衡樹(shù),但是哈希是通過(guò)key值進(jìn)行查找,并不支持區(qū)間查詢,而平衡樹(shù)如果想進(jìn)行區(qū)間查詢,就只能通過(guò)修改結(jié)構(gòu)來(lái)進(jìn)行中序遍歷達(dá)到這個(gè)效果,遠(yuǎn)遠(yuǎn)不及線性結(jié)構(gòu)的效率。


跳表=鏈表+索引

從上面的比較可以看出來(lái),鏈表的局限就在于其不支持下標(biāo)隨機(jī)訪問(wèn),導(dǎo)致了無(wú)法使用二分查找來(lái)確認(rèn)位置,那么還有其他的方法來(lái)解決這個(gè)問(wèn)題嗎?

當(dāng)我們看書的時(shí)候,通常會(huì)先查詢目錄,再根據(jù)目錄來(lái)快速的確認(rèn)我們需要查看的位置,而書上的 頁(yè)碼就充當(dāng)了一個(gè)索引。

所以我們可以效仿這個(gè)思路,為鏈表也增加上這么一層索引

此時(shí)我們可以考慮將鏈表中的中的一半節(jié)點(diǎn)提取出來(lái),充當(dāng)索引。當(dāng)我們需要堆數(shù)據(jù)進(jìn)行查詢的時(shí)候,就可以先去查詢索引鏈表,如果能在索引鏈表中找到,則可以直接通過(guò)關(guān)聯(lián)指針來(lái)找到對(duì)應(yīng)的節(jié)點(diǎn),即使找不到,也可以通過(guò)其他索引的關(guān)聯(lián)節(jié)點(diǎn)來(lái)進(jìn)入原鏈表迅速定位數(shù)據(jù)。

這一整個(gè)過(guò)程就類似我們翻書,即使我們需要的內(nèi)容不在目錄的書頁(yè)中,也能根據(jù)相應(yīng)的章節(jié)來(lái)減少翻書次數(shù)。

由于索引鏈表的結(jié)點(diǎn)個(gè)數(shù)是原始鏈表的一半,查找結(jié)點(diǎn)所需的訪問(wèn)次數(shù)也相應(yīng)減少了一半。

順著這個(gè)思路繼續(xù)往下,為何我們不借鑒B+樹(shù)的思路,再往上構(gòu)建出索引的索引,這樣的話效率又能再進(jìn)一步的提高

此時(shí)查詢數(shù)據(jù)時(shí),就會(huì)先查詢高級(jí)索引,再自頂向下一級(jí)一級(jí)查詢,這樣查詢的效率又會(huì)再一次的進(jìn)行提高。但是提升也是存在極限的,當(dāng)只剩下一個(gè)索引的時(shí)候已經(jīng)失去的索引的意義,所以極限就是最高層只有兩個(gè)索引。
不斷往上提取索引,這樣的一個(gè)多層鏈表的結(jié)構(gòu),就是跳躍表,所以跳躍表又被稱為索引+鏈表。
通過(guò)這樣不斷提升的方式在使得效率提升的同時(shí),也因?yàn)椴粩鄤?chuàng)建新的索引節(jié)點(diǎn)而帶來(lái)了大量的空間消耗,空間復(fù)雜度接近原來(lái)的兩倍,所以這是一種典型的以空間換時(shí)間的數(shù)據(jù)結(jié)構(gòu)


跳表的原理

晉升

當(dāng)有大量的新節(jié)點(diǎn)插入時(shí),原來(lái)的索引節(jié)點(diǎn)就會(huì)漸漸的不夠用,此時(shí)就需要考慮對(duì)新插入的節(jié)點(diǎn)進(jìn)行晉升——即將他作為索引放入上層。

跳表的設(shè)計(jì)人提出了一種晉升的規(guī)則,就是當(dāng)有新節(jié)點(diǎn)到來(lái)時(shí),就拋一次硬幣(概率50%),來(lái)判斷是否需要將其晉升,如果為正面則晉升為索引,反面則作為普通節(jié)點(diǎn)。并且如果結(jié)果為正面,就會(huì)再次拋硬幣來(lái)決定是否需要再次升級(jí),直到拋到反面才結(jié)束晉升。

例如9插入進(jìn)來(lái),此時(shí)拋硬幣為正,將其晉升

第二次拋硬幣為反面,則停止晉升。

之所以采用拋硬幣是因?yàn)椴迦牒蛣h除是不可預(yù)測(cè)的,很難有一種方法來(lái)確保其始終均勻,所以就使用拋硬幣的方法來(lái)保證其大體上處于均勻。


插入

插入的核心晉升已經(jīng)在上面講過(guò)了,接下來(lái)的步驟就簡(jiǎn)單多了

插入的邏輯分為以下三個(gè)步驟

  • 遍歷各級(jí)索引,找到插入節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn) O(logN)
  • 將節(jié)點(diǎn)插入進(jìn)最底層鏈表 O(1)
  • 通過(guò)拋硬幣的方式來(lái)決定是否需要進(jìn)行提升,如果為正則提升,并繼續(xù)拋硬幣,為反面則停止 O如果提升時(shí)已處于最高層,則再創(chuàng)建一層(logN),
  • bool insert(const T& data) {//找到前驅(qū)節(jié)點(diǎn)的位置Node* prev = findPrev(data);if (prev->_data == data){//如果相同,則說(shuō)明已經(jīng)插入,直接返回即可return false;}//將節(jié)點(diǎn)追加到前驅(qū)節(jié)點(diǎn)后面Node* cur = new Node(data);appendNode(prev, cur);//判斷是否需要晉升int curLevel = 0;std::default_random_engine eg; //隨機(jī)數(shù)生成引擎std::uniform_real_distribution<double> random(0, 1); //隨機(jī)數(shù)分布對(duì)象//如果拋到正面則一直晉升while (random(eg) < _promoteRate){//判斷當(dāng)前是否為最高層,如果是最高層則需要增加層數(shù)if (curLevel == _maxLevel){addLever();}//找到上一層的前驅(qū)節(jié)點(diǎn)while (prev->_up == nullptr){prev = prev->_left;}prev = prev->_up;//構(gòu)造cur節(jié)點(diǎn)的上層索引節(jié)點(diǎn),插入到上層的前驅(qū)節(jié)點(diǎn)后Node* upCur = new Node(data);appendNode(prev, upCur);upCur->_down = cur;cur->_up = upCur;cur = upCur; //繼續(xù)往上晉升++curLevel;}return true; }//在前驅(qū)節(jié)點(diǎn)后面插入節(jié)點(diǎn) void appendNode(Node* prev, Node* cur) {cur->_left = prev;cur->_right = prev->_right;prev->_right->_left = cur;prev->_right = cur; }//增加一層 void addLever() {Node* upHead = new Node();Node* upTail = new Node();//修改相互關(guān)系upHead->_right = upTail;upTail->_left = upHead;upHead->_down = _head;_head->_up = upHead;upTail->_down = _tail;_tail->_up = upTail;//因?yàn)椴樵兪亲皂斚蛳碌?#xff0c;所以將新的頭尾節(jié)點(diǎn)作為當(dāng)前的頭尾節(jié)點(diǎn)_head = upHead;_tail = upTail;++_maxLevel; //層數(shù)加一 }

    刪除

    1.遍歷各級(jí)索引,找到需要?jiǎng)h除節(jié)點(diǎn)的位置 O(logN)
    2.自底向上,一級(jí)一級(jí)刪除節(jié)點(diǎn)與其索引,如果當(dāng)前某一層(除了第一層)除了頭尾節(jié)點(diǎn)外只剩下該節(jié)點(diǎn)的索引,則直接刪除該層。 O(logN)

    //刪除元素 bool erase(const T& data) {Node* cur = find(data);if (cur == nullptr){//如果為空則說(shuō)明該節(jié)點(diǎn)不存在,不需要?jiǎng)h除return false;}//自底向上將該節(jié)點(diǎn)及它的索引刪除int curLevel = 0;while (cur != nullptr){cur->_right->_left = cur->_left;cur->_left->_right = cur->_right;//如果當(dāng)前為層只有該節(jié)點(diǎn),則刪除這一層if (curLevel != 0 && cur->_right->_data == INT_MAX && cur->_left->_data == INT_MAX){earseLevel(cur->_left);}else{++curLevel;}//刪除該層的節(jié)點(diǎn)后繼續(xù)往上刪除索引Node* upCur = cur->_up;delete cur;cur = upCur;}return true; }//刪除一層 void earseLevel(const Node* upHead) {Node* upTail = upHead->_right;//如果當(dāng)前為最高層,則可以直接刪除if (upTail->_up == nullptr){upHead->_down->_up = nullptr;upTail->_down->_up = nullptr;//更換新的首尾_head = upHead->_down;_tail = upTail->_down;}else{upHead->_up->_down = upHead->_down;upHead->_down->_up = upHead->_up;upTail->_up->_down = upTail->_down;upTail->_down->_up = upTail->_up;}delete upHead;delete upTail;--_maxLevel; }

    跳表的實(shí)現(xiàn)

    #pragma once#include<cstdlib> #include<ctime> #include<iostream> #include<limits> #include<random>namespace lee {template<class T>struct less{bool operator()(const T& x, const T& y){return x < y;}};template<class T>struct greater{bool operator()(const T& x, const T& y){return x > y;}};//跳表節(jié)點(diǎn)template<class T>struct SkipListNode{SkipListNode(T data = INT_MAX): _data(data), _up(nullptr), _down(nullptr), _left(nullptr), _right(nullptr){}T _data;SkipListNode<T>* _up;SkipListNode<T>* _down;SkipListNode<T>* _left;SkipListNode<T>* _right;};template<class T ,class Compare = less<T>>class SkipList{typedef SkipListNode<T> Node;private: Node* _head; //頭節(jié)點(diǎn)Node* _tail; //尾節(jié)點(diǎn)double _promoteRate; //晉升概率int _maxLevel; //最高層數(shù)public:SkipList(): _head(new Node), _tail(new Node), _promoteRate(0.5), _maxLevel(0){_head->_right = _tail;_tail->_left = _head;}~SkipList(){clear();delete _head;delete _tail;}//懶得寫拷貝構(gòu)造,就直接防拷貝了/*SkipList(const SkipList&) = delete;SkipList& operator=(const SkipList&) = delete;*///插入元素bool insert(const T& data){//找到前驅(qū)節(jié)點(diǎn)的位置Node* prev = findPrev(data);if (prev->_data == data){//如果相同,則說(shuō)明已經(jīng)插入,直接返回即可return false;}//將節(jié)點(diǎn)追加到前驅(qū)節(jié)點(diǎn)后面Node* cur = new Node(data);appendNode(prev, cur);//判斷是否需要晉升int curLevel = 0;std::default_random_engine eg; //隨機(jī)數(shù)生成引擎std::uniform_real_distribution<double> random(0, 1); //隨機(jī)數(shù)分布對(duì)象//如果拋到正面則一直晉升while (random(eg) < _promoteRate){//判斷當(dāng)前是否為最高層,如果是最高層則需要增加層數(shù)if (curLevel == _maxLevel){addLever();}//找到上一層的前驅(qū)節(jié)點(diǎn)while (prev->_up == nullptr){prev = prev->_left;}prev = prev->_up;//構(gòu)造cur節(jié)點(diǎn)的上層索引節(jié)點(diǎn),插入到上層的前驅(qū)節(jié)點(diǎn)后Node* upCur = new Node(data);appendNode(prev, upCur);upCur->_down = cur;cur->_up = upCur;cur = upCur; //繼續(xù)往上晉升++curLevel;}return true;}//刪除元素bool erase(const T& data){Node* cur = find(data);if (cur == nullptr){//如果為空則說(shuō)明該節(jié)點(diǎn)不存在,不需要?jiǎng)h除return false;}//自底向上將該節(jié)點(diǎn)及它的索引刪除int curLevel = 0;while (cur != nullptr){cur->_right->_left = cur->_left;cur->_left->_right = cur->_right;//如果當(dāng)前為層只有該節(jié)點(diǎn),則刪除這一層if (curLevel != 0 && cur->_right->_data == INT_MAX && cur->_left->_data == INT_MAX){earseLevel(cur->_left);}else{++curLevel;}//刪除該層的節(jié)點(diǎn)后繼續(xù)往上刪除索引Node* upCur = cur->_up;delete cur;cur = upCur;}return true;}//刪除全部節(jié)點(diǎn)void clear(){//從最底層開(kāi)始遍歷,一個(gè)一個(gè)順著往上刪除Node* cur = _head;while (cur->_down != nullptr){cur = cur->_down;}if (cur->_right->_data == INT_MAX){return;}//刪除所有節(jié)點(diǎn)cur = cur->_right;while (cur->_data != INT_MAX){Node* next = cur->_right;erase(cur->_data);cur = next;}}//查找元素Node* find(const T& data){Node* ret = findPrev(data);//如果找到了則返回節(jié)點(diǎn),沒(méi)找到則返回空指針if (ret->_data == data){return ret;}return nullptr;}void printAll(){Node* cur = _head;while (cur->_down != nullptr){cur = cur->_down;}cur = cur->_right;while (cur->_data != INT_MAX){std::cout << cur->_data << std::ends;cur = cur->_right;}}private://查找前驅(qū)節(jié)點(diǎn)Node* findPrev(const T& data){Node* cur = _head;while (1){//找到該層最接近目標(biāo)的索引while (cur->_right->_data != INT_MAX && Compare()(cur->_right->_data, data)){cur = cur->_right;}//如果當(dāng)前已經(jīng)到了最底層,則說(shuō)明當(dāng)前位置就是前驅(qū)節(jié)點(diǎn),否則繼續(xù)往下if (cur->_down == nullptr){break;}else{cur = cur->_down;}}return cur;}//在前驅(qū)節(jié)點(diǎn)后面插入節(jié)點(diǎn)void appendNode(Node* prev, Node* cur){cur->_left = prev;cur->_right = prev->_right;prev->_right->_left = cur;prev->_right = cur;}//增加一層void addLever(){Node* upHead = new Node();Node* upTail = new Node();//修改相互關(guān)系upHead->_right = upTail;upTail->_left = upHead;upHead->_down = _head;_head->_up = upHead;upTail->_down = _tail;_tail->_up = upTail;//因?yàn)椴樵兪亲皂斚蛳碌?#xff0c;所以將新的頭尾節(jié)點(diǎn)作為當(dāng)前的頭尾節(jié)點(diǎn)_head = upHead;_tail = upTail;++_maxLevel; //層數(shù)加一}//刪除一層void earseLevel(const Node* upHead){Node* upTail = upHead->_right;//如果當(dāng)前為最高層,則可以直接刪除if (upTail->_up == nullptr){upHead->_down->_up = nullptr;upTail->_down->_up = nullptr;//更換新的首尾_head = upHead->_down;_tail = upTail->_down;}else{upHead->_up->_down = upHead->_down;upHead->_down->_up = upHead->_up;upTail->_up->_down = upTail->_down;upTail->_down->_up = upTail->_up;}delete upHead;delete upTail;--_maxLevel;}}; };

    簡(jiǎn)單測(cè)試一下

    #include"SkipList.hpp"using namespace std;int main() {lee::SkipList<int> sl;sl.insert(1);sl.insert(3);sl.insert(5);sl.insert(7);sl.insert(9);sl.insert(11);sl.insert(13);sl.insert(15);sl.insert(17);sl.printAll();return 0; }


    跳表VS紅黑樹(shù)

    從上面的描述可以看出來(lái),跳表的功能和性能都與紅黑樹(shù)類似(不了解紅黑樹(shù)的可以看我往期博客數(shù)據(jù)結(jié)構(gòu):紅黑樹(shù)的原理以及實(shí)現(xiàn)(C++))

    在Redis中,并沒(méi)有選擇使用紅黑樹(shù)和B+樹(shù)來(lái)所謂實(shí)現(xiàn)有序集合,而是使用了跳表,原因如下

    • 跳表的插入、刪除、修改等功能與紅黑樹(shù)性能大體一樣,但是在區(qū)間查找這一方面紅黑樹(shù)并不如跳表(平衡樹(shù)都需要通過(guò)中序遍歷來(lái)確認(rèn)區(qū)間,跳表只需要確認(rèn)起點(diǎn)后順序遍歷),而區(qū)間查找在數(shù)據(jù)庫(kù)中又經(jīng)常使用。
    • 跳表實(shí)現(xiàn)起來(lái)相對(duì)簡(jiǎn)單,不容易出錯(cuò)。
    • 紅黑樹(shù)在插入刪除的時(shí)候都會(huì)涉及到平衡的問(wèn)題,導(dǎo)致其需要進(jìn)行旋轉(zhuǎn)、變色等操作來(lái)維持平衡,而跳表只需要進(jìn)行簡(jiǎn)單的鏈表插入

    但是跳表也有一個(gè)最大的不足

    • 因?yàn)椴粩嗤蠘?gòu)建索引導(dǎo)致空間占用大,典型的以空間換時(shí)間(但是Redis官方設(shè)計(jì)手冊(cè)中提到了可以通過(guò)調(diào)參來(lái)降低內(nèi)存消耗,使其能夠接近平衡樹(shù)的空間復(fù)雜度。)

    總結(jié)

    以上是生活随笔為你收集整理的高级数据结构与算法 | 跳跃表(Skip List)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。