【初学高级数据结构:跳表】关于跳表get/remove/size接口的实现

本文介绍了实现跳表(Skiplist)类的size、remove、get接口的过程,包括代码实现、调试中遇到的问题及解决方案。在size()中,需在删除元素时更新计数;get(key)主要根据结构查找;remove(key)需考虑多层删除和空层处理。作者强调了在编码前理清思路和编写测试用例的重要性。
摘要由CSDN通过智能技术生成


学校讲到跳表并布置了相关作业,要求实现size、remove、get这三个接口,完成之后对跳表有了更深的理解,同时也注意到了之前忽视的一些不良习惯,特此记录。

skiplist基本知识

见大佬帖
https://blog.csdn.net/weixin_41462047/article/details/81253106

题目要求

Description

实现一个Skiplist类,部分代码已给出。

size_t size() 返回元素个数。

V* get(const K&) 若key存在返回指向value的指针;否则返回nullptr。若有多个相同key存在,返回靠左靠上的元素(即取层数最高的元素,如果有多个相同层高的,取最左边的)。

bool remove(const K&)若key存在删除该元素,有多层需要删除多层,然后返回true;否则返回false。若有多个相同key存在,只需删除靠左靠上的元素(即取层数最高的元素,如果有多个相同层高的,取最左边的)。

Example:

head -> (2, 2)

| |

L2 -> (2,2)

| |

L1 -> (1, 2) -> (2, 2)

实现代码

#include <iostream>
#include <vector>


template<typename K, typename V>
struct Node {
    Node<K, V>* right, * down;   //向右向下足矣
    K key;
    V val;
    Node(Node<K, V>* right, Node<K, V>* down, K key, V val) : right(right), down(down), key(key), val(val) {}
    Node() : right(nullptr), down(nullptr) {}
};

template<typename K, typename V>
class Skiplist {
public:
    Node<K, V>* head;
    int level = 1;
    Skiplist() {
        head = new Node<K, V>();  //初始化头结点
    }

    size_t size() {
        return sizeOfList;
    }

    void show() {
        Node<K,V>* newNode = head, *tmp = newNode->right;
        while (newNode) {
            tmp = newNode->right;
            while (tmp) {
                std::cout << "\"" << tmp->key << "," << tmp->val << "\"";
                tmp = tmp->right;
            }
            std::cout << std::endl;
            newNode = newNode->down;
        }
    }

    V* get(const K& key) {
        Node<K, V>* p = head;
        for (int i = 1; i <= level; ++i) {
            while (p->right && p->right->key < key) {
                p = p->right;
            }
            if (p->right && p->right->key == key) {
                return &(p->right->val);
            }
            p = p->down;
        }
        return nullptr;
    }

    void put(const K& key, const V& val) {
        std::vector<Node<K, V>*> pathList;    //从上至下记录搜索路径
        Node<K, V>* p = head;
        ++sizeOfList;
        while (p) {
            while (p->right && p->right->key < key) {
                p = p->right;
            }
            pathList.push_back(p);
            p = p->down;
        }
        
        bool insertUp = true;
        Node<K, V>* downNode = nullptr;
        while (insertUp && pathList.size() > 0) {   //从下至上搜索路径回溯,50%概率
            Node<K, V>* insert = pathList.back();
            pathList.pop_back();
            insert->right = new Node<K, V>(insert->right, downNode, key, val); //add新结点
            downNode = insert->right;    //把新结点赋值为downNode
            insertUp = (rand() & 1);   //50%概率
        }
        if (insertUp) {  //插入新的头结点,加层
            Node<K, V>* oldHead = head;
            head = new Node<K, V>();
            head->right = new Node<K, V>(NULL, downNode, key, val);
            head->down = oldHead;
            ++level;
        }
    }

    bool remove(const K& key) {
        Node<K, V>* p = head, * delNode = head, *nextNode = head;
        int isDelete = false, currentLevel = level, i = 0;
        for (; i < currentLevel; ++i) {
            while (p && p->right && p->right->key < key) {
                p = p->right;
            }
            if (p->right && p->right->key == key) {
                delNode = p->right;
                isDelete = true;
                break;
            }
            else p = p->down;
        }
        if (isDelete) {
            --sizeOfList;
            for (; i < currentLevel; ++i) {
                delNode = p->right;
                p->right = delNode->right;
                nextNode = delNode->down;
                delete delNode;
                delNode = nextNode;

                if (p == head && p->right == nullptr && p->down) {
                    head = head->down;
                    --level;
                    delete p;
                    p = head;
                }
                else p = p->down;
                while (p && p->right && p->right != delNode) {
                    p = p->right;
                }
            }

        }

        return isDelete;
    }
private:
        int sizeOfList = 0;
};

