c++如何實現(xiàn)跳表(skiplist)
引言
二分查找底層依賴的是數(shù)組隨機訪問的特性,所以只能用數(shù)組來實現(xiàn)。如果數(shù)據(jù)存儲在鏈表中,就真的沒法用二分查找算法了嗎?實際上,只需要對鏈表稍加改造,就可以支持類似“二分”的查找算法。改造之后的數(shù)據(jù)結(jié)構(gòu)叫作跳表。
定義
跳表是一個隨機化的數(shù)據(jù)結(jié)構(gòu)。它允許快速查詢一個有序連續(xù)元素的數(shù)據(jù)鏈表。跳躍列表的平均查找和插入時間復(fù)雜度都是O(log n),優(yōu)于普通隊列的O(n)。性能上和紅黑樹,AVL樹不相上下,但跳表的原理非常簡單,目前Redis和LevelDB中都有用到。
跳表是一種可以替代平衡樹的數(shù)據(jù)結(jié)構(gòu)。跳表追求的是概率性平衡,而不是嚴格平衡。因此,跟平衡二叉樹相比,跳表的插入和刪除操作要簡單得多,執(zhí)行也更快。
C++簡單實現(xiàn)
下面實現(xiàn)過程主要是簡單實現(xiàn)跳表的過程,不是多線程安全的,LevelDB實現(xiàn)的跳表支持多線程安全,用了std::atomic原子操作,本文主要是為了理解跳表的原理,所以采用最簡單的實現(xiàn)。
#ifndef SKIPLIST_H #define SKIPLIST_H #include <ctime> #include <initializer_list> #include <iostream> #include <random> template <typename Key> class Skiplist { public: struct Node { Node(Key k) : key(k) {} Key key; Node* next[1]; // C語言中的柔性數(shù)組技巧 }; private: int maxLevel; Node* head; enum { kMaxLevel = 12 }; public: Skiplist() : maxLevel(1) { head = newNode(0, kMaxLevel); } Skiplist(std::initializer_list<Key> init) : Skiplist() { for (const Key& k : init) { insert(k); } } ~Skiplist() { Node* pNode = head; Node* delNode; while (nullptr != pNode) { delNode = pNode; pNode = pNode->next[0]; free(delNode); // 對應(yīng)malloc } } // 禁止拷貝構(gòu)造和賦值 Skiplist(const Skiplist&) = delete; Skiplist& operator=(const Skiplist&) = delete; Skiplist& operator=(Skiplist&&) = delete; private: Node* newNode(const Key& key, int level) { /* * 開辟sizeof(Node) + sizeof(Node*) * (level - 1)大小的空間 * sizeof(Node*) * (level - 1)大小的空間是給Node.next[1]指針數(shù)組用的 * 為什么是level-1而不是level,因為sizeof(Node)已包含一個Node*指針的空間 */ void* node_memory = malloc(sizeof(Node) + sizeof(Node*) * (level - 1)); Node* node = new (node_memory) Node(key); for (int i = 0; i < level; ++i) node->next[i] = nullptr; return node; } /* * 隨機函數(shù),范圍[1, kMaxLevel],越小概率越大 */ static int randomLevel() { int level = 1; while (rand() % 2 && level < kMaxLevel) level++; return level; } public: Node* find(const Key& key) { // 從最高層開始查找,每層查找最后一個小于key的前繼節(jié)點,不斷縮小范圍 Node* pNode = head; for (int i = maxLevel - 1; i >= 0; --i) { while (pNode->next[i] != nullptr && pNode->next[i]->key < key) { pNode = pNode->next[i]; } } // 如果第一層的pNode[0]->key == key,則返回pNode->next[0],即找到key if (nullptr != pNode->next[0] && pNode->next[0]->key == key) return pNode->next[0]; return nullptr; } void insert(const Key& key) { int level = randomLevel(); Node* new_node = newNode(key, level); Node* prev[kMaxLevel]; Node* pNode = head; // 從最高層開始查找,每層查找最后一個小于key的前繼節(jié)點 for (int i = level - 1; i >= 0; --i) { while (pNode->next[i] != nullptr && pNode->next[i]->key < key) { pNode = pNode->next[i]; } prev[i] = pNode; } // 然后每層將新節(jié)點插入到前繼節(jié)點后面 for (int i = 0; i < level; ++i) { new_node->next[i] = prev[i]->next[i]; prev[i]->next[i] = new_node; } if (maxLevel < level) // 層數(shù)大于最大層數(shù),更新最大層數(shù) maxLevel = level; } void erase(const Key& key) { Node* prev[maxLevel]; Node* pNode = head; // 從最高層開始查找,每層查找最后一個小于key的前繼節(jié)點 for (int i = maxLevel - 1; i >= 0; --i) { while (pNode->next[i] != nullptr && pNode->next[i]->key < key) pNode = pNode->next[i]; prev[i] = pNode; } // 如果找到key, if (pNode->next[0] != nullptr && pNode->next[0]->key == key) { Node *delNode = pNode->next[0]; // 從最高層開始,如果當(dāng)前層的next節(jié)點的值等于key,則刪除next節(jié)點 for (int i = maxLevel - 1; i >= 0; --i) { if (prev[i]->next[i] != nullptr && key == prev[i]->next[i]->key) prev[i]->next[i] = prev[i]->next[i]->next[i]; } free(delNode); // 最后銷毀pNode->next[0]節(jié)點 } // 如果max_level>1且頭結(jié)點的next指針為空,則該層已無數(shù)據(jù),max_level減一 while (maxLevel > 1 && head->next[maxLevel] == nullptr) { maxLevel--; } } }; #endif
Redis和LevelDB選用跳表而棄用紅黑樹的原因
- Skiplist的復(fù)雜度和紅黑樹一樣,而且實現(xiàn)起來更簡單。
- 在并發(fā)環(huán)境下Skiplist有另外一個優(yōu)勢,紅黑樹在插入和刪除的時候可能需要做一些rebalance的操作,這樣的操作可能會涉及到整個樹的其他部分,而skiplist的操作顯然更加局部性一些,鎖需要盯住的節(jié)點更少,因此在這樣的情況下性能好一些。
以上就是c++如何實現(xiàn)跳表的詳細內(nèi)容,更多關(guān)于c++ 跳表的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++如何將二叉搜索樹轉(zhuǎn)換成雙向循環(huán)鏈表(雙指針或數(shù)組)
這篇文章主要介紹了C++如何將二叉搜索樹轉(zhuǎn)換成雙向循環(huán)鏈表(雙指針或數(shù)組),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-05-05Linux下g++編譯與使用靜態(tài)庫和動態(tài)庫的方法
下面小編就為大家?guī)硪黄狶inux下g++編譯與使用靜態(tài)庫和動態(tài)庫的方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-05-05C語言數(shù)據(jù)結(jié)構(gòu)之順序數(shù)組的實現(xiàn)
這篇文章主要介紹了C語言數(shù)據(jù)結(jié)構(gòu)之順序數(shù)組的實現(xiàn)的相關(guān)資料,這里提供實現(xiàn)實例,希望通過本文能幫助到大家,需要的朋友可以參考下2017-08-08C++中使用哈希表(unordered_map)的一些常用操作方法
C++標準庫中使用的unordered_map底層實現(xiàn)是哈希表,下面這篇文章主要給大家介紹了關(guān)于C++中使用哈希表(unordered_map)的一些常用操作方法,需要的朋友可以參考下2022-03-03C++數(shù)據(jù)結(jié)構(gòu)之搜索二叉樹的實現(xiàn)
了解搜索二叉樹是為了STL中的map和set做鋪墊,我們所熟知的AVL樹和平衡搜索二叉樹也需要搜索二叉樹的基礎(chǔ)。本文將詳解如何利用C++實現(xiàn)搜索二叉樹,需要的可以參考一下2022-05-05C/C++字符串與數(shù)字互轉(zhuǎn)的實現(xiàn)
這篇文章主要介紹了C/C++字符串與數(shù)字互轉(zhuǎn)的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-01-01