0X00序文
数日前、Redisを見ると、インターネットを参照してください。彼は、Skiplist
赤黒木ではなくRedisが使用されていると言いましたが、私が全体を無意味にしたとき。
スキップリストと呼ばれるものを調べてみると、翻訳は非常に簡単です。リストをスキップしてください。非常に高度に見えるので、すぐに情報を確認してください
0X10スキップリストの本来の意図
スキップリストLushanの素顔を見る前に、まずそれがどのように表示されたか、またはなぜ表示されたかを理解しましょう。
リンクリストは誰もが知っている必要があります。一般的には、次のようになります。
上記は順序付けられたリンクリストです。リンクリストの利点の1つは、削除と挿入に便利なことです。
ただし、軟膏のハエは、クエリがその弱点の1つであるということです。
リンクリストで検索するため、最初から1つずつしか検索できないため、一般的にリンクリストの検索効率はO(n)です。
それで、これに基づいて、計画を始めた偉大な神々がいます。リンクリストの利便性と配列の検索の利点を組み合わせるにはどうすればよいですか(順序付き配列でのバイナリ検索を参照)。
それで、魔法のジャンプテーブルが出てきました(偉大な神は偉大な神にふさわしいです、数分で出てきます:))
0X20スキップリストの素顔
上記のアイデアは良いのですが、実現するのは難しいですか?
私たちだけがそれを考えることができない、神がそれをすることができないとしか言えません
ジャンプテーブルジャンプテーブル、最も重要なのは跳
上記の一言で、それをジャンプすることに頼る-、索引
それは意味します指针
まあ、それは効率のためのスペースの交換でもあります(そうでなければ、偉大な神は真の神ではありませんが、薄い空気から変形することができます)
さて、準備はほぼ完了しました。「1000回の電話の後に出てください」の時間です。
上の写真!
(見覚えはありますか?はい、データ構造の先生がクラスで話していたのかもしれません!)
奇妙に見えますが、人々はそれを検索しますが、それは良い考えです
例を見て
みましょう。検索する必要があるとします25
。それが一般的な順序のリンクリストである場合、何回比較する必要がありますか?それをしなかった、9回、25より前のすべてのものをもう一度トラバースする必要があります
では、スキップテーブルをどのように使用し、どのように検索し、何回必要ですか?
黒板をノックして、ポイントが来ています!
さあ始めましょう
- 1.現在のポインター
curr
をヘッドノードにポイントし、次にトップポインターがポイントするノードを確認します。21
最初の比較では、21が25より小さいため、現在のcurrは21
このノードをポイントします。 - 2.同じ手法を繰り返し、
21
最初のトップポインターをもう一度見て、NULLをポイントします。これは、2番目の比較が終わったことを意味します。 - 3.この時点で、1レベル下に移動し、それでもNULLをポイントし、もう一度移動して、3回目の比較を行います。
- 4. 2番目のレイヤー(下から上に数える)は
26
このノードを指しています.4番目の比較では、比率が25
大きく、まだ下がる必要があることを示しています - 5.最後のレイヤーになると、今あるかそうでないかで、5回目の比較が行われ、25が見つかります。
最初の9回から5回までは、半分の時間ではありません。
0X30スキップリストのルシャンの素顔
今見てきたように、時計をスキップすることはまだ非常に便利です。しかし、実際、今見たジャンプウォッチは本物のジャンプウォッチではありません。なぜですか?
注意深く観察すると、上記のジャンプテーブルには厳しい規制があることがわかります。上位層のノード数は下位層のノード数の1/2です。
これはまだ非常に問題があります。たとえば、ノードを挿入しても、構造は破壊されませんか?または、そのような構造を維持するために、特定の方法で変更する必要があります。これは赤黒木とほとんど同じです
偉大なる神の解決策は何ですか?それは「規制」することではありません。もちろん、私たちがしているのは、上記の厳格な規制を回避することであり、ルールは一種のランダムになります。
上位層と下位層では1:2は必須ではありません。ノードにインデックスが付けられているかどうか、およびインデックスの層がいくつ構築されているかは、ノードが挿入されたときのコイントスによってすべて決定されます。
もちろん、インデックス付きノードとインデックス付きレベルの数はランダムですが、検索の効率を確保するために、各レベルのノード数が前のセクションの構造とほぼ同じであることを大まかに確認する必要があります。ランダムに表示される実際のジャンプテーブルは次のようになります。
比率は厳密には1:2ではなくなっていますが、全体的にはかなり規則的です。
以下はノード17を挿入するプロセスです。
パフォーマンス分析は行われません(2333がわからないため)が、ジャンプテーブルの挿入/削除/ルックアップはO(logn)であることに注意する必要があります。
0X30ハードコアコーディング-スキップリストのC ++実装
PS:次のコードの99.99%はコピーです(もちろん、直接のコピーではありません。コードコードに少し独自のものを追加してから、コメントを追加してください)が、皆さんが一生懸命勉強して入力できることを願っています。
可能なタイピングのプロセスを自分で消化する何かがおかしいので、注意してください!(後でコードソースへのリンクがあります)
skiplist.h
#ifndef _SKIPLSIT_H
#define _SKIPLSIT_H
#include <iostream>
#include <sstream>
template <typename K,typename V,int MAXLEVEL>
class skiplist_node // 跳表的节点类
{
private:
K key;
V value;
skiplist_node<K,V,MAXLEVEL>* forwards;
public:
skiplist_node()
{
forwards=new skiplist_node<K,V,MAXLEVEL> [MAXLEVEL];
for(int i=0;i<MAXLEVEL;i++)
forwards[i]=nullptr;
}
skiplist_node(K searchKey):key(searchKey)
{
for(int i=0;i<MAXLEVEL;i++)
forwards[i]=nullptr;
}
~skiplist_node()
{
delete [] forwards;
}
};
template<typename K,typename V,MAXLEVEL=16>
class skiplist //跳表类
{
protected:
K m_minKey; //最小键值
K m_maxKey; //最大键值
int man_curr_level; //当前最大层数
skiplist_node<K,V,MAXLEVEL>* m_pHeader; //头指针
skiplist_node<K,V,MAXLEVEL>* m_pTail; //尾指针
double uniformRandom(); //随机函数
int randomLevel(); //生成随机层数
public:
//别名
typedef K KeyType;
typedef V ValueType;
typedef skiplist_node<K,V,MAXLEVEL> NodeType;
const int max_level; //最大层数
skiplist(K minKey,K maxKey);
virtual ~skiplist_node();
void insert(K searchKey,V newValue); //插入
void erase(K searchKey); //删除
const NodeType* find(K searchKey); //查找
bool empty() const; //判断是否为空
std::string printList(); //打印
};
skiplist.cc
#include "stdlib.h"
double skiplist::uniformRandom() //随机函数
{
return rand()/double(RAND_MAX);
}
int skiplist::randomLevel() //生成随机层数
{
int level=1;
double p=0.5;
while(uniformRandom()<p && level < MAXLEVEL)
level++;
return level;
}
skiplist::skiplist(K minKey,K maxKey):m_pHeader(NULL),m_pTail(NULL), //构造函数
max_curr_level(1),max_level(MAXLEVEL),
m_minKey(minKey),m_maxKey(maxKey)
{
m_pHeader = new NdoeType(m_minKey);
m_pTail = new NodeType(m_maxKey);
for(int i=0;i<MAXLEVEL;i++)
m_pHeader->forwards[i]=m_pTail;
}
skiplist::~sikplist() //析构函数
{
NodeType* currNode =m_pHeader->forwards[0];
while(currNode != m_pTail)
{
NodeType* tempNode = currNode;
currNode = currNode->forwards[0];
delete tempNode;
}
delete m_pHeader;
delete m_pTail;
}
void skiplist::insert(K searchKey,V newValue) //插入函数
{
skiplist_node<K,V,MAXLEVEL>* update[MAXKLEVEL];
NodeType* currNode= m_pHear;
for(int level=max_curr_level-1; level>=0; level--) //这个循环用来查找待插入的位置
{
//若第level指向的小于 待查找(searchKey) 的值,则在本层一直往下走
while (currNdoe->forwards[level]->key<searchKey)
currNode = currNode->forwards[level];
//否则,到下一层
update[level] = currNode; //存储走过的路径
}
currNode = currNode->forwards[0]; //此时便是待插入的位置
if(currNode->key == searchKey) //若键值 key 相同
currNode->value = newValue; //更新值
else //否则进行插入
{
int newlevel = randmoLevel(); //或者随机层数
if(newlevel > max_curr_level) //大于当前最大层数,则需要更新
{
for(int level = max_curr_level; level < newlevel; level++)
upadte[level]=m_pHeader;
max_curr_level=newlevel;
}
currNode=new NodeType(searchKey,newValue);
//更新指针的指向,比新节点高的则指向updata后面的,比新节点低的则将updata指向新节点
for(int lv=0;lv<max_curr_level;lv++)
{
currNode->forwards[lv]=update[lv]->forwards[lv];
update[lv]->forwards[lv]=currNode;
}
}
}
void listskip::erase(K searchKey)
{
skiplist_node<K,V,MAXLEVEL>* update[MAXLEVEL];
NodeType* currNode = mpHeader;
for(int level=max_curr_level-1; level>=0; level--) //同上面的 insert
{
while(currNode->forwards[level]->key < searchKey)
currNode = currNode->forwards[level];
update[level]=currNode;
}
currNode = currNode->forwards[0]; //找到需要查找的位置
if(currNode->key == searchKey)//如果相等则说明存在需要删除
{
for(int lv=0;lv<max_curr_level;lv++) //更新指针指向
{
if(update[lv]->forwards[lv]!=currNode) break;
update[lv]->forwards[lv]=currNode->forwards[lv];
}
delete currNode;
// update the max level
while(max_curr_level > 0 && m_pHeader->forwards[max_curr_level]==nullptr)
max_curr_level--;
}
}
const NodeType* skiplist::find(K searchKey)
{
NodeType* currNode = m_pHeader;
for(int level=max_curr_level-1;level>=0;level--) //同上,但寻找无需存储走过的路径
while(currNode->forwards[level]->key < searchKey)
currNode = currNode->forwards[level];
currNode = currNode->forwards[0];
if(currNode->key==searchKey) //找到返回 找到的节点
return currNode;
else return nullptr; //否则返回空
}
bool skiplist::empty() const //判断是否为空
{
return (m_pHeader->forwards[0]==m_pTail);
}
std::string skiplist::printList() //打印
{
std::stringstream sstr;
NodeTpye* currNode = m_pHeader->forwards[0];
while(currNode != m_pTail)
{
sstr << "(" << currNode->key << "," << currNode->value << ")" << endl;
currNode = currNode->forwards[0];
}
return sstr.str();
}
コードは上の図と少し異なるため、説明する点がいくつかあり
ます。1。コードは<key、value>の形式を格納するため、上記の単一の値だけではありません。キーは、
ヘッド2、コード、テールノードを比較するために使用されます。図のNULLではなく、特定のmin_value
andに格納されるmax_value
ため、特定のキー範囲が必要です。
0X30あとがき
明確にする必要があるのは、たくさんの詩を除いて、上記のコンテンツのほとんどはオンラインの大物に基づいているということです。気にしないでください。
最後に、大物の記事へのリンクを投稿します。一緒に学びましょう〜
その後、またお会いしましょう