skip list

list的操作:

查找,插入,删除。

 

有序链表

link list

 

上面链表是有序排列的,那么如果想查找某一个元素x,则可以如下操作

 

 

 

如此以来,得到了p之前的元素均小于x,而p之后的元素均不小于x,p是不小于x的第一个元素节点指针.

 

如果要按照顺序插入一个元素x,则可以如下操作

 

 

删除操作,删除不小于x的第一个元素

 

 

以上是顺序链表,而skip list能够在这三个操作上节省时间。将时间由原来的O(n)节省到O(log(n))。

1.如何构建一个skip list?

按照顺序排列的链表作为底层;

将链表中随机抽取一些元素,作为第二层,而第二层的每一个元素均有向下的一个指针,指向第一层中与其相同的元素;

而后再在第二层元素中,随机选取一部分元素,作为第三层,而第三层中的元素也均有向下的指针指向第二层与其相同的元素;以此类推,

直到构建了一层只有最大值最小值(-1,1)指针的元素。如此构建一个skip list。

 

skip list有以下特征:

最下面得一层具有所有元素;

上面每一层的元素均是来自其下面一层的元素;

每层元素均有向下的指针与其下一层相同的元素相关联;

有一个top指针指向最高一层的第一个元素。

 

在skip list中,要实现查找、插入、删除等操作时,只需要操作O(log(n))次。

 

此时的操作方式分别为:

 

查找元素x

 

 

 

插入元素,假设要在第k层插入元素x,k的值不同也会有不同:

 

Determine k the number of levels in which x participates (explained later)
Do find(x), and insert x to the appropriate places in the lowest k levels. (after the elements at which the search path turns down or terminates)
Example - inserting 119. k =2

 

 

 

If k is larger than the current number of levels, add new levels (and update top)
Example - inser (119) when k=4
详见SkipList的PPT,是Arizona的一个guy写的,感觉很NICE,很形象。可惜很多图我无法粘贴。不知道为何。
下面是来自另外一个网站的资料。
目的:向跳跃表中插入一个元素x 
 首先明确,向跳跃表中插入一个元素,相当于在表中插入一列从S0中某一位置出发向上的连续一段元素。有两个参数需要确定,即插入列的位置以及它的“高度”。 
 关于插入的位置,我们先利用跳跃表的查找功能,找到比x小的最大的数y。根据跳跃表中所有链均是递增序列的原则,x必然就插在y的后面。 
 而插入列的“高度”较前者来说显得更加重要,也更加难以确定。由于它的不确定性,使得不同的决策可能会导致截然不同的算法效率。为了使插入数据之后,保持该数据结构进行各种操作均为O(logn)复杂度的性质,我们引入随机化算法(Randomized Algorithms)。 
我们定义一个随机决策模块,它的大致内容如下: 
产生一个0到1的随机数r                            r ← random() 
如果r小于一个常数p,则执行方案A,  if  r<p then do A 
 否则,执行方案B                                     else do B 
初始时列高为1。插入元素时,不停地执行随机决策模块。如果要求执行的是A操作,则将列的高度加1,并且继续反复执行随机决策模块。直到第i次,模块要求执行的是B操作,我们结束决策,并向跳跃表中插入一个高度为i的列。 
性质1: 根据上述决策方法,该列的高度大于等于k的概率为p^(k-1)。 
此处有一个地方需要注意,如果得到的i比当前跳跃表的高度h还要大的话,则需要增加新的链,使得跳跃表仍满足先前所提到的条件。 
三、删除 
 目的:从跳跃表中删除一个元素x 
 删除操作分为以下三个步骤: 
(1) 在跳跃表中查找到这个元素的位置,如果未找到,则退出  * 
(2) 将该元素所在整列从表中删除        * 
(3) 将多余的“空链”删除         * 
【复杂度分析】 
一个数据结构的好坏大部分取决于它自身的空间复杂度以及基于它一系列操作的时间复杂度。跳跃表之所以被誉为几乎能够代替平衡树,其复杂度方面自然不会落后。我们来看一下跳跃表的相关复杂度: 
空间复杂度: O(n)    (期望) 
跳跃表高度: O(logn)  (期望) 
相关操作的时间复杂度: 
 查找:  O(logn)  (期望) 
 插入:   O(logn)  (期望) 
 删除:  O(logn)  (期望) 
  
之所以在每一项后面都加一个“期望”,是因为跳跃表的复杂度分析是基于概率论的。有可能会产生最坏情况,不过这种概率极其微小。 

一、 空间复杂度分析  O(n) 
假设一共有n个元素。根据性质1,每个元素插入到第i层(Si)的概率为pi-1 ,则在第i层插入的期望元素个数为npi-1,跳跃表的元素期望个数为  ,当p取小于0.5的数时,次数总和小于2n。 
所以总的空间复杂度为O(n) 
二、跳跃表高度分析  O(logn) 
根据性质1,每个元素插入到第i层(Si)的概率为p^i ,则在第i层插入的期望元素个数为np^(i-1)。 
考虑一个特殊的层:第1+ 层。 
 层的元素期望个数为 np^0+np^1+...+np^(h-1),当n取较大数时,这个式子的值接近0,故跳跃表的高度为O(logn)级别的。 
