c++如何實(shí)現(xiàn)跳表(skiplist)
引言
二分查找底層依賴的是數(shù)組隨機(jī)訪問(wèn)的特性,所以只能用數(shù)組來(lái)實(shí)現(xiàn)。如果數(shù)據(jù)存儲(chǔ)在鏈表中,就真的沒(méi)法用二分查找算法了嗎?實(shí)際上,只需要對(duì)鏈表稍加改造,就可以支持類似“二分”的查找算法。改造之后的數(shù)據(jù)結(jié)構(gòu)叫作跳表。
定義
跳表是一個(gè)隨機(jī)化的數(shù)據(jù)結(jié)構(gòu)。它允許快速查詢一個(gè)有序連續(xù)元素的數(shù)據(jù)鏈表。跳躍列表的平均查找和插入時(shí)間復(fù)雜度都是O(log n),優(yōu)于普通隊(duì)列的O(n)。性能上和紅黑樹(shù),AVL樹(shù)不相上下,但跳表的原理非常簡(jiǎn)單,目前Redis和LevelDB中都有用到。
跳表是一種可以替代平衡樹(shù)的數(shù)據(jù)結(jié)構(gòu)。跳表追求的是概率性平衡,而不是嚴(yán)格平衡。因此,跟平衡二叉樹(shù)相比,跳表的插入和刪除操作要簡(jiǎn)單得多,執(zhí)行也更快。
C++簡(jiǎn)單實(shí)現(xiàn)
下面實(shí)現(xiàn)過(guò)程主要是簡(jiǎn)單實(shí)現(xiàn)跳表的過(guò)程,不是多線程安全的,LevelDB實(shí)現(xiàn)的跳表支持多線程安全,用了std::atomic原子操作,本文主要是為了理解跳表的原理,所以采用最簡(jiǎn)單的實(shí)現(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語(yǔ)言中的柔性數(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); // 對(duì)應(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)
{
/*
* 開(kāi)辟sizeof(Node) + sizeof(Node*) * (level - 1)大小的空間
* sizeof(Node*) * (level - 1)大小的空間是給Node.next[1]指針數(shù)組用的
* 為什么是level-1而不是level,因?yàn)閟izeof(Node)已包含一個(gè)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;
}
/*
* 隨機(jī)函數(shù),范圍[1, kMaxLevel],越小概率越大
*/
static int randomLevel()
{
int level = 1;
while (rand() % 2 && level < kMaxLevel)
level++;
return level;
}
public:
Node* find(const Key& key)
{
// 從最高層開(kāi)始查找,每層查找最后一個(gè)小于key的前繼節(jié)點(diǎn),不斷縮小范圍
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;
// 從最高層開(kāi)始查找,每層查找最后一個(gè)小于key的前繼節(jié)點(diǎn)
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é)點(diǎn)插入到前繼節(jié)點(diǎn)后面
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;
// 從最高層開(kāi)始查找,每層查找最后一個(gè)小于key的前繼節(jié)點(diǎn)
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];
// 從最高層開(kāi)始,如果當(dāng)前層的next節(jié)點(diǎn)的值等于key,則刪除next節(jié)點(diǎn)
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é)點(diǎn)
}
// 如果max_level>1且頭結(jié)點(diǎn)的next指針為空,則該層已無(wú)數(shù)據(jù),max_level減一
while (maxLevel > 1 && head->next[maxLevel] == nullptr)
{
maxLevel--;
}
}
};
#endif
Redis和LevelDB選用跳表而棄用紅黑樹(shù)的原因
- Skiplist的復(fù)雜度和紅黑樹(shù)一樣,而且實(shí)現(xiàn)起來(lái)更簡(jiǎn)單。
- 在并發(fā)環(huán)境下Skiplist有另外一個(gè)優(yōu)勢(shì),紅黑樹(shù)在插入和刪除的時(shí)候可能需要做一些rebalance的操作,這樣的操作可能會(huì)涉及到整個(gè)樹(shù)的其他部分,而skiplist的操作顯然更加局部性一些,鎖需要盯住的節(jié)點(diǎn)更少,因此在這樣的情況下性能好一些。
以上就是c++如何實(shí)現(xiàn)跳表的詳細(xì)內(nèi)容,更多關(guān)于c++ 跳表的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一篇文章帶你了解C語(yǔ)言的選擇結(jié)構(gòu)
這篇文章主要為大家介紹了C語(yǔ)言的選擇結(jié)構(gòu),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-01-01
C++中基類和派生類之間的轉(zhuǎn)換實(shí)例教程
這篇文章主要介紹了C++中基類和派生類之間的轉(zhuǎn)換,有助于深入理解C++面向?qū)ο蟪绦蛟O(shè)計(jì),需要的朋友可以參考下2014-08-08
C語(yǔ)言使用ffmpeg和sdl實(shí)現(xiàn)多路音頻播放
這篇文章主要為大家詳細(xì)介紹了一種基于ffmpeg和sdl實(shí)現(xiàn)的音頻多路混合的方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以參考一下2023-06-06

