数据结构——跳表的原理与代码应用

简介: 数据结构——跳表的原理与代码应用

一、为什么选择跳表?


目前经常使用的平衡数据结构有:B树,红黑树,AVL树,Splay Tree, Treep等。想象一下,给你一张草稿纸,一只笔,一个编辑器,你能立即实现一颗红黑树,或者AVL树出来吗?


很难吧,这需要时间,要考虑很多细节,要参考一堆算法与数据结构之类的树,还要参考网上的代码,相当麻烦。


用跳表吧,跳表是一种随机化的数据结构,目前开源软件 Redis 和 LevelDB 都有用到它,它的效率和红黑树以及 AVL 树不相上=下,但跳表的原理相当简单,只要你能熟练操作链表,就能轻松实现一个 SkipList。


二、定义


如果你要在一个有序的序列中查找元素 k ,相信大多数人第一反应都是二分查找。


如果你需要维护一个支持插入操作的有序表,大家又会想到链表。


简单的说,要达到以logn的速度查找链表中的元素


我们先来看看这张图:



如果要在这里面找 21 ,过程为 3→ 6 → 7 → 9 → 12 → 17 → 19 → 21 。


我们考虑从中抽出一些节点,建立一层索引作用的链表:



跳表的主要思想就是这样逐渐建立索引,加速查找与插入。


一般来说,如果要做到严格 O(logn) ,上层结点个数应是下层结点个数的 1/2 。但是这样实现会把代码变得十分复杂,就失去了它在 OI 中使用的意义。


此外,我们在实现时,一般在插入时就确定数值的层数,而且层数不能简单的用随机数,而是以1/2的概率增加层数。


用实验中丢硬币的次数 K 作为元素占有的层数。显然随机变量 K 满足参数为 p = 1/2 的几何分布,K 的期望值 E[K] = 1/p = 2. 就是说,各个元素的层数,期望值是 2 层。


同时,为了防止出现极端情况,设计一个最大层数MAX_LEVEL。如果使用非指针版,定义这样一个常量会方便许多,更能节省空间。如果是指针版,可以不加限制地任由它增长。


inline int rand_level()
{
   int ret = 1;
   while (rand() % 2 && ret <= MAX_LEVEL)
      ++ret;
   return ret;
}


我们来看看存储结点的结构体:


struct node
 {
     int key;
     int next[MAX_LEVEL + 1];
 } sl[maxn + 10];


next[i] 表示这个结点在第 i 层的下一个结点编号。


分配新结点


为了充分地利用空间,就是用一个栈或是队列保存已经被删除的节点,模拟一个内存池,记录可以使用的内存单元。


可以节省很多空间,使空间在 O(n * MAX_LEVEL) 级


inline void new_node(int &p, int key)
{
    if (top)
        p = st[top--];
    else
        p = ++node_tot;
    sl[p].key = key;
}


回收结点


其实就是维护内存池,讲腾出的空间记录下来,给下一个插入的节点使用


inline void free_node(int p)
{
    st[++top] = p;
}


初始化


按照定义,链表头尾应分别为负与正无穷。但是有时候是不需要的,不过为避免某些锅还是打上的好


inline void init()
{
    new_node(head, -INF), new_node(tail, INF);
    for (register int i = 1; i <= MAX_LEVEL; ++i)
        sl[head].next[i] = tail;
}


查找


从最上层开始,如果key小于或等于当层后继节点的key,则平移一位;如果key更大,则层数减1,继续比较。最终一定会到第一层(想想为什么)


查找 117



插入


先确定该元素要占据的层数 K(采用丢硬币的方式,这完全是随机的)。


然后在 Level 1 … Level K 各个层的链表都插入元素。


用Update数组记录插入位置,同样从顶层开始,逐层找到每层需要插入的位置,再生成层数并插入。


例子:插入 119, K = 2



void insert(int key)
{
    int p = head;
    int update[MAX_LEVEL + 5];
    int k = rand_level();
    for (register int i = MAX_LEVEL; i; --i)
    {
        while (sl[p].next[i] ^ tail && sl[sl[p].next[i]].key < key)
            p = sl[p].next[i];
        update[i] = p;
    }
    int temp;
    new_node(temp, key);
    for (register int i = k; i; --i)
    {
        sl[temp].next[i] = sl[update[i]].next[i];
        sl[update[i]].next[i] = temp;
    }
}


删除


与插入类似


void erase(int key)
{
    int p = head;
    int update[MAX_LEVEL + 5];
    for (register int i = MAX_LEVEL; i; --i)
    {
        while (sl[p].next[i] ^ tail && sl[sl[p].next[i]].key < key)
            p = sl[p].next[i];
        update[i] = p;
    }
    free_node(sl[p].next[1]);
    for (register int i = MAX_LEVEL; i; --i)
    {
        if (sl[sl[update[i]].next[i]].key == key)
            sl[update[i]].next[i] = sl[sl[update[i]].next[i]].next[i];
    }
}


三、全部代码示例