三、查找的时间复杂度分析  O(logn) 
我们采用逆向分析的方法。假设我们现在在目标节点,想要走到跳跃表最左上方的开始节点。这条路径的长度,即可理解为查找的时间复杂度。 
设当前在第i层第j列那个节点上。 
i) 如果第j列恰好只有i层(对应插入这个元素时第i次调用随机化模块时所产生的B决策,概率为1-p),则当前这个位置必然是从左方的某个节点向右跳过来的。 
ii) 如果第j列的层数大于i(对应插入这个元素时第i次调用随机化模块时所产生的A决策,概率为p),则当前这个位置必然是从上方跳下来的。(不可能从左方来,否则在以前就已经跳到当前节点上方的节点了,不会跳到当前节点左方的节点) 
设C(k)为向上跳k层的期望步数(包括横向跳跃) 
有: 
C(0) = 0 
C(k) = (1-p)(1+向左跳跃之后的步数)+p(1+向上跳跃之后的步数) 
     = (1-p)(1+C(k)) + p(1+C(k-1)) 
C(k) = 1/p + C(k-1) 
C(k) = k/p 
 而跳跃表的高度又是logn级别的,故查找的复杂度也为logn级别。 
对于记忆化查找(Search with fingers)技术我们可以采用类似的方法分析,很容易得出它的复杂度是O(logk)的(其中k为此次与前一次两个被查找元素在跳跃表中位置的距离)。 
四、插入与删除的时间复杂度分析  O(logn) 
 插入和删除都由查找和更新两部分构成。查找的时间复杂度为O(logn),更新部分的复杂度又与跳跃表的高度成正比,即也为O(logn)。 
 所以,插入和删除操作的时间复杂度都为O(logn) 
最后,概率因子一般用1/2或1/e 

 

### 跳表(Skiplist数据结构概述 跳表是一种基于概率的有序链表扩展的数据结构,它通过多层索引来加速查询操作。每一层都是一个有序链表,上一层是下一层的一个子集。这种设计使得跳表能够在平均 \(O(\log n)\) 的时间复杂度内完成插入、删除和查找操作。 #### 跳表的核心特性 - **层次化结构**:每层是一个独立的有序链表,最底层包含所有节点,而高层逐渐减少节点数量。 - **随机性**:新节点被提升到更高层数的概率通常设置为固定值(如 0.5),这决定了跳表的高度以及性能表现[^3]。 以下是跳表的一种典型实现方式: ```c #include <stdio.h> #include <stdlib.h> #include <time.h> #define MAX_LEVEL 16 typedef struct Node { int value; struct Node **forward; // 指向下一节点的指针数组 } Node; typedef struct SkipList { int level; // 当前最大层数 int size; // 节点总数 Node *header; // 头结点 float p; // 提升概率 } SkipList; // 创建一个新的节点 Node* create_node(int value, int level) { Node *node = (Node *)malloc(sizeof(Node)); node->value = value; node->forward = (Node **)malloc((level + 1) * sizeof(Node *)); for (int i = 0; i <= level; ++i) { node->forward[i] = NULL; } return node; } // 初始化跳表 SkipList* init_skiplist(float probability) { SkipList *list = (SkipList *)malloc(sizeof(SkipList)); list->level = 0; list->size = 0; list->p = probability; list->header = create_node(-1, MAX_LEVEL); return list; } // 随机生成节点的最大层数 int random_level(float p) { int lvl = 0; while ((rand() / (float)RAND_MAX) < p && lvl < MAX_LEVEL) { lvl++; } return lvl; } // 查找指定值的位置 Node* search_value(SkipList *list, int value) { Node *current = list->header; for (int i = list->level; i >= 0; --i) { while (current->forward[i] != NULL && current->forward[i]->value < value) { current = current->forward[i]; } } current = current->forward[0]; // 移动到最后可能匹配的节点 if (current != NULL && current->value == value) { return current; } else { return NULL; } } ``` 上述代码展示了如何初始化跳表并执行基本的查找操作。其中 `random_level` 函数用于决定新节点可以达到的最大层数,从而控制跳表的整体高度[^4]。 --- 关于提到的错误码 **15445** 并未在已知的标准数据库或编程环境中找到具体定义。如果该错误码与特定环境相关,则需进一步确认其上下文含义。然而,在 SQL 或其他存储系统中,类似的错误码通常表示权限不足或者资源不可访问等问题[^1]。 对于跳表而言,常见的错误场景包括但不限于: - 插入失败:由于内存分配问题或其他异常情况导致无法创建新的节点。 - 删除失败:目标节点不存在于跳表中。 - 查询超时:当跳表过大且缺乏优化时可能出现性能瓶颈。 这些潜在问题可以通过严格的边界条件检测以及合理的参数配置来规避。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值