【追求卓越10】算法--跳表

引导

        在上一节中,我们学习到二分查找,惊叹于它超高的效率(时间复杂度为O(logn))。但是二分查找有一个局限性就是依赖于数组,这就导致它应用并不广泛。

        那么适用链表是否可以做到呢?答案是可以的。只不过要复杂一点,算法中称为跳表。应用很广泛。

跳表

        跳表的实质就是通过多级索引结构加快查找速度。这么说可能不理解,我们通过下面一系列图来进行理解。

        我们在链表中查找一个元素的时间复杂度时O(n),这个我们在链表章节已经说明了。但是如何能够提高查找的效率呢?如果加上索引是否会快一些。

        如上图所示,我们每两个节点添加一个索引。通过这样的结构,查找13这个元素。原先需要9次比较操作,现在只需要5次。是不是减少了很多?但是当数据量n很大时,每两个节点引出一个索引,那么第一级索引就有n/2个节点。这样在第一级索引中也会消耗很多时间。

有什么好的解决方式吗?--多级索引

        通过添加多级索引,能够加快我们的查找速度,这就是跳表。

跳表的效率

        我们通过上面的图,了解到跳表的确能够提高我们的查找速度,但是它的时间复杂度时多少呢?我们继续上面的例子来分析。

        假设我们原始链表的长度为n,那么第一即的索引个数就是n/2,第二级索引个数就是n/4,...第k级索引个数就是n/2^k。

每层索引最多查找3次。故该跳表的最差情况的时间复杂度是O(3*logn)=O(logn)。

其实我们知道这也是典型的空间换时间的方式。这么多的索引岂不是很浪费内存?

跳表浪费内存吗?

答案是肯定的,这么多的索引节点,当然会浪费内存。根据上面的跳表,我们也可以计算出空间复杂度为O(n-2)。但是这种空间的浪费重要吗?或者说有什么方式可以缩减这个空间消耗呢?

  1. 通过跳表原理的介绍,我们知道索引节点只需要存储几个指针和关键值。相对于原始数据中可能是很大的对象,这个就可以忽略了。
  2. 我们也可以增加节点间隔来产生索引,比如10个节点产生一个索引。你可以尝试计算一下空间复杂度

插入删除

        我们知道链表的插入删除操作的时间复杂度是O(1),但无奈于插入删除之前有查找操作,这就导致实际中我们想要删除或插入一个节点的时间复杂度是O(n)。现在跳表查找的时间复杂度是O(logn),那么我们插入删除的炒作也就是O(logn)。

插入操作:

插入一个数据变得更加高效了。如果只在原始链表中进行数据的插入,不对索引进行更新,就会出现下面的问题:

这样复杂度容易退化O(n)。因此跳表的删除,插入操作一定要更新索引表,用来保持这种平衡:当原始数据多了,就适当增加索引的节点。避免复杂度退化。

跳表平衡的维护

一般这种平衡性是通过随机函数来维持的。一般常用的方式是抛硬币法。我将会在代码中进行解析。

#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#define MAXLEVEL 15
typedef struct node{
    int val;
    struct node * level[MAXLEVEL];
}Node;
int initslist(Node* head)
{
    head->val = -1;
    memset(head->level,0,sizeof(Node*)*MAXLEVEL);
    return 0;
}
int random_level(void)
{
    int i, level = 1;
    for (i = 1; i < MAXLEVEL; i++)
            if (random() % 2 == 1)
                    level++;
    return level;
}
insertslist(Node* head,int val)
{
    int level = random_level();
    Node* new = (Node*)malloc(sizeof(Node));
    new->val = val;
    memset(new->level,0,sizeof(Node*)*MAXLEVEL);
    Node* p = head;
    Node* update[MAXLEVEL] = {0};

    int i = 0;
    for(i = level - 1 ; i >= 0 ; i--)
    {
        while(p->level[i] && p->level[i]->val < val)
            p = p->level[i];
        update[i] = p;
    }
    for(i = 0 ; i < level ; i++)
    {
        new->level[i] = update[i]->level[i];
        update[i]->level[i] = new;
    }
}
Node * findslist(Node* head,int target)
{
    Node * p = head;
    int i = MAXLEVEL - 1;
    for( ; i >= 0 ; i--)
    {
            while(p->level[i] && p->level[i]->val < target)
            {
                    p = p->level[i];
            }
    }
    if(p->level[0] && p->level[0]->val == target)
        return p->level[0];
    else
    return NULL;
}
int deleteslist(Node* head, int target)
{
    Node * p = head;
    int i = MAXLEVEL - 1;
    Node* update[MAXLEVEL] = {0};
    for(; i >= 0 ; i--)
    {
        while(p->level[i] && p->level[i]->val < target)
            p = p->level[i];
       update[i] = p;
    }
    if(update[0]->level[0] == NULL || update[0]->level[0]->val != target)
        return -1;

    for(i = MAXLEVEL -1  ; i >= 0 ; i--)
    {
        if(update[i]->level[i] && update[i]->level[i]->val == target)
          {
                update[i]->level[i] = update[i]->level[i]->level[i];
          }
    }
   

    return 0;
}
int printslist(Node* head)
{
    Node * p = head;
    int i = MAXLEVEL - 1;
    for(; i >= 0 ; i -- )
    {
        p = head;
        printf("Level[%02d]",i);
        while(p->level[i])
        {
            printf(" %4d (%p)",p->level[i]->val,p->level[i]);
            p = p->level[i];
        }
        printf("\n");
    }
    printf("\n");
}
int main()
{
    Node head;
    initslist(&head);
    srandom(time(NULL));
    int i =0;
    for(i = 20 ; i < 30 ; i++)
    insertslist(&head,i);
    printslist(&head);
    Node* temp = findslist(&head,25);
    printf("temp = %p\n",temp);
    deleteslist(&head,25);
    printslist(&head);
    return 0;
}

总结

        本节我们接触到了跳表这种数据结构。它的查找,删除,插入操作时间复杂度都是O(logn),并且不依赖于数组,因而它的应用场景更加广泛。

跳表是利用空间换时间的方式得到更高的效率。

        跳表在插入删除操作的时候不仅仅要对原始链表进行处理,也要更新索引结构,以保持两者的平衡,避免复杂度退化。保持平衡的方法一般用“抛硬币”方式。

        跳表的实现并不简单,需要好好琢磨并练习。

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢艺华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值