skiplist:size,remove,get接口的实现
学校讲到跳表并布置了相关作业,要求实现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
主要有以下四点:
- 实现时为了表示每一层都要找/删节点,我使用了一个level来记录当前层数。而一开始为了方便,在循环体里面,我直接使用了数据成员level(表示现在跳表最大层数),但这导致了一个问题:如果remove的过程中删除某层,那么必然会涉及到对level数值的改变,而如果这时循环还没有完成的话就会使得循环次数减少,也就是在操作过程中对要用到的数据进行了修改。
教训:如果要调用某个数据成员,且不希望其发生改变,最好把其数值拿出来,以防运行过程中对该成员产生修改。 - 开始设计时没有考虑到remove某个元素之后造成空层的情况。
这直接导致如果出现空层,put函数会卡死。
解决思路:每次执行完一层的寻找和删除之后,按理说p应该是被删除的元素前面那个元素。如果删除完这个元素之后这一层是空层,由于删除是从最高层一路删下来的,所以p一定是head节点,而且head->right必定为空,这就说明这一层已经没有元素了。那么就把head定义为head->down,并把原来的头结点删掉。 - 上面的解决方案里面存在漏洞:如果把整个list都删掉了呢?按照上面的思路删下来,最后删到底层的时候会把最底层的head节点删掉,并且让head置为nullptr,相当于把最底层也全部删除了。这明显不合理。
解决思路:对最底层特殊处理,如果判断出来要删除的是最底层,那么不删除head,只是把head->right置为nullptr,因为最底层删完下一层就没东西了。这个判断很简单,除了最底层之外,head->down都非nullptr。 - 在上面的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就可以。
反思
- 应当在写代码之前仔细考虑全过程,把逻辑、语义想明白,然后再开始动手写。
- 在设计测试用例时,在测试完各个功能后,也应该回头测试各个功能的组合。譬如这次put/get/remove各自测试都没有问题,但是remove不删掉空层可能会给put造成干扰。
- 在设计测试用例时应该尽量让所有想测试的东西都能直接看出来,不得马虎。开始为了测试是否能在删除时遇到key相同删除最左最上元素,直接输入了多个key、value相同的元素,但这很明显是无法看出到底删除的是哪一个的。如果换成key相同、value不同,则可以判断到底是哪一个元素被删掉了。
- 应当仔细考虑边界条件,写代码不得偷懒。譬如对空层的一些处理,一开始考虑到了,但是觉得不影响;然而后面测试时产生了干扰,又要返工重修。