/*************************************************************************
    > File Name: skiplist.c
    > Author: 杨永利
    > Mail: 1795018360@qq.com 
    > Created Time: 2020年10月24日 星期六 07时57分57秒
 ************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#define LEVEL_MAX_LEN 10
#define true 1
#define false 2
typedef char bool;
// 跳跃表节点结构体
typedef struct SkipListNode {
    int key;
    int data;
    struct SkipListNode *forward[1];
}SkipListNode;
//跳跃表链表结构
typedef struct SkipList {
  int level;
  struct SkipListNode *head;
}SkipList;
//节点初始化
SkipListNode *CreatSkipNode(int level, int key, int data)
{
  SkipListNode *newNode = (SkipListNode *)malloc(sizeof(SkipListNode) + level * sizeof(SkipListNode));
  if (newNode == NULL)
    return NULL;
  newNode->key = key;
  newNode->data = data;
  return newNode;
}
//初始化跳表
SkipList *CreatSkipList()
{
  SkipList *newlist = (SkipList *)malloc(sizeof(SkipList));
  if (newlist == NULL)
    return NULL;
  newlist->head = CreatSkipNode(LEVEL_MAX_LEN - 1, 0, 0);
  for (int i = LEVEL_MAX_LEN - 1; i >= 0; i--) {
    newlist->head->forward[i] = NULL;
  }
  return newlist;
}
//随机产生层数
int RandLevel()
{
  int k = 1;
  while (rand() % 2)
    k++;
  return (k < LEVEL_MAX_LEN) ? k : LEVEL_MAX_LEN;
}
//插入节点
bool InsertNode(SkipList *sl, int key, int data)
{
  SkipListNode *update[LEVEL_MAX_LEN];
  SkipListNode *p, *q = NULL;
  p = sl->head;
  int k = sl->level;
  //从高到低找节点插入的位置,update存储每一层应该插入的位置
  for (int i = k - 1; i >= 0; i--) {
    while ((q = p->forward[i]) && (q->key < key)) {
      p = q;
    }
    update[i] = p;
  }
  //不能插入相同的key
  if (q && q->key == key) {
    return false;
  }
  //产生一个随机层数
  //新建一个待插入节点q,层层插入
  k = RandLevel();
  //更新跳跃表的level
  if (k > (sl->level)) {
    for (int i = (sl->level); i < k; ++i) {
      update[i] = sl->head;
    }
    sl->level = k;
  }
  q = CreatSkipNode(k, key, data);
  //逐层更新节点的指针,跟普通链表的插入一样
  for (int i = 0; i < k; ++i) {
    q->forward[i] = update[i]->forward[i];
    update[i]->forward[i] = q;
  }
  return true;
}
//搜索指定的key
int SearchByKey(SkipList *sl, int key)
{
  SkipListNode *p, *q = NULL;
  int k = sl->level;
  p = sl->head;
  for (int i = k - 1; i >= 0; i--) {
    while ((q = p->forward[i]) && (q->key <= key)) {
      if (q->key == key)
        return (q->key);
      p = q;
    }
  }
  return 0;
}
//删除指定的key
bool deleteNode(SkipList *sl, int key)
{
  SkipListNode *p, *q = NULL;
  SkipListNode *update[LEVEL_MAX_LEN];
  p = sl->head;
  int k = sl->level;
  for (int i = k - 1; i >= 0; i--) {
    while ((q = p->forward[i]) && (q->key < key)) {
      p = q;
    }
    update[i] = p;
  }
  if (q && q->key == key) {
    //逐层删除,和删除普通链表一样
    for (int i = 0; i < sl->level; ++i) {
      if (update[i]->forward[i] == q) {
        update[i]->forward[i] = q->forward[i];
      }
    }
    free(q);
    //如果删除的是最大层的节点,那末需要重新维护跳表
    for (int i = sl->level - 1; i >= 0; i--) {
      if (sl->head->forward[i] == NULL) {
        sl->level--;
      }
    }
    return true;
  }
  else {
    return false;
  }
}
//打印跳跃表
void PrintSkipList(SkipList *sl)
{
  SkipListNode *p, *q = NULL;
  //从最高层开始打印
  int  k = sl->level;
  for (int i = k - 1; i >= 0; i--) {
    p = sl->head;
    while (q = p->forward[i]) {
      printf("%d->", p->data);
      p = q;
    }
    printf("\n");
  }
  printf("\n");
}
int main(int argc, char* argv[]){
    SkipList *sl=CreatSkipList();
    for(int i=0;i<=19;i++)
    {
        InsertNode(sl,i,i*2);
    }
    PrintSkipList(sl);
    int m=SearchByKey(sl,4);
    printf("找到的结果为:%d\n",m);
    bool b = deleteNode(sl, 4);
  if (b) {
    printf("删除成功\n");
  }
  PrintSkipList(sl);
  system("pause");
    return 0;
}


相关文章
|
1月前
|
NoSQL 算法 安全
Redis原理—1.Redis数据结构
本文介绍了Redis 的主要数据结构及应用。
Redis原理—1.Redis数据结构
|
5月前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
70 1
|
1月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
5月前
|
机器学习/深度学习 存储 人工智能
数据结构在实际开发中的广泛应用
【10月更文挑战第20天】数据结构是软件开发的基础,它们贯穿于各种应用场景中,为解决实际问题提供了有力的支持。不同的数据结构具有不同的特点和优势,开发者需要根据具体需求选择合适的数据结构,以实现高效、可靠的程序设计。
324 63
|
4月前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
116 5
|
4月前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
111 1
|
4月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
88 5
|
4月前
|
存储 NoSQL Redis
Redis常见面试题:ZSet底层数据结构,SDS、压缩列表ZipList、跳表SkipList
String类型底层数据结构,List类型全面解析,ZSet底层数据结构;简单动态字符串SDS、压缩列表ZipList、哈希表、跳表SkipList、整数数组IntSet
|
4月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
428 9
|
4月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
68 1