当前位置: 首页 > news >正文

跳跃表详解及案例

文章目录

    • 跳跃表节点与结构
    • 详解跳跃表的插入和删除操作
    • 关于删除操作以及一些实现上的注意事项
    • 综合案例

跳跃表节点与结构

跳跃表(Skip List)是一种非完全平衡的搜索结构,它的设计目的是为了提高搜索效率,尤其是在大型有序列表中。跳跃表中的每个节点都包含了一个或多个指向其他节点的链接(或称为“指针”),这些链接允许搜索算法在遍历过程中跳过一些元素,从而加快搜索的速度。

节点结构

跳跃表中的一个典型节点通常包含以下部分:

  1. 数据元素:这是节点存储的实际数据,可以是任何类型的数据(整数、字符串等)。
  2. 向前指针数组:每个节点都包含一个向前指针数组,数组中的每个元素都是一个指针,指向该层的下一个节点。数组的第一个元素对应于最高层,最后一个元素对应于最低层。
  3. 向后指针(可选):有些实现会包含一个向后的指针,这有助于删除操作的执行。

结构

跳跃表的结构特点如下:

  1. 多层结构:跳跃表由多个层级组成,其中每一层都是一个有序的链表。最顶层的链表包含的节点最少,而底层链表包含所有节点。
  2. 层级高度:节点的层级高度是随机确定的,通常使用一个概率分布来决定一个新插入节点的层级高度。常见的概率分布是50%,这意味着新节点有一半的概率在当前层创建一个额外的前向指针。
  3. 更新指针:在插入或删除节点时,通常会维护一个“更新指针”数组,它记录了当前遍历路径上每个层级的前一个节点。这样做的目的是为了在插入或删除时能够快速更新受影响节点的指针。

示例

假设我们有一个跳跃表,其节点包含两个向前指针,表示该节点最多可以在两层内向前指。节点结构可能如下所示:

struct SkipListNode {int key;       // 存储的键值void* value;   // 关联的值SkipListNode** forward; // 向前指针数组
};

在实际应用中,forward数组的大小可以根据需要动态调整。例如,如果某个节点的高度为3,则forward数组将包含3个元素。

插入和删除操作

  • 插入:插入一个新节点时,首先需要确定其高度,然后从最高层开始,逐层向下找到合适的位置插入新节点,并更新必要的指针。
  • 删除:删除一个节点时,从最高层开始查找该节点,一旦找到就断开该节点的所有前向指针,并更新其前后节点的指针关系。

跳跃表的这些特性使得它在处理大量有序数据时非常有效,尤其是在实时系统中需要快速响应的情况下。

详解跳跃表的插入和删除操作

以下是跳跃表的插入和删除操作,以及它们的具体实现细节。

插入操作

插入一个新节点到跳跃表中涉及到以下几个步骤:

  1. 确定节点高度:新节点的高度是随机确定的,通常使用一个随机数生成器,按照一定的概率(如50%)来决定是否增加新节点的高度。

  2. 寻找插入位置:从跳跃表的最高层开始,使用当前节点的向前指针逐步向下移动,直到找到合适的位置。这个过程类似于二分查找,利用了跳跃表的层次结构来加速定位。

  3. 更新指针:在找到插入位置之后,需要更新该位置及其前面节点的指针,使其指向新的节点。同时,如果新节点的高度大于当前层,那么还需要在更高的层上进行同样的指针更新。

  4. 创建新节点并插入:创建一个新的节点,并将其插入到适当的位置。新节点的向前指针需要初始化为适当的值。

删除操作

删除一个节点的过程相对直接,但需要注意的是,删除操作可能会导致节点的高度降低,进而影响跳跃表的整体结构。

  1. 查找目标节点:同样地,从跳跃表的最高层开始,逐步向下移动,直到找到要删除的目标节点。

  2. 检查并删除:一旦找到目标节点,就需要检查该节点在各个层级上的向前指针,并将这些指针重新指向被删除节点之前的节点。

  3. 更新跳跃表高度:如果删除的节点是最高层的一部分,并且删除后该层不再有任何节点,那么跳跃表的高度就会减少。

实现细节

在实现跳跃表时,有几个细节需要特别注意:

  • 随机性:节点的高度是随机确定的,通常使用一个均匀分布的随机数生成器,如 rand() 函数,并通过比较来决定是否增加高度。
  • 更新指针数组:在插入或删除时,需要维护一个更新指针数组(通常称为 updateupdateStack),记录每层的前一个节点,以便于快速更新受影响的指针。
  • 边界条件:处理边界情况,如插入到空表或删除最后一层的情况,需要特别注意避免指针错误。

