【LeetCode 1206】 设计跳表,C语言实现

跳跃表简介

跳跃表是一种以O(log N)期望时间支持查找、插入、删除操作的、有序的数据结构。其性能和红黑树相当,且跳跃表实现更为简单。

如何理解”跳跃“二字

  1. 首先,考察一个有序的单链表。如下图所示,该链表由8个元素组成,为了查找元素14,需要依次遍历 2 -> 4 -> 6 -> 8 -> 10 -> 12 -> 14, 共考察7个节点。考察的节点数正比于链表长度,查找的时间复杂度为O(n),效率很低。

在这里插入图片描述

  1. 为了加快查找速度,可以对单链表进行改造,每隔一个节点新增一个指针,指向它前面两个位置上的节点。所有新增的节点组成一条新的单链表(下图中的链表1)。同样查找元素14,现在只需考察4, 8, 12, 14,共4个节点。
    在这里插入图片描述

  2. 对链表1做类似的操作,又得到一条新的单链表(下图中的链表2)。此时查找元素14,只需考察8, 12, 14,共3个节点。

在这里插入图片描述

可以看出,跳跃表是由多条有序链表组成,支持折半查找的数据结构。

实现跳跃表

以下用C语言实现一个简单的跳表。跳表实现要求如下,详细参考:LeetCode 1206 设计跳表

  • 需实现跳表创建、查找、插入、删除、释放等操作,不需实现区间查找。
  • 跳表中的元素类型均为int
  • 跳表中可以存在多个相同的值。
0. 数据结构设计

设计SkiplistNode结构表示跳跃表节点,如下:

typedef struct SkiplistNode {
	int value;							// 存储值
	struct SkiplistLevel {
		struct SkiplistNode *next;
	} level[];							// 层,这里设计成柔性数组,简化malloc和free操作
} SkiplistNode;

设计Skiplist结构持有这些跳跃表节点,如下:

typedef struct {
	struct SkiplistNode *head;			// 跳跃表表头节点
    int length;							// 跳跃表节点数,获取跳跃表长度的时间复杂度O(1)
    int level;							// 记录跳跃表内,层数最大的那个节点的层数。
} Skiplist;
  • length表示跳跃表节点数,使得获取跳表长度的时间复杂度降为O(1)。
  • level表示跳表层数,跳表的插入、删除操作需要读取和更新level的值。

下图表示一个层数为3的节点:

在这里插入图片描述

下图表示一个长度为6,层数为4的跳表:

在这里插入图片描述

1. 创建跳跃表

设计Skiplist* skiplistCreate()方法创建跳跃表,要点如下:

  • 给Skiplist分配空间,长度初始为0,层高初始为1
  • 创建并初始化跳表的附加头节点,并设置层高为SKIPLIST_MAXLEVEL(32)
#define SKIPLIST_MAXLEVEL 32
// 跳表的创建
Skiplist* skiplistCreate() {
    Skiplist *sl = (Skiplist *)malloc(sizeof(*sl));
    sl->length = 0;
    sl->level = 1;
    sl->head = skiplistNodeCreate(SKIPLIST_MAXLEVEL, INT_MIN); // 初始化表头节点的层高为32
    for (int i = 0; i < SKIPLIST_MAXLEVEL; ++i) {
        sl->head->level[i].next = NULL;		// 初始化表头节点
    }
    return sl;
}
// helper func
SkiplistNode* skiplistNodeCreate(int level, int value) {
    SkiplistNode *p = (SkiplistNode *)malloc(sizeof(*p) + sizeof(struct SkiplistLevel) * level);
    p->value = value;
    return p;
}
2. 查找

设计bool skiplistSearch(Skiplist* obj, int target)实现跳表的查找。

上面分析过,跳表就是由N条有序链表组成的,所以对跳表的查找相当于从高到低,依次在N条有序链表中查找。

举例说明,在下图给出的跳表中,查找元素60,红色箭头表示遍历过程。

在这里插入图片描述

代码实现如下:

// 跳表的查找, 时间复杂度O(logN)
bool skiplistSearch(Skiplist* obj, int target) {
   SkiplistNode *p = obj->head;
   int levelIdx = obj->level - 1;
   for (int i = levelIdx; i >= 0; --i) {
       // 如果第i层节点值小于target, 就沿着当前层继续查找
       while (p->level[i].next && p->level[i].next->value < target) {
           p = p->level[i].next;
       }
       // 第i层未找到该节点, 或者节点值已大于target, 沿着下一层继续查找
       if (p->level[i].next == NULL || p->level[i].next->value > target) {
           continue;
       }
       return TRUE;
   }
   return FALSE;
}
3. 插入

