跳表

由二分查找引出跳表

二分查找:二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0。

二分查找底层依赖数据结构:数组——即可以按照下标随机访问元素的数据结构

二分查找的时间复杂度:O(logn)

思考:一个有序的链表如何快速查找到需要的数据?

什么是跳表

对于单链表来说,即便链表中的数据是有序的,如果我们想要在其中查找某个数据,那么我们也只能从头到尾遍历链表,这样时间复杂度就会为O(n)。

image
那么如何提高我们的查找效率?有一种思路——我们是不是可以模拟数组的样子建立索引?什么意思呢?看图。
在这里插入图片描述
注意:其中down表示down指针,指向下一层。

这样我们对链表建立一级索引,现在假设我们需要查找10的节点的时候,我们在索引层先遍历到9的时候,由于索引层9的下一个数是11,比我们要查找的10大,这时候我们通过down指针下降到原始链表这一层,然后继续遍历。这个时候,我们只需要再遍历两个节点就可以了。那么我们如果没有一级索引的时候,我们需要遍历10个节点才能查找到我们的目标值,加上索引之后,只需要遍历7个节点

从这个例子中,我们可以看出,加了一层索引之后,查找一个节点需要遍历的节点数减少了,说明我们的查找效率提高了。那么如果我们再加一层索引,查找的次数会不会进一步减少呢?答案是肯定的,我们再看一张图。
在这里插入图片描述
这次我们找到10的数据变为了6次,所以效率又一次提高了一点。
由于我讲的例子数据比较少,提高的效率不是很明显。那么我们再画一个比较明显的。64个数据,5级索引。
在这里插入图片描述
我们需要找到第62个节点,只需要11次遍历即可。所以当链表的长度比较大的时候,通过建立索引能够很明显的提高查找效率。像这样,我们把具有索引层级的链表称为跳表

跳表的查找效率的分析

查找效率我们可以用查找的时间复杂度来衡量。我们知道在一个单链表中,我们查找某个数据的时间复杂度是O(n)。我们接下来就分析跳表的查询的时间时间复杂度。
首先有n个节点的链表,我们需要建立多少层级的索引?

按照我们上面的分析,假设每两个节点都会抽出一个节点作为上一级的索引的节点,那么第一级索引就是n/2个节点,第二级索引的个数就是n/4,依次类推,第k级索引的节点数是第k-1层索引的一半。那么第k级索引的个数就是n/(2k)。
假设索引有h级,最高的索引只有2个节点,我们可以得到n/(2h) = 1。得出h = log2n-1,如果包含链表这一层那么整一个跳表的高度就是 log2n。我们再跳表中查询某个数据的时候,如果每一层都要遍历m个节点,那么跳表中查询一个数据的时间复杂度就是O( m*logn)

假设我们需要查找的数据是x,在第k级索引中,我们遍历到y节点之后,发现x大于y,小于y后面的节点z,所以这时候我们通过y的down指针,从第k+1级索引下降到第k级索引。在第k层索引中y到z之间包含3个节点(包括y和z),所以在第k-1级最多遍历3个节点,依次类推,每一级索引最多只需要遍历3个节点。 所以跳表查询的时间复杂度为O( 3*logn)。由于时间复杂度一般忽略系数的影响,所以最终得到时间复杂度为O( logn)。结合图再来看一下
在这里插入图片描述

跳表的空间复杂度分析

看了以上的分析之后,我们可以分析跳表的空间复杂度,和上面一样第一级的索引数为n/2。依次类推第二,第三级索引数为n/4,n/8…这是一个等比数列,最终算得一共需要n/2+n/4+n/8+…+2 = n-2。所以跳表的空间复杂度为O(n),也就是说,如果将包含n个结点的单链表构建成一个跳表,我们需要额外再用接近n个结点的存储空间空间。那么我们有没有方法降低索引占用的内存空间呢?
我们前面都是每两个结点抽取一个结点,那现在我们3个结点或者5个结点再抽取一个节点的话,索引占据的空间是不是会降低。如果是每3个结点抽取一个节点那么最终的所需要的创建的节点数为n/2,这就大概减少了一半的存储空间。
实际上,这只是链表存储对象为整数的时候显得很浪费空间,如果链表对象很大,我们在索引中只需要存储比较的关键字和一些指针,不需要存储整个对象,所以如果对象很大的时候,那么索引的空间就可以忽略了。

高效的动态操作

实际上跳表不仅支持查找操作,它还支持动态的插入和删除操作,时间复杂度也是O( logn)
下面我们来看一下它是如何做到插入的时间复杂度为O( logn) 的。我们知道链表在某个节点后插入一个结点,时间复杂度为O(1),也就是说如果我们找到需要插入那个位置的前一个结点,那么插入的操作就是O(1),也就是说找到节点的时间复杂度就是整个插入过程的时间复杂度。前面我们也知道查找的时间复杂度为O( logn),所以插入的时间复杂度为O( logn)。删除操作同理。

跳表的索引的动态更新

当我们不停的往跳表中不停的插入数据的时候,如果我们不更新缩影,那么就会可能出现2个索引之间出现非常多结点的情况。极端的情况下,跳表会退化成单链表。如图:
在这里插入图片描述
作为一种动态数据结构,我们需要某种手段来维护索引与原始链表大小之间的平衡,也就是说,如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,以及查找、插入、删除操作性能下降。也就是说我们往跳表中插入数据的时候,我们可以选择可以选择同时将这个数据插入到部分索引层中。
那么如何选择加入哪些索引层呢?可以通过一个随机函数来决定这个结点插入到哪一级索引中,比如随机函数生成了值K,那么我们就将这个结点添加到第一级到底K级索引中。

