目 录
跳表
跳表使用空间换时间的设计思路,通过构建多级索引来提高查询的效率,实现了基于链表的“二分查找”。跳表是一种动态数据结构,支持快速的插入、删除、查找操作,时间复杂度都是 O(logn)。跳表的空间复杂度是 O(n)。不过,跳表的实现非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
1、背景思路
skiplist受到多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。
2、实现思想
skiplist正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到O(log n)。但是,这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的2:1的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成O(n)。删除数据也有同样的问题。
skiplist为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。如下图可以看出。
从上面skiplist的创建和插入过程可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度。实际上,这是skiplist的一个很重要的特性。这让它在插入性能上明显优于平衡树的方案。
通过随机函数来决定数据出现的层数。其中第0层是所有元素都在的一个有序链表。随机函数如下:
bool randNum() {
int rand_data = rand();
if(1 == (rand_data % 2))
return true;
return false;
}
根据上图中的skiplist结构,我们很容易理解这种数据结构的名字的由来。skiplist,翻译成中文,可以翻译成“跳表”或“跳跃表”,指的就是除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针故意跳过了一些节点(而且越高层的链表跳过的节点越多)。这就使得我们在查找数据的时候能够先在高层的链表中进行查找,然后逐层降低,最终降到第1层链表来精确地确定数据位置。在这个过程中,我们跳过了一些节点,从而也就加快了查找速度。
查找23;
分析
但是,如果你是第一次接触skiplist,那么一定会产生一个疑问:节点插入时随机出一个层数,仅仅依靠这样一个简单的随机数操作而构建出来的多层链表结构,能保证它有一个良好的查找性能吗?为了回答这个疑问,我们需要分析skiplist的统计性能。
3、时间复杂度
在单链表中,一旦定好要插入的位置,时间复杂度是很低的为O(1),但是,为了保持原始链表中数据的有序性,通常需要遍历链表找到需要插入的位置,这个查找操作会比较耗时。但是在跳表中,时间复杂度为O(logn),如下图,插入数据 6;在删除操作时,如果删除的节点在索引中也有出现,除了要删除原始链表中的数据外,还要删除索引中的节点
主频是2.4GHz查找几乎可以在单个 clock tick 的时间=2.4×1091=24000000001秒=0.4167×10−9 秒=0.4167 纳秒完成
跳表索引动态更新
当我们不停地往跳表中插入数据时,如果我们不更新索引,就有可能出现某 2 个索引结点之间数据非常多的情况。极端情况下,跳表还会退化成单链表。
作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。
而跳表是通过随机函数来维护前面提到的“平衡性”。当我们往跳表中插入数据的时候,我们可以选择同时将这个数据插入到部分索引层中。如何选择加入哪些索引层呢?我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。
4、随机的重要特性
skiplist为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是为每个节点随机出一个层数(level)。比如,一个节点随机出的层数是3,那么就把它链入到第1层到第3层这三层链表中。每一个节点的层数(level)是随机出来的,而且新插入一个节点不会影响其它节点的层数。因此,插入操作只需要修改插入节点前后的指针,而不需要对很多节点都进行调整。这就降低了插入操作的复杂度。实际上,这是skiplist的一个很重要的特性。
5、意义
skiplist,翻译成中文,可以翻译成“跳表”或“跳跃表”,指的就是除了最下面第1层链表之外,它会产生若干层稀疏的链表,这些链表里面的指针故意跳过了一些节点(而且越高层的链表跳过的节点越多)。这就使得我们在查找数据的时候能够先在高层的链表中进行查找,然后逐层降低,最终降到第1层链表来精确地确定数据位置。在这个过程中,我们跳过了一些节点,从而也就加快了查找速度。
相关参考网址
http://zhangtielei.com/posts/blog-redis-skiplist.html
https://www.cnblogs.com/NoneID/p/15558088.html
哈希表
1、基本概念
要说哈希表,我们必须先了解一种新的存储方式—散列技术。
散列技术是指在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使每一个关键字都对应一个存储位置。即:存储位置=f(关键字)。这样,在查找的过程中,只需要通过这个对应关系f 找到给定值key的映射f(key)。只要集合中存在关键字和key相等的记录,则必在存储位置f(key)处。我们把这种对应关系f 称为散列函数或哈希函数。
按照这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为哈希表。所得的存储地址称为哈希地址或散列地址。
2、实现方法
哈希表其实也叫散列表
哈希表本质是一个数组
实现哈希表可以采用的两种方法:
-
数组+链表
-
数组+二叉树
3、散列函数
那就是取姓名的首字母做一个排序,那么这是不是就是通过一些特定的方法去得到一个特定的值,比如这里取人名的首字母,那么如果是放到数学中,是不是就是类似一个函数似的,给你一个值,经过某些加工得到另外一个值,就像这里的给你个人名,经过些许加工我们拿到首字母,那么这个函数或者是这个方法在哈希表中就叫做散列函数,其中规定的一些操作就叫做函数法则。
4、关键值key
哈希表就是通过将关键值也就是key通过一个散列函数加工处理之后得到一个值,这个值就是数据存放的位置,我们就可以根据这个值快速的找到我们想要的数据。
在哈希表中,哈希函数的设计很重要,一个好的哈希函数可以极大的提升性能,而且如果你的哈希函数设计的比较简单粗陋,那很容易被那些不怀好意的人捣乱,比如知道了你哈希函数的规则,故意制造容易冲突的key值,那就有意思了,你的哈希表就会一直撞啊。
例子:
跳表和哈希表的异同
**相似之处:**
1. **快速操作**:两者都支持快速的搜索、插入和删除操作。
2. **动态数据结构**:都可以动态地随着数据的插入和删除而调整大小。
**不同之处:**
1. **数据组织方式**:
- 跳表通过多层链表组织数据,保持了元素的有序性,便于进行范围查询和中序遍历。
- 哈希表通过哈希函数将键映射到数组索引,不保持元素的有序性,不适合范围查询。
2. **时间复杂度**:
- 跳表的最坏时间复杂度为O(n),因为所有元素可能位于同一层。
- 哈希表的平均时间复杂度为O(1),但最坏情况下为O(n),这取决于哈希函数的质量和冲突解决机制。
3. **有序性**:
- 跳表天然支持有序数据操作,可以轻松实现元素的有序遍历。
- 哈希表不支持有序操作,因为它不保证元素的存储顺序。
4. **实现复杂度**:
- 跳表的实现相对简单,但需要处理多层链表的维护。
- 哈希表的实现较为复杂,需要设计高效的哈希函数和冲突解决策略。
5. **空间效率**:
- 跳表由于多层链表的结构,空间效率相对较低。
- 哈希表的空间效率较高,因为它只需要一个数组和一些额外的冲突解决结构。
6. **适用场景**:
- 跳表适用于需要有序数据和范围查询的场景。
- 哈希表适用于需要快速查找且不关心数据有序性的场景。
总的来说,跳表和哈希表各有优势和适用场景。选择哪种数据结构取决于具体的应用需求,如是否需要有序性、是否需要快速范围查询、对时间复杂度的要求等。
跳表(Skip List)和哈希表(Hash Table)都是用于快速查找的数据结构,但它们的实现和性能特性有所不同。下面是一个表格,总结了跳表和哈希表的理论基础以及它们的异同:
特性 | 跳表(Skip List) | 哈希表(Hash Table) |
理论基础 | 基于有序链表,通过多层索引提高查找效率。 | 基于哈希函数,通过哈希值直接定位数据。 |
时间复杂度 | 平均情况下为 O(log n),最坏情况下为 O(n)。 | 平均情况下为 O(1),最坏情况下为 O(n)(当哈希冲突严重时)。 |
空间复杂度 | O(n) | O(n) |
有序性 | 支持有序操作,如范围查询。 | 不支持有序操作,数据无序。 |
动态性 | 动态,可以随时插入和删除元素。 | 动态,可以随时插入和删除元素,但可能需要重新哈希。 |
实现复杂度 | 较高,需要维护多层索引。 | 较低,实现相对简单。 |
冲突处理 | 无冲突,每个元素根据概率分布在不同的层。 | 有冲突,需要通过链表,开放寻址等方法解决冲突。 |
适用场景 | 适合需要有序数据结构的场景,如范围查询。 | 适合需要快速查找的场景,尤其是无序数据。 |
时间复杂度比较