int main() {
    Skiplist<int, int> newSkipList;
    int key1 = 1, val1=2;
    std::string cmd;
    for (int i = 0; i < 9; ++i) {
        std::cin >> key1 >> val1;
        newSkipList.put(key1, val1);
    }
    std::cout << "over" << std::endl;
    while (true) {
        std::cin >> cmd;
        if (cmd == "put") {
            std::cin >> key1 >> val1;
            newSkipList.put(key1, val1);
        }
        if (cmd == "siz") {
            std::cout << newSkipList.size();
        }
        if (cmd == "get") {
            std::cin >> key1;
            if(newSkipList.get(key1))
            std::cout << *newSkipList.get(key1) << std::endl;
        }
        if (cmd == "del") {
            std::cin >> key1;
            if(newSkipList.remove(key1)) std::cout << "delete finished" << std::endl;
        }
        newSkipList.show();
        std::cout << newSkipList.level << std::endl;
    }
    return 0;
}

实现过程中debug过程

关于size()

· 设置了数据成员sizeOfList,并在put里面每put一个新元素就加一。但是由于开始时没有进行整体上的考虑,导致写到remove的时候忘记要在删除元素之后size-1了,提交频繁WA。
· 教训:在写代码前先把整体规划、伪代码写好,把每一步的思路理清楚,然后再开始写。

关于get(key)

get实现过程中并没有遇到太多问题,主要是画出结构图之后对照结构图一步步写下去就好。

关于remove(key)

这个函数是遇到问题最多的。

思路

p = head,然后从head开始往右找直到右边的元素key大于等于key。这时候右边的元素如果正好是要找的元素(p->right->key == key),那么就把右边元素这一列删除;否则p = p->down,要找的元素只可能存在于下一层。

bug

主要有以下四点:

  1. 实现时为了表示每一层都要找/删节点,我使用了一个level来记录当前层数。而一开始为了方便,在循环体里面,我直接使用了数据成员level(表示现在跳表最大层数),但这导致了一个问题:如果remove的过程中删除某层,那么必然会涉及到对level数值的改变,而如果这时循环还没有完成的话就会使得循环次数减少,也就是在操作过程中对要用到的数据进行了修改。
    教训:如果要调用某个数据成员,且不希望其发生改变,最好把其数值拿出来,以防运行过程中对该成员产生修改。
  2. 开始设计时没有考虑到remove某个元素之后造成空层的情况。
    这直接导致如果出现空层,put函数会卡死。
    解决思路:每次执行完一层的寻找和删除之后,按理说p应该是被删除的元素前面那个元素。如果删除完这个元素之后这一层是空层,由于删除是从最高层一路删下来的,所以p一定是head节点,而且head->right必定为空,这就说明这一层已经没有元素了。那么就把head定义为head->down,并把原来的头结点删掉。
  3. 上面的解决方案里面存在漏洞:如果把整个list都删掉了呢?按照上面的思路删下来,最后删到底层的时候会把最底层的head节点删掉,并且让head置为nullptr,相当于把最底层也全部删除了。这明显不合理。
    解决思路:对最底层特殊处理,如果判断出来要删除的是最底层,那么不删除head,只是把head->right置为nullptr,因为最底层删完下一层就没东西了。这个判断很简单,除了最底层之外,head->down都非nullptr。
  4. 在上面的remove里面还存在一个问题:由于一开始考虑逻辑上是这样的循环:对于每一层,找到右边元素大于等于key的元素,判断是否找到key == key的元素,是的话就删掉,然后p跳到下一层,直接从下一层的同一位置开始往后找。这样必定会找到一列key = key的元素并且删除,但是存在一个问题:它只是寻找每一层最左边的key符合要求的元素,但是如果遇到这种情况:
    (7,2) ------------->(9,2)
    (7,2)->(9,1)->(9,2)
    这时候按照上面的逻辑删除,会把上层的9,2和下层的9,1删掉,因为每次都会从左边开始重新寻找一个。这是函数运行逻辑上存在的问题,语义不符合要求。
    反思一下实际上的操作,应该是从最高层开始找,找到第一个key值相符的元素之后把这一列删掉。那么应当有一个保存这一列的方法,而这是很简单的,只需要每次删除之后把delNode设置为原先节点的down就可以。

反思

  1. 应当在写代码之前仔细考虑全过程,把逻辑、语义想明白,然后再开始动手写。
  2. 在设计测试用例时,在测试完各个功能后,也应该回头测试各个功能的组合。譬如这次put/get/remove各自测试都没有问题,但是remove不删掉空层可能会给put造成干扰。
  3. 在设计测试用例时应该尽量让所有想测试的东西都能直接看出来,不得马虎。开始为了测试是否能在删除时遇到key相同删除最左最上元素,直接输入了多个key、value相同的元素,但这很明显是无法看出到底删除的是哪一个的。如果换成key相同、value不同,则可以判断到底是哪一个元素被删掉了。
  4. 应当仔细考虑边界条件,写代码不得偷懒。譬如对空层的一些处理,一开始考虑到了,但是觉得不影响;然而后面测试时产生了干扰,又要返工重修。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值