设计void skiplistAdd(Skiplist* obj, int num)实现跳表的查找,要点如下:

  • 新增节点时,确定这个新增节点的层高。
  • 如果新增节点的层数为N,需对这N条单链表分别执行插入操作。
  • 成功插入节点后,注意更新跳表的长度和层高。
3.1 如何确定新增节点的层高?

跳表使用抛硬币的思想决定一个新增节点的层高,即有1/2的概率层数为1,1/4的概率层数为2,1/8的概率层数为3,以此类推。 这里实现GetSkipNodeRandomLevel方法,确定新增节点层高,如下:

int GetSkipNodeRandomLevel() {
    int level = 1;
    while (rand() & 0x1) {					// 抛硬币思想,随机数为奇数的概率可认为是1/2
        ++level;
    }
    return min(level,SKIPLIST_MAXLEVEL); 	// 返回的最大层数不超过32
}
3.2 新增节点后,如何更新跳表中对应的N条单链表?

举例说明,给定一个包含6个元素,层数为4的跳表,现在新增一个节点值为80,层数为5,插入前后的变化如下:

在这里插入图片描述

可以看出,往跳表中插入元素,只需在遍历跳表的过程中,保存这5条链表待插入位置的前驱节点(红圈表示),再分别对每条单链表执行插入操作即可,最后更新跳表的长度和层高。代码实现如下:

// 跳表的插入 O(logN)
void skiplistAdd(Skiplist* obj, int num) {
    SkiplistNode *p = obj->head;
    int levelIdx = obj->level - 1;
    struct SkiplistNode *preNodes[SKIPLIST_MAXLEVEL]; // 保存待插入节点的所有前驱节点的值
    for (int i = obj->level; i < SKIPLIST_MAXLEVEL; ++i) {
        preNodes[i] = obj->head;					  // 初始化值为附加头结点
    }

    for (int i = levelIdx; i >= 0; --i) {
        // 如果第i层节点值小于target, 沿当前层继续查找插入的位置
        while( p->level[i].next && p->level[i].next->value < num) {
            p = p->level[i].next;
        }
        preNodes[i] = p;
    }

    int newLevel = GetSkipNodeRandomLevel();		// 计算新插入节点的层数
    struct SkiplistNode *newNode = skiplistNodeCreate(newLevel, num);
    for (int i = 0; i < newLevel; ++i) {
        newNode->level[i].next = preNodes[i]->level[i].next;
        preNodes[i]->level[i].next = newNode;
    }
    obj->level = max(obj->level, newLevel);         // 完成插入动作后,更新跳跃表当前层数
    ++obj->length;									// 完成插入动作后,更新跳跃表长度
}
4. 删除

设计bool skiplistErase(Skiplist* obj, int num)方法实现跳表的删除,要点如下:

  • 遍历跳表,确认待删除的值是否存在,这步和跳表的查找操作类似。
  • 设待删除节点的层数为N,需对N条单链表分别执行删除操作。
  • 成功删除节点后,注意更新跳表的长度和层高。

代码实现如下:

// 跳跃表删除操作 O(logN)
bool skiplistErase(Skiplist* obj, int num) {
    SkiplistNode *p = obj->head;
    int levelIdx = obj->level - 1;
    struct SkiplistNode *preNodes[SKIPLIST_MAXLEVEL]; // 存储所有待删除节点的前驱节点的值
    for (int i = levelIdx; i >= 0; --i) {
        // 如果第i层节点值小于num, 沿当前层继续查找
        while (p->level[i].next && p->level[i].next->value < num) {
            p = p->level[i].next;
        }
        preNodes[i] = p;
    }

    p = p->level[0].next;
    if (p && p->value == num) {	
        skiplistNodeDelete(obj, p, preNodes);
        return TRUE;
    }
    return FALSE;
}

void skiplistNodeDelete(Skiplist *obj, SkiplistNode *cur, SkiplistNode **preNodes)
{
    for (int i = 0; i < obj->level; ++i) {
        if (preNodes[i]->level[i].next == cur) { // 被删除的节点层数可能比跳表层数少,所以要加上这里的判断
            preNodes[i]->level[i].next = cur->level[i].next;
        }
    }
    // 如果删除的节点是层数最大的,那么可能需要更新跳表长度
    for (int i = obj->level - 1; i >= 1; --i) {
        if (obj->head->level[i].next != NULL) {
            break;
        }
        --obj->level;
    }
    --obj->length;
    // 释放被删除节点空间
    free(cur);
}
5. 跳表的释放

释放操作很简单。对于每个跳跃表节点,只需调1次free()即可。这也是SkiplistNode结构中level成员设计为柔性数组的好处。

void skiplistFree(Skiplist* obj) {
    SkiplistNode *cur = obj->head->level[0].next;
    SkiplistNode *d;
    while(cur) {
        d = cur;
        cur = cur->level[0].next;
        free(d);
    }
    free(obj->head);		   
    free(obj);
}