示例代码片段

下面是一个简化的C++示例,展示如何在跳跃表中插入一个新节点:

void insert(int key, void* value) {SkipListNode* newNode = new SkipListNode;newNode->key = key;newNode->value = value;int newNodeHeight = randomHeight(); // 根据概率确定高度SkipListNode* update[newNodeHeight]; // 更新指针数组SkipListNode* current = head; // 从头部开始for (int i = newNodeHeight - 1; i >= 0; i--) {while (current->forward[i] != nullptr && current->forward[i]->key < key) {current = current->forward[i];}update[i] = current;}// 如果找到了相等的键值,则更新其值if (current->forward[0] != nullptr && current->forward[0]->key == key) {current->forward[0]->value = value;return;}// 创建新节点并插入for (int i = 0; i < newNodeHeight; i++) {newNode->forward[i] = update[i]->forward[i];update[i]->forward[i] = newNode;}
}

这个示例假设 randomHeight() 是一个返回随机高度的函数,head 是跳跃表的头节点。

跳跃表因其简洁性和高性能而在许多应用场景中得到了广泛的应用,特别是在不需要严格平衡的情况下,它可以提供比平衡树更好的性能。

关于删除操作以及一些实现上的注意事项

删除操作

删除一个节点的操作涉及以下步骤:

  1. 查找目标节点:从跳跃表的最高层开始,逐步向下移动,直到找到要删除的目标节点。在查找过程中,需要记录每个层级的前一个节点,这将用于后续更新指针。

  2. 更新指针:找到目标节点后,需要更新该节点在各层的向前指针。具体来说,需要将目标节点前一个节点的向前指针重定向为目标节点的下一个节点。

  3. 调整跳跃表的高度:如果删除的节点位于最高层,并且删除后该层没有任何节点,则需要将跳跃表的高度减一。

下面是删除操作的一个简化示例代码:

void remove(int key) {SkipListNode* update[MAX_LEVELS]; // 更新指针数组SkipListNode* current = head; // 从头部开始// 查找目标节点for (int i = numLevels - 1; i >= 0; i--) {while (current->forward[i] != nullptr && current->forward[i]->key < key) {current = current->forward[i];}update[i] = current;}// 检查目标节点是否存在current = current->forward[0];if (current != nullptr && current->key == key) {// 更新指针for (int i = 0; i < numLevels; i++) {if (current->forward[i] != nullptr) {update[i]->forward[i] = current->forward[i];} else {update[i]->forward[i] = nullptr;}}// 释放节点delete current;// 调整跳跃表的高度while (numLevels > 1 && head->forward[numLevels - 1] == nullptr) {numLevels--;}}
}

在这个示例中,MAX_LEVELS 是一个常量,表示跳跃表的最大可能层数,numLevels 表示当前跳跃表的实际层数。

实现注意事项

在实现跳跃表时,还需要注意以下几个方面:

  1. 内存管理:在删除节点时,确保正确释放节点占用的内存。此外,对于动态调整的指针数组,也需要适当地分配和释放内存。

  2. 边界条件处理:处理边界条件,例如删除空跳跃表中的节点或删除不存在的键值时,应避免程序崩溃或产生未定义行为。

  3. 随机性的影响:节点高度的随机性可能导致跳跃表的平均高度接近 log(n),但在极端情况下,跳跃表的高度可能显著高于预期。因此,在实践中,可能需要监控跳跃表的高度以确保性能不会显著下降。

  4. 并发访问:如果跳跃表被多线程访问,需要考虑同步机制来防止数据竞争和不一致状态。

性能优化

跳跃表的性能可以通过以下几种方式进行优化:

  1. 预分配空间:在插入节点之前,可以预先分配足够的空间给节点的向前指针数组,以减少动态调整数组带来的开销。

  2. 缓存机制:对于频繁访问的节点,可以使用缓存机制来提高访问速度。

  3. 局部性优化:在内存布局上,尽量让频繁交互的节点靠近,以利用CPU缓存。

跳跃表是一种非常灵活的数据结构,适合需要快速查找、插入和删除操作的应用场景。在实际应用中,可以根据具体需求对跳跃表进行定制化实现,以达到最佳性能。

综合案例

让我们通过一个详细的综合案例来展示跳跃表是如何在实际应用中实现和使用的。我们将从零开始构建一个跳跃表,并实现基本的插入、查找和删除功能。
案例背景

