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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

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

文章目錄

  • 區間查詢時鏈表與順序表的局限
  • 跳表=鏈表+索引
  • 跳表的原理
    • 晉升
    • 插入
    • 刪除
  • 跳表的實現
  • 跳表VS紅黑樹


區間查詢時鏈表與順序表的局限

假設有這樣一個情景, 此時需要設計一個拍賣系統,對于商品的展示需要支持按照價格、銷量、好評、拍賣人編號等方式進行排序,并且還需要支持按照名字的精確查詢以及不需要名字的全量查詢。

拍賣行商品的列表是線性的,那么首選的數據結構應該就是線性結構中的鏈表和順序表。

假設此時是一個按照價格進行排序的集合

如果此時使用的是一個順序表,當有商品插入時首先就要確認其插入的位置,因為順序表支持下標隨機訪問,所以可以通過二分查找以O(logN)的效率來找到數據的位置。但是因為順序表是空間上的順序結構,當有數據插入時就需要將數據往后挪動,此時插入的效率就為O(N),對于拍賣行動輒百萬的商品,這個效率顯然不行。

那如果是鏈表呢?因為其是邏輯上的線性結構,所以其可以在O(1)的時間內完成插入和刪除,但是又由于其不支持下標的隨機訪問,所以它沒有辦法使用二分查找,導致了確認位置就需要花費O(n)的時間,這顯然也是不行的。

當然也有人會想到使用哈希或者平衡樹,但是哈希是通過key值進行查找,并不支持區間查詢,而平衡樹如果想進行區間查詢,就只能通過修改結構來進行中序遍歷達到這個效果,遠遠不及線性結構的效率。


跳表=鏈表+索引

從上面的比較可以看出來,鏈表的局限就在于其不支持下標隨機訪問,導致了無法使用二分查找來確認位置,那么還有其他的方法來解決這個問題嗎?

當我們看書的時候,通常會先查詢目錄,再根據目錄來快速的確認我們需要查看的位置,而書上的 頁碼就充當了一個索引。

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

此時我們可以考慮將鏈表中的中的一半節點提取出來,充當索引。當我們需要堆數據進行查詢的時候,就可以先去查詢索引鏈表,如果能在索引鏈表中找到,則可以直接通過關聯指針來找到對應的節點,即使找不到,也可以通過其他索引的關聯節點來進入原鏈表迅速定位數據。

這一整個過程就類似我們翻書,即使我們需要的內容不在目錄的書頁中,也能根據相應的章節來減少翻書次數。

由于索引鏈表的結點個數是原始鏈表的一半,查找結點所需的訪問次數也相應減少了一半。

順著這個思路繼續往下,為何我們不借鑒B+樹的思路,再往上構建出索引的索引,這樣的話效率又能再進一步的提高

此時查詢數據時,就會先查詢高級索引,再自頂向下一級一級查詢,這樣查詢的效率又會再一次的進行提高。但是提升也是存在極限的,當只剩下一個索引的時候已經失去的索引的意義,所以極限就是最高層只有兩個索引。
不斷往上提取索引,這樣的一個多層鏈表的結構,就是跳躍表,所以跳躍表又被稱為索引+鏈表。
通過這樣不斷提升的方式在使得效率提升的同時,也因為不斷創建新的索引節點而帶來了大量的空間消耗,空間復雜度接近原來的兩倍,所以這是一種典型的以空間換時間的數據結構


跳表的原理

晉升

當有大量的新節點插入時,原來的索引節點就會漸漸的不夠用,此時就需要考慮對新插入的節點進行晉升——即將他作為索引放入上層。

跳表的設計人提出了一種晉升的規則,就是當有新節點到來時,就拋一次硬幣(概率50%),來判斷是否需要將其晉升,如果為正面則晉升為索引,反面則作為普通節點。并且如果結果為正面,就會再次拋硬幣來決定是否需要再次升級,直到拋到反面才結束晉升。

例如9插入進來,此時拋硬幣為正,將其晉升

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

之所以采用拋硬幣是因為插入和刪除是不可預測的,很難有一種方法來確保其始終均勻,所以就使用拋硬幣的方法來保證其大體上處于均勻。


插入

插入的核心晉升已經在上面講過了,接下來的步驟就簡單多了

