目次
作者には言いたいことがある
なぜジャンプウォッチを学ぶのですか?速くなるために、速くなるために、自分を苦しめるために...
ジャンプ台のアクションシーン
1. 多くの企業がハッシュ テーブルを独自に設計するため、ハッシュの競合を解決することは避けられません。通常はリンクアドレスが使われますが、競合が発生した場合、追加の連結リストに1ビット追加することはよく知られています(使用する循環二重連結リストを後で直接追加しない場合、効率は非常に高くなりますO(1) . . . である場合 単方向リンクリストは、ヘッド補間法を使用してヘッドに直接追加することもでき、非常に効率的です (O(1)、ジャンプテーブルを使用する場合は O(logN) が必要です)。
2.これを見て、多くの友人は時計をジャンプする必要があると感じ、速度が低下します。心配しないで、引き続き下を見てみましょう。最初に単純な頭差法を使用します。取得されたシーケンスは順不同であると見なすことができます (順次挿入が順序付けられているかどうかに関係なく)。その後、検索はより面倒になり、O(N) が必要になります。 . ただし、スキップ テーブル ルックアップを使用した場合の平均的な効率は、O(logN)、挿入 O(logN)、削除 O(logN) です。
[ここに書かれていることは平均的な状況であることに注意してください。ジャンプ テーブルが適切に設計されていない場合、ジャンプ テーブルがリンクされたリストに縮退することは容易です]
3. ジャンプテーブルは赤黒木やAVLツリーを置き換えることもある. ジャンプテーブルの挿入や削除の保守性はAVLツリーよりも低く, 赤黒木に似ているとも言われている. .
スキップテーブルの主なアイデア
2点!2点!それでも2点!
プログラミングに長く触れていると、二分法の考え方に基づいて多くの優れたアルゴリズムが進化していることに気付くでしょう。
アルゴリズムの選択方法については、特定のビジネス状況と組み合わせる必要があり、アルゴリズムの最適な時間複雑度、平均時間複雑度、および最悪の時間複雑度を考慮する必要があります。通常、それらの時間計算量は O(logN) であると考えていても、異なるアプリケーション シナリオで同じ機能を持つ 2 つのアルゴリズムの間には大きなギャップがあります。
時間の複雑性と同様の概念は空間の複雑性であり、空間の複雑性も考慮する必要がある問題です。1 つは CPU を占有し、1 つはメモリを占有します...
空間の複雑さは常に時間の複雑さと相反するものであり、2 つは一対の敵です。しかし、一般的には、許容されるメモリ消費量の範囲内で、最速のアルゴリズムを選択することを受け入れます。世界一の格闘技は足が速い…
スキップテーブルの特徴
1.片方向リスト(これは絶対ではありません。範囲内で検索したい場合は、両方向リストを使用する方が高速です==>未テスト)
2. 秩序ある保存(二分法の前提条件)
3.追加、削除、検索をサポート
4. 検索効率 O(logN)
ジャンプリストと通常の連結リストの違い
通常の一重連結リスト
通常の単方向リストでは、ノードが前にあるほど検索が速くなります。後のノードでは、検索効率が低くなります。その平均効率 = (1+ 2 + 3 + ... + n) / n = (1/2 * n(a1+an))/n = (1 + n) / 2 ==> O(N)
シンプルなスキップテーブル
ジャンプテーブルの構造は、任意の要素の検索が線形時間 O(logN) に近づくように、高緯度インデックスを確立することによって低緯度を減らすことです。実際、テーブルをスキップするという考えは複雑ではなく、検索効率を向上させるために、中間ノードの次元を増やし、検索プロセス中に検索プロセスを徐々に半分に減らします。
スキップテーブルの作成
ジャンプ台の基本的な考え方がわかったら、手動でシミュレートしてジャンプ台を作成しましょう。次に、7 つの要素 1 5 8 3 2 7 9 を順番に挿入します。===> [1 間隔で高緯度ノードを抽出]
1. 初期化 ==> 先頭ノードを準備する. リンク リストには 2 つの方法があることがわかっています (先頭ノードがあるものとないもの)。最初のノードの操作を容易にするために、先頭ノードを一様に使用します。ノード)。
注: ポインター フィールドはここでは描画されません。
2. 1 を挿入 ==> 単方向リストのようにノードを右に直接挿入するだけです。
注: 空白の部分は、ワイルド ポインターを防ぐためにポインターが NULL を指すことを意味します。
3. 5 を挿入 ==>ここでは、要素を上に抽出する (または抽出しない) ことを考慮し、最初に検索する位置を見つけます (5 > 1) ので、1 の後に挿入するだけで十分です。
==> 次の一貫性を確保し、コードを書くときに考慮すべき状況を 1 つ減らすために、5 を抽出します。
注: 空白の部分は、ワイルド ポインターを防ぐためにポインターが NULL を指すことを意味します。
4. 8 を挿入 ==> 同じ理由で、8 を見つけるために検索する必要がある位置を抽出する必要があります。8 > 5 5 は事前に改善されているため、1 を比較してから 5 を比較する必要はありません。直接 5 を取得すると、5 の後に 8 を挿入する必要があることがわかり、ジャンプ テーブルの利点が示されます。
【本工程の挿入工程は2段階に分かれており、まず1次元連結リストに8を挿入】
注: 空白の部分は、ワイルド ポインターを防ぐためにポインターが NULL を指すことを意味します。
注 :空白の部分は、ポインターが野生のポインターを防ぐためにヌルを指すことを意味します
5. Insert 3 ==> 最初に 5 > 3 を比較し、次に範囲全体が [1-5] になり、次に 1 < 3 < 5 になります。次に、1と5の間に3を挿入する必要があります
[挿入 3、2 つの抽出の原則に従って、3 をアップグレードする必要があり、3 と 5 は同じレイヤーにあるため、5 をアップグレードする必要があります]
注 :空白の部分は、ポインターが野生のポインターを防ぐためにヌルを指すことを意味します
6. Insert 2 ==> プロセスは上記と同じです。余計な説明はいらない…
[絵を描くのは簡単ではありません。ただ注意してください....高品質の記事のみを出力してください: アルゴリズムについて何か考えがある場合は、以下にメッセージを残してください]
注: 空白の部分は、ワイルド ポインターを防ぐためにポインターが NULL を指すことを意味します。
7. 7 を挿入 ==> プロセスは上記と同じです。余計な説明はいらない…
注: 空白の部分は、ワイルド ポインターを防ぐためにポインターが NULL を指すことを意味します。
8. 9 を挿入 ==> プロセスは上記と同じです。余計な説明はいらない…
注: 空白の部分は、ワイルド ポインターを防ぐためにポインターが NULL を指すことを意味します。
[上記は、ジャンプ テーブルを設定する全体のプロセスです。理解できない場合は、WeChat C++ 技術交換グループに参加できます: C++ 技術交換グループ - 陳おじさん]
[陳大樹おじさんを探しているbilibiliのビデオは後で更新されます。注意していただきありがとうございます...作成するのは簡単ではありません。フォローするだけです... ]
テーブル ルックアップをスキップ
ジャンプ テーブルのルックアップは非常に単純で、これは二分探索のロジックです. 検索プロセスをよりよく理解するために、例を見てみましょう.
例 1: 検索 3
最初の比較 5 ==> 3 < 5 ==> 検索範囲は 1 ~ 5 の間になります
次に、ヘッダーを 1 ビット下に移動し、3 == 3 ==> を比較して 3 を見つけ、true を返します。
例 2: 9 を検索
スキップリストの削除
ジャンプ テーブルのルックアップは非常に単純で、これは二分探索のロジックです. 検索プロセスをよりよく理解するために、例を見てみましょう.
例 1: 削除 2
継続的な増加とレイヤー数が多すぎるのを防ぐために、調整を行うことができます
スキップ表の抽出次元の説明
ジャンプ テーブルの実装が、時間をスペースと交換する、データに対する上向きの冗長操作の典型的な例であることを見つけるのは難しくありません。
設定する冗長パーティクルが小さいほど、必要なスペースが大きくなります。したがって、特定の状況に応じて決定する必要がある適切な冗長性粒度を選択することが非常に重要です。
再ジャンプテーブルにはランダム機能があり、その意味は、いつ上向きの冗長性を作成する必要があるかを判断することです。実際の建設プロセスは例と同じではないため、コストは比較的高くなります。挿入されたノードの増加によりジャンプ テーブルがリンクされたリストに縮退するのを防ぐために、通常はランダム関数を使用してアップグレードのタイミングを決定します。
ジャンプ台コード
#pragma once #ifndef SKIPLIST_ENTRY_H_ #define SKIPLIST_ENTRY_H_ /* 一个更具备代表性的泛型版本 */ #include <ctime> #include <cstdlib> template<typename T> class Entry { private: int key; // 排序值 T value; // 保存对象 Entry* pNext; Entry* pDown; public: Entry(int k, T v) :value(v), key(k), pNext(nullptr), pDown(nullptr) {} Entry(const Entry& e) :value(e.value), key(e.key), pNext(nullptr), pDown(nullptr) {} public: /* 重载运算符 */ bool operator<(const Entry& right) { return key < right.key; } bool operator>(const Entry& right) { return key > right.key; } bool operator<=(const Entry& right) { return key <= right.key; } bool operator>=(const Entry& right) { return key >= right.key; } bool operator==(const Entry& right) { return key == right.key; } Entry*& next() { return pNext; } Entry*& down() { return pDown; } }; template<typename T> class SkipList_Entry { private: struct Endpoint { Endpoint* up; Endpoint* down; Entry<T>* right; }; struct Endpoint* header; int lvl_num; // level_number 已存在的层数 unsigned int seed; bool random() { srand(seed); int ret = rand() % 2; seed = rand(); return ret == 0; } public: SkipList_Entry() :lvl_num(1), seed(time(0)) { header = new Endpoint(); } /* 插入新元素 */ void insert(Entry<T>* entry) { // 插入是一系列自底向上的操作 struct Endpoint* cur_header = header; // 首先使用链表header到达L1 while (cur_header->down != nullptr) { cur_header = cur_header->down; } /* 这里的一个简单想法是L1必定需要插入元素,而在上面的各跳跃层是否插入则根据random确定 因此这是一个典型的do-while循环模式 */ int cur_lvl = 0; // current_level 当前层数 Entry<T>* temp_entry = nullptr; // 用来临时保存一个已经完成插入的节点指针 do { Entry<T>* cur_cp_entry = new Entry<T>(*entry); // 拷贝新对象 // 首先需要判断当前层是否已经存在,如果不存在增新增 cur_lvl++; if (lvl_num < cur_lvl) { lvl_num++; Endpoint *new_header = new Endpoint(); new_header->down = header; header->up = new_header; header = new_header; } // 使用cur_lvl作为判断标准,!=1表示cur_header需要上移并连接“同位素”指针 if (cur_lvl != 1) { cur_header = cur_header->up; cur_cp_entry->down() = temp_entry; } temp_entry = cur_cp_entry; // 再需要判断的情况是当前所在链表是否已经有元素节点存在,如果是空链表则直接对右侧指针赋值并跳出循环 if (cur_header->right == nullptr) { cur_header->right = cur_cp_entry; break; } else { Entry<T>* cursor = cur_header->right; // 创建一个游标指针 while (true) { // 于当前链表循环向右寻找可插入点,并在找到后跳出当前循环 if (*cur_cp_entry < *cursor) { // 元素小于当前链表所有元素,插入链表头 cur_header->right = cur_cp_entry; cur_cp_entry->next() = cursor; break; } else if (cursor->next() == nullptr) { // 元素大于当前链表所有元素,插入链表尾 cursor->next() = cur_cp_entry; break; } else if (*cur_cp_entry < *cursor->next()) { // 插入链表中间 cur_cp_entry->next() = cursor->next(); cursor->next() = cur_cp_entry; break; } cursor = cursor->next(); // 右移动游标 } } } while(random()); } /* 查询元素 */ bool search(Entry<T>* entry) const { if (header->right == nullptr) { // 判断链表头右侧空指针 return false; } Endpoint* cur_header = header; // 在lvl_num层中首先找到可以接入的点 for (int i = 0; i < lvl_num; i++) { if (*entry < *cur_header->right) { cur_header = cur_header->down; } else { Entry<T>* cursor = cur_header->right; while (cursor->down() != nullptr) { while (cursor->next() != nullptr) { if (*entry <= *cursor->next()) { break; } cursor = cursor->next(); } cursor = cursor->down(); } while (cursor->next() != nullptr) { if (*entry > *cursor->next()) { cursor = cursor->next(); } else if (*entry == *cursor->next()) { return true; } else { return false; } } return false; // 节点大于L1最后一个元素节点,返回false } } return false; // 找不到接入点,则直接返回false; } /* 删除元素 */ void remove(Entry<T>* entry) { if (header->right == nullptr) { return; } Endpoint* cur_header = header; Entry<T>* cursor = cur_header->right; int lvl_counter = lvl_num; // 因为在删除的过程中,跳跃表的层数会中途发生变化,因此应该在进入循环之前要获取它的值。 for (int i = 0; i < lvl_num; i++) { if (*entry == *cur_header->right) { Entry<T>* delptr = cur_header->right; cur_header->right = cur_header->right->next(); delete delptr; } else { Entry<T> *cursor = cur_header->right; while (cursor->next() != nullptr) { if (*entry == *cursor->next()) { // 找到节点->删除->跳出循环 Entry<T>* delptr = cursor->next(); cursor->next() = cursor->next()->next(); delete delptr; break; } cursor = cursor->next(); } } // 向下移动链表头指针的时候需要先判断当前链表中是否还存在Entry节点 if (cur_header->right == nullptr) { Endpoint* delheader = cur_header; cur_header = cur_header->down; header = cur_header; delete delheader; lvl_num--; } else { cur_header = cur_header->down; } } } }; #endif // !SKIPLIST_ENTRY_H_
C++の学習に関しては、コミュニケーションできる人が少なく、オンライン教材も比較的少ないことが最大の問題の1つです。
C++ について疑問がある場合、または友人とコミュニケーションを取りたい場合は、V: Errrr113 に参加することを忘れないでください。
初心を忘れず、勇気と決断力を持って…テクノロジーを愛するすべての仲間へ!! !