源码参考:https://leetcode-cn.com/problems/design-skiplist/solution/tiao-yue-biao-cyu-yan-shi-xian-by-pcj700

参考资料

【1】 Skip List–跳表

【2】《数据结构与算法分析 C语言描述》原书第2版 10.4.2 —— 跳跃表

【3】《Redis设计与实现》—— 第5章 跳跃表

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
好的,以下是LeetCode 635题用C语言实现的代码,希望能够帮到您。 题目描述: 设计一个简易日志系统,支持以下操作: 1. 新增一条日志到日志系统中。 2. 统计日志系统中某个给定时间段内的日志数量。 3. 统计日志系统中所有日志的数量。 实现: ```c typedef struct { char* id; int timestamp; } Log; typedef struct { Log* logs; int size; int capacity; } LogSystem; LogSystem* logSystemCreate() { LogSystem* obj = (LogSystem*)malloc(sizeof(LogSystem)); obj->logs = (Log*)malloc(sizeof(Log) * 1001); obj->size = 0; obj->capacity = 1001; return obj; } void logSystemAdd(LogSystem* obj, int id, char* timestamp) { obj->logs[obj->size].id = (char*)malloc(sizeof(char) * 15); sprintf(obj->logs[obj->size].id, "%d", id); char year[5], month[3], day[3], hour[3], minute[3], second[3]; strncpy(year, timestamp, 4); year[4] = '\0'; strncpy(month, timestamp + 5, 2); month[2] = '\0'; strncpy(day, timestamp + 8, 2); day[2] = '\0'; strncpy(hour, timestamp + 11, 2); hour[2] = '\0'; strncpy(minute, timestamp + 14, 2); minute[2] = '\0'; strncpy(second, timestamp + 17, 2); second[2] = '\0'; obj->logs[obj->size].timestamp = atoi(year) * 100000000 + atoi(month) * 1000000 + atoi(day) * 10000 + atoi(hour) * 100 + atoi(minute); obj->size++; } int* logSystemRetrieve(LogSystem* obj, char* s, char* e, char* gra, int* returnSize) { int start, end, len; int* res = (int*)malloc(sizeof(int) * obj->size); *returnSize = 0; if (strcmp(gra, "Year") == 0) { start = atoi(strncpy(s, s, 4)) * 1000000; end = atoi(strncpy(e, e, 4)) * 1000000; len = 4; } else if (strcmp(gra, "Month") == 0) { start = atoi(strncpy(s, s, 7)) * 10000; end = atoi(strncpy(e, e, 7)) * 10000; len = 7; } else if (strcmp(gra, "Day") == 0) { start = atoi(strncpy(s, s, 10)) * 100; end = atoi(strncpy(e, e, 10)) * 100; len = 10; } else if (strcmp(gra, "Hour") == 0) { start = atoi(strncpy(s, s, 13)); end = atoi(strncpy(e, e, 13)); len = 13; } else if (strcmp(gra, "Minute") == 0) { start = atoi(strncpy(s, s, 16)); end = atoi(strncpy(e, e, 16)); len = 16; } else { start = atoi(strncpy(s, s, 19)); end = atoi(strncpy(e, e, 19)); len = 19; } for (int i = 0; i < obj->size; i++) { int time = obj->logs[i].timestamp; if (time >= start && time < end) { res[(*returnSize)++] = atoi(obj->logs[i].id); } } int* result = (int*)malloc(sizeof(int) * (*returnSize)); for (int i = 0; i < *returnSize; i++) { result[i] = res[i]; } free(res); return result; } void logSystemFree(LogSystem* obj) { for (int i = 0; i < obj->size; i++) { free(obj->logs[i].id); } free(obj->logs); free(obj); } ``` 这段代码实现了一个简单的日志系统,包含了新增日志、统计日志数量等操作。在这个实现中,我们使用了结构体Log和LogSystem来示日志和日志系统。其中,Log包含了日志的id和时间戳,LogSystem包含了日志数组、数组大小和数组容量。 在新增日志的操作中,我们将日志的id和时间戳存储到日志对象中,同时将日志对象存储到日志数组中。 在统计日志数量的操作中,我们首先根据粒度参数gra将起始时间s和结束时间e转换成整数形式,然后遍历日志数组,统计符合要求的日志数量。 最后,在释放日志系统对象的操作中,我们需要将每个日志对象中的id字符串释放掉,并释放日志数组和日志系统对象本身所占用的内存。 希望这段代码能够帮到您,如果您有任何问题或疑问,请随时向我提出。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

pcj_888

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

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

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

打赏作者

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

抵扣说明:

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

余额充值