假设我们需要实现一个简单的键值存储系统,用于存储和检索整数键对应的任意数据。这个系统需要支持高效的插入、查找和删除操作,并且需要保证有序性。跳跃表非常适合这种场景。

数据结构定义

首先,我们需要定义跳跃表中的节点结构和跳跃表本身的数据结构。

节点结构

struct SkipListNode {int key;               // 存储的键值void* value;           // 关联的值std::vector<SkipListNode*> forward; // 向前指针数组SkipListNode(int k, void* val) : key(k), value(val), forward() {}
};

跳跃表结构

class SkipList {
public:SkipList() : head(new SkipListNode(std::numeric_limits<int>::min(), nullptr)), numLevels(1) {}~SkipList() {SkipListNode* node = head;while (node != nullptr) {SkipListNode* next = node->forward[0];delete node;node = next;}}void insert(int key, void* value);bool find(int key, void*& value);bool remove(int key);private:int randomLevel();void updatePointers(std::vector<SkipListNode*>& update, int level, SkipListNode* node);SkipListNode* head;     // 头部节点int numLevels;          // 当前跳跃表的高度
};

实现插入功能

插入功能需要确定新节点的高度,并在适当的位置插入节点。

void SkipList::insert(int key, void* value) {int newNodeHeight = randomLevel();// 如果新节点的高度大于当前跳跃表的高度,则扩展跳跃表的高度if (newNodeHeight > numLevels) {numLevels = newNodeHeight;}std::vector<SkipListNode*> update(numLevels, nullptr); // 更新指针数组SkipListNode* current = head;// 寻找插入位置for (int i = numLevels - 1; i >= 0; i--) {while (current->forward[i] != nullptr && current->forward[i]->key < key) {current = current->forward[i];}update[i] = current;}// 如果找到了相同的键值,则更新其值if (current->forward[0] != nullptr && current->forward[0]->key == key) {current->forward[0]->value = value;return;}// 创建新节点并插入SkipListNode* newNode = new SkipListNode(key, value);updatePointers(update, newNodeHeight, newNode);
}

实现查找功能

查找功能需要从跳跃表的最高层开始查找指定的键值。

bool SkipList::find(int key, void*& value) {SkipListNode* current = head;for (int i = numLevels - 1; i >= 0; i--) {while (current->forward[i] != nullptr && current->forward[i]->key < key) {current = current->forward[i];}}current = current->forward[0];if (current != nullptr && current->key == key) {value = current->value;return true;}return false;
}

实现删除功能

删除功能需要找到指定的键值,并更新相应的指针。

bool SkipList::remove(int key) {std::vector<SkipListNode*> update(numLevels, nullptr);SkipListNode* current = head;// 寻找要删除的节点for (int i = numLevels - 1; i >= 0; i--) {while (current->forward[i] != nullptr && current->forward[i]->key < key) {current = current->forward[i];}update[i] = current;}current = current->forward[0];if (current != nullptr && current->key == key) {// 更新指针for (int i = 0; i < numLevels; i++) {if (current->forward[i] != nullptr) {update[i]->forward[i] = current->forward[i];} else {update[i]->forward[i] = nullptr;}}delete current;// 调整跳跃表的高度while (numLevels > 1 && head->forward[numLevels - 1] == nullptr) {numLevels--;}return true;}return false;
}int SkipList::randomLevel() {int level = 1;while (rand() % 2 && level < MAX_LEVELS) { // MAX_LEVELS 为最大高度限制level++;}return level;
}void SkipList::updatePointers(std::vector<SkipListNode*>& update, int level, SkipListNode* node) {for (int i = 0; i < level; i++) {node->forward[i] = update[i]->forward[i];update[i]->forward[i] = node;}
}

测试跳跃表的功能

现在我们有了基本的插入、查找和删除功能,可以编写一个测试函数来验证这些功能。

#include <iostream>
#include <cstdlib>void testSkipList() {srand(time(nullptr)); // 初始化随机种子SkipList sl;sl.insert(10, "data1");sl.insert(20, "data2");sl.insert(30, "data3");void* data;if (sl.find(20, data)) {std::cout << "Found key 20 with value: " << data << std::endl;}sl.remove(20);if (!sl.find(20, data)) {std::cout << "Key 20 was successfully removed." << std::endl;}
}int main() {testSkipList();return 0;
}

这个例子展示了如何使用跳跃表进行基本的操作,并且提供了一个简单的测试用例来验证跳跃表的功能。你可以根据实际需求进一步扩展和完善这个跳跃表实现。

我们继续完善跳跃表的实现,并加入更多实用的功能和测试用例。