如何实现跳表
public class SkipList {

  private static final float SKIPLIST_P = 0.5f;
  private static final int MAX_LEVEL = 16;

  private int levelCount = 1;//有多少级索引

  private Node head = new Node();  // 带头链表

  public Node find(int value) {
    Node p = head;
	//从最高级索引开始,先判断这个索引的下一个索引的值是不是比要查询的大,如果是,那么下降到下一级索引
    for (int i = levelCount - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
    }
	
    if (p.forwards[0] != null && p.forwards[0].data == value) {
      return p.forwards[0];
    } else {
      return null;
    }
  }

  public void insert(int value) {
    int level = randomLevel();
    Node newNode = new Node();
    newNode.data = value;
    newNode.maxLevel = level;
    Node update[] = new Node[level];//所有层级,存储newNode插入的前一个节点
    for (int i = 0; i < level; ++i) {
      update[i] = head;
    }

    // record every level largest value which smaller than insert value in update[]
    Node p = head;
    for (int i = level - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
      update[i] = p;// use update save node in search path
    }

    // in search path node next node become new node forwords(next)
    //更新索引
	for (int i = 0; i < level; ++i) {
      newNode.forwards[i] = update[i].forwards[i];
      update[i].forwards[i] = newNode;
    }

    // update node hight
    if (levelCount < level) levelCount = level;
  }

  public void delete(int value) {
    Node[] update = new Node[levelCount];
    Node p = head;
    for (int i = levelCount - 1; i >= 0; --i) {
      while (p.forwards[i] != null && p.forwards[i].data < value) {
        p = p.forwards[i];
      }
      update[i] = p;
    }

    if (p.forwards[0] != null && p.forwards[0].data == value) {
      for (int i = levelCount - 1; i >= 0; --i) {
        if (update[i].forwards[i] != null && update[i].forwards[i].data == value) {
          update[i].forwards[i] = update[i].forwards[i].forwards[i];
        }
      }
    }

    while (levelCount>1&&head.forwards[levelCount]==null){
      levelCount--;
    }

  }

   /**
	*理论来讲,一级索引中元素个数应该占原始数据的 50%,二级索引中元素个数占 25%,三级索引12.5% ,一直到最顶层。
	*因为这里每一层的晋升概率是 50%。对于每一个新插入的节点,都需要调用 randomLevel 生成一个合理的层数。
	*该 randomLevel 方法会随机生成 1~MAX_LEVEL 之间的数,且:
    *      50%的概率返回 1
    *      25%的概率返回 2
    *    12.5%的概率返回 3 ...
	*/
  private int randomLevel() {
    int level = 1;

    while (Math.random() < SKIPLIST_P && level < MAX_LEVEL)
      level += 1;
    return level;
  }

  public void printAll() {
    Node p = head;
    while (p.forwards[0] != null) {
      System.out.print(p.forwards[0] + " ");
      p = p.forwards[0];
    }
    System.out.println();
  }

  
  public class Node {
    private int data = -1;
    private Node forwards[] = new Node[MAX_LEVEL];//一个结点拥有的索引,其中最后高级的forwards[maxLevel]为这一级索引的下一个索引,即类似next指针			
    private int maxLevel = 0;

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      builder.append("{ data: ");
      builder.append(data);
      builder.append("; levels: ");
      builder.append(maxLevel);
      builder.append(" }");

      return builder.toString();
    }
  }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言中,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言中常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言中常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言中用于封装代码的单元,可以实现代码的复用和模块化。C语言中定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言中用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言中定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言中用于存储同类型数据的结构,可以通过索引访问和修改数组中的元素。字符串是C语言中用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言中用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言中通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习和实践打下坚实的基础。
在 PostgreSQL 数据库中,并没有内置的跳表(Skip List)索引实现。PostgreSQL 提供了多种索引类型,如B树索引、哈希索引、GiST索引和GIN索引等,但没有直接支持跳表的索引类型。 B树索引是 PostgreSQL 中最常用的索引类型之一。它适用于范围查询和等值查询,并且可以保持数据有序性。B树索引在处理数据块的平衡性和查询效率方面具有很好的性能。 除了B树索引之外,PostgreSQL 还提供了其他类型的索引用于特定的场景。例如,哈希索引适用于等值查询,可以提供快速的哈希查找;GiST 索引(通用搜索树)和 GIN 索引(通用倒排索引)适用于全文搜索和复杂的匹配查询。 虽然 PostgreSQL 不提供内置的跳表索引实现,但是你可以使用扩展或自定义索引实现跳表的功能。通过编写自定义插件或使用第三方扩展,你可以在 PostgreSQL 中实现跳表索引。这需要一定的开发工作,并且需要充分测试和评估性能。 需要注意的是,自定义实现的跳表索引可能会受到 PostgreSQL 内核版本更新的影响,并且可能无法享受到 PostgreSQL 内置索引的一些优化和支持。 总之,PostgreSQL 并没有内置的跳表索引实现,但提供了其他类型的索引,如B树索引、哈希索引、GiST索引和GIN索引等,用于满足不同的查询需求。如果需要使用跳表索引,你可以考虑自定义实现或使用第三方扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值