插入的邏輯分為以下三個步驟

  • 遍歷各級索引,找到插入節點的前驅節點 O(logN)
  • 將節點插入進最底層鏈表 O(1)
  • 通過拋硬幣的方式來決定是否需要進行提升,如果為正則提升,并繼續拋硬幣,為反面則停止 O如果提升時已處于最高層,則再創建一層(logN),
  • bool insert(const T& data) {//找到前驅節點的位置Node* prev = findPrev(data);if (prev->_data == data){//如果相同,則說明已經插入,直接返回即可return false;}//將節點追加到前驅節點后面Node* cur = new Node(data);appendNode(prev, cur);//判斷是否需要晉升int curLevel = 0;std::default_random_engine eg; //隨機數生成引擎std::uniform_real_distribution<double> random(0, 1); //隨機數分布對象//如果拋到正面則一直晉升while (random(eg) < _promoteRate){//判斷當前是否為最高層,如果是最高層則需要增加層數if (curLevel == _maxLevel){addLever();}//找到上一層的前驅節點while (prev->_up == nullptr){prev = prev->_left;}prev = prev->_up;//構造cur節點的上層索引節點,插入到上層的前驅節點后Node* upCur = new Node(data);appendNode(prev, upCur);upCur->_down = cur;cur->_up = upCur;cur = upCur; //繼續往上晉升++curLevel;}return true; }//在前驅節點后面插入節點 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();//修改相互關系upHead->_right = upTail;upTail->_left = upHead;upHead->_down = _head;_head->_up = upHead;upTail->_down = _tail;_tail->_up = upTail;//因為查詢是自頂向下的,所以將新的頭尾節點作為當前的頭尾節點_head = upHead;_tail = upTail;++_maxLevel; //層數加一 }

    刪除

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

    //刪除元素 bool erase(const T& data) {Node* cur = find(data);if (cur == nullptr){//如果為空則說明該節點不存在,不需要刪除return false;}//自底向上將該節點及它的索引刪除int curLevel = 0;while (cur != nullptr){cur->_right->_left = cur->_left;cur->_left->_right = cur->_right;//如果當前為層只有該節點,則刪除這一層if (curLevel != 0 && cur->_right->_data == INT_MAX && cur->_left->_data == INT_MAX){earseLevel(cur->_left);}else{++curLevel;}//刪除該層的節點后繼續往上刪除索引Node* upCur = cur->_up;delete cur;cur = upCur;}return true; }//刪除一層 void earseLevel(const Node* upHead) {Node* upTail = upHead->_right;//如果當前為最高層,則可以直接刪除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; }

    跳表的實現

    #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;}};//跳表節點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; //頭節點Node* _tail; //尾節點double _promoteRate; //晉升概率int _maxLevel; //最高層數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;}//懶得寫拷貝構造,就直接防拷貝了/*SkipList(const SkipList&) = delete;SkipList& operator=(const SkipList&) = delete;*///插入元素bool insert(const T& data){//找到前驅節點的位置Node* prev = findPrev(data);if (prev->_data == data){//如果相同,則說明已經插入,直接返回即可return false;}//將節點追加到前驅節點后面Node* cur = new Node(data);appendNode(prev, cur);//判斷是否需要晉升int curLevel = 0;std::default_random_engine eg; //隨機數生成引擎std::uniform_real_distribution<double> random(0, 1); //隨機數分布對象//如果拋到正面則一直晉升while (random(eg) < _promoteRate){//判斷當前是否為最高層,如果是最高層則需要增加層數if (curLevel == _maxLevel){addLever();}//找到上一層的前驅節點while (prev->_up == nullptr){prev = prev->_left;}prev = prev->_up;//構造cur節點的上層索引節點,插入到上層的前驅節點后Node* upCur = new Node(data);appendNode(prev, upCur);upCur->_down = cur;cur->_up = upCur;cur = upCur; //繼續往上晉升++curLevel;}return true;}//刪除元素bool erase(const T& data){Node* cur = find(data);if (cur == nullptr){//如果為空則說明該節點不存在,不需要刪除return false;}//自底向上將該節點及它的索引刪除int curLevel = 0;while (cur != nullptr){cur->_right->_left = cur->_left;cur->_left->_right = cur->_right;//如果當前為層只有該節點,則刪除這一層if (curLevel != 0 && cur->_right->_data == INT_MAX && cur->_left->_data == INT_MAX){earseLevel(cur->_left);}else{++curLevel;}//刪除該層的節點后繼續往上刪除索引Node* upCur = cur->_up;delete cur;cur = upCur;}return true;}//刪除全部節點void clear(){//從最底層開始遍歷,一個一個順著往上刪除Node* cur = _head;while (cur->_down != nullptr){cur = cur->_down;}if (cur->_right->_data == INT_MAX){return;}//刪除所有節點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);//如果找到了則返回節點,沒找到則返回空指針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://查找前驅節點Node* findPrev(const T& data){Node* cur = _head;while (1){//找到該層最接近目標的索引while (cur->_right->_data != INT_MAX && Compare()(cur->_right->_data, data)){cur = cur->_right;}//如果當前已經到了最底層,則說明當前位置就是前驅節點,否則繼續往下if (cur->_down == nullptr){break;}else{cur = cur->_down;}}return cur;}//在前驅節點后面插入節點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();//修改相互關系upHead->_right = upTail;upTail->_left = upHead;upHead->_down = _head;_head->_up = upHead;upTail->_down = _tail;_tail->_up = upTail;//因為查詢是自頂向下的,所以將新的頭尾節點作為當前的頭尾節點_head = upHead;_tail = upTail;++_maxLevel; //層數加一}//刪除一層void earseLevel(const Node* upHead){Node* upTail = upHead->_right;//如果當前為最高層,則可以直接刪除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;}}; };

    簡單測試一下

    #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紅黑樹

    從上面的描述可以看出來,跳表的功能和性能都與紅黑樹類似(不了解紅黑樹的可以看我往期博客數據結構:紅黑樹的原理以及實現(C++))

    在Redis中,并沒有選擇使用紅黑樹和B+樹來所謂實現有序集合,而是使用了跳表,原因如下

    • 跳表的插入、刪除、修改等功能與紅黑樹性能大體一樣,但是在區間查找這一方面紅黑樹并不如跳表(平衡樹都需要通過中序遍歷來確認區間,跳表只需要確認起點后順序遍歷),而區間查找在數據庫中又經常使用。
    • 跳表實現起來相對簡單,不容易出錯。
    • 紅黑樹在插入刪除的時候都會涉及到平衡的問題,導致其需要進行旋轉、變色等操作來維持平衡,而跳表只需要進行簡單的鏈表插入

    但是跳表也有一個最大的不足

    • 因為不斷往上構建索引導致空間占用大,典型的以空間換時間(但是Redis官方設計手冊中提到了可以通過調參來降低內存消耗,使其能夠接近平衡樹的空間復雜度。)

    總結

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

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