完善跳跃表的功能

接下来,我们将在现有的基础上添加一些辅助函数和增强功能,以便更好地管理和测试跳跃表。

辅助函数

  1. 打印跳跃表的状态:这个函数可以帮助我们在调试时查看跳跃表的当前状态。

  2. 获取跳跃表的大小:这个函数可以返回跳跃表中元素的数量。

  3. 清理跳跃表:这个函数可以清除跳跃表中的所有元素。

打印跳跃表的状态

void printList() const {std::cout << "Current levels: " << numLevels << std::endl;for (int i = numLevels - 1; i >= 0; i--) {SkipListNode* current = head;std::cout << "Level " << i << ": ";while (current->forward[i] != nullptr) {std::cout << "(" << current->forward[i]->key << ", " << current->forward[i]->value << ") ";current = current->forward[i];}std::cout << std::endl;}
}

获取跳跃表的大小

int getSize() const {int count = 0;SkipListNode* current = head;while (current->forward[0] != nullptr) {current = current->forward[0];count++;}return count;
}

清理跳跃表

void clear() {SkipListNode* node = head;while (node != nullptr) {SkipListNode* next = node->forward[0];delete node;node = next;}head = new SkipListNode(std::numeric_limits<int>::min(), nullptr);numLevels = 1;
}

综合测试

现在我们已经有了更多的辅助函数,可以编写一个更全面的测试用例来验证跳跃表的功能。

#include <iostream>
#include <cstdlib>
#include <ctime>void testSkipList() {srand(time(nullptr)); // 初始化随机种子SkipList sl;// 插入一些元素sl.insert(10, "data1");sl.insert(20, "data2");sl.insert(30, "data3");sl.insert(5, "data4");sl.insert(15, "data5");sl.insert(25, "data6");sl.insert(35, "data7");// 打印跳跃表的状态std::cout << "After insertion:" << std::endl;sl.printList();// 查找元素void* data;if (sl.find(20, data)) {std::cout << "Found key 20 with value: " << data << std::endl;}// 删除元素sl.remove(20);std::cout << "After removing key 20:" << std::endl;sl.printList();// 再次查找已删除的元素if (!sl.find(20, data)) {std::cout << "Key 20 was successfully removed." << std::endl;}// 验证跳跃表的大小std::cout << "Size of the skip list: " << sl.getSize() << std::endl;// 清理跳跃表sl.clear();std::cout << "After clearing the skip list:" << std::endl;sl.printList();
}int main() {testSkipList();return 0;
}

运行测试

这个测试用例包含了插入、查找、删除、打印状态、获取大小和清理功能。运行上述代码,可以看到跳跃表的各个功能是否正常工作。

总结

通过上述实现和测试,我们已经完成了跳跃表的基本功能,并且通过一系列的测试用例验证了其正确性和实用性。跳跃表作为一种高效的数据结构,适用于需要快速查找、插入和删除操作的应用场景。在实际应用中,还可以根据具体需求进一步优化和扩展跳跃表的功能。

😍😍 海量H5小游戏、微信小游戏、Web casualgame源码😍😍
😍😍试玩地址: https://www.bojiogame.sg😍😍
😍看上哪一款,需要源码的csdn私信我😍

————————————————

​最后我们放松一下眼睛
在这里插入图片描述


http://www.mrgr.cn/news/52964.html

相关文章:

  • 掌控板读取板载光线传感器数值
  • kubernetes安装web界面
  • MFC中多线程进度条的简单代码实现
  • 中英互译大比拼,这5款工具随心选!
  • 上海桶饭配送中腾食品:资源整合与一站式服务典范
  • 四步向gem5中添加用户自定义的分支预测器
  • vue综合指南(六)
  • springboot033小徐影城管理系统(论文+源码)_kaic
  • 复现EfficientNet
  • 平台上新 | 智能分析——你的智能体调优工具已上线!
  • 倍思、公牛、西圣充电宝好用吗?测评PK 谁是性价比之王!
  • 我与C语言二周目邂逅vlog——7.预处理
  • Java项目-基于SpringBoot框架的学生考勤管理系统项目实战(附源码+文档)
  • Nuxt.js 应用中的 app:templates 事件钩子详解
  • 使用.NET MAUI开发第一个安卓APP
  • rust入门基础笔记(最全详细的笔记)
  • 在MySQL中为啥引入批量键访问(Batch Key Access, BKA)
  • 大模型缺的脑子,终于在智能体上长好了
  • 车企放弃自研?高阶智驾「火拼」
  • matlab相位图