Edit: This code was reworked and repostetd under a new question: Non generic Skip List implementation in C++ Version 2
To sharpen my C++ skills I tried to implement a skip list.
I was quite happy when it finally seemed to work correctly. Unfortunately, the implementation is kinda slow. When I run the main were I randomly insert / erase a map and my skip list, it's always like twice as slow as the map (I tried it with Visual Studio 2017 in Release).
I wonder what can be improved to get the big performance gain. I suspect the use of vector in the skipnode and the random number generator. I wonder if there's a faster way to flip a coin to establish the levels. I would also appreciate it if you have any other suggestions to improve the code.
One Note: I know it should be normally generic with templates. But first I want to make the code good before in the next step make it generic for any type.
skiplist.h
#ifndef SKIP_LIST_GUARD
#define SKIP_LIST_GUARD
#include <iostream>
#include <random>
#include <functional>
#include <vector>
#include <utility>
#include <exception>
namespace Skiplist {
struct Skipnode {
Skipnode(int key, int val, int lvl);
std::pair<int, int> kv; //first key, second value
std::vector <Skipnode*> next;
};
class Skiplist {
public:
Skiplist()
:maxlvl{ 0 }, head{ nullptr }, m_mt(std::random_device()())
{
}
~Skiplist();
void insert(int key, int val);
bool erase(int key); //search for an element and erase it from the skip list
Skipnode* find(int key) const; //find element by key and return the value
int size() const;
int get_maxlvl() const { return maxlvl; }
void print() const;
void debug_print() const;
private:
bool next_level()
//flips a coin if true goes up one lvl
// with every layer the chance is half so 1 = 50% 2 = 25% etc.....
{
std::uniform_int_distribution<int> dist(0, 1);
return dist(m_mt);
}
Skipnode* head; //element before first element
int maxlvl; // maximum level the nodes have reached so far
std::mt19937 m_mt; //random generator member
};
int get_random(int min, int max);
}
#endif
skiplist.cpp
#include "skiplist.h"
namespace Skiplist {
Skipnode::Skipnode(int key, int val, int lvl)
{
kv = std::make_pair(key, val);
for (int i = 0; i < lvl; ++i) //build all pointers for the lvl
next.push_back(nullptr);
next.shrink_to_fit(); //to not waste space
}
Skiplist::~Skiplist()
{
if (head == nullptr) return;
Skipnode* currpos = head; //start on head
while (currpos->next[0] != nullptr) {
Skipnode* lastpos = currpos;
currpos = currpos->next[0];
delete lastpos;
}
delete currpos; //delete last element
}
void Skiplist::insert(int key, int val)
{
//calculate max height of new node
int new_node_lvl = 0;
do {
++new_node_lvl;
if (new_node_lvl == (maxlvl + 1)) { //new node can maximum grow by one lvl;
++maxlvl;
if (maxlvl == 1) { //case first row needs to be created;
head = new Skipnode(0, 0, 0); //make a empty head
}
head->next.push_back(nullptr);
head->next.shrink_to_fit(); //to not waste to much space
break;
}
} while (next_level()); //flip coin. every time it is true go to the next lvl
Skipnode* new_node = new Skipnode(key,val,new_node_lvl); //create new node
int currlvl = maxlvl - 1; //start on highest lvl
Skipnode* currpos = head; //start on head
while (true) {
if (currpos->next[currlvl] == nullptr || currpos->next[currlvl]->kv.first > key) {
if (currlvl < new_node->next.size()){ //if node will be on this lvl. Install node on it
new_node->next[currlvl] = currpos->next[currlvl];
currpos->next[currlvl] = new_node;
}
--currlvl; // go to the next lvl
if (currlvl < 0)
break;
continue;
}
currpos = currpos->next[currlvl];
}
}
bool Skiplist::erase(int key)
{
Skipnode* currpos = head; //start on head
int currlvl = maxlvl - 1; //start on highest lvl
while (currlvl >=0) {
if (currpos->next[currlvl] == nullptr) {
--currlvl;
continue;
}
else if (currpos->next[currlvl]->kv.first > key) {
--currlvl;
continue;
}
else if (currpos->next[currlvl]->kv.first == key) { //key found on current lvl
--currlvl; //go down first before link is deleted
if(currlvl+1 !=0)
currpos->next[currlvl+1] = currpos->next[currlvl+1]->next[currlvl+1]; //take out pointer of found element from list
else { //case end
Skipnode* keynode = currpos->next[currlvl+1];
currpos->next[currlvl+1] = currpos->next[currlvl + 1]->next[currlvl + 1];
delete keynode;
if (head->next[maxlvl - 1] == nullptr && maxlvl >1) { //no nodes on highest lvl
head->next.pop_back(); //delete empty lvl
--maxlvl;
}
return true;
}
continue;
}
currpos = currpos->next[currlvl];
}
return false;
}
Skipnode* Skiplist::find(int key) const
//find element by key and return value
{
Skipnode* currpos = head; //start on head
int currlvl = maxlvl - 1; //start on highest lvl
while (true) {
if (currpos->next[currlvl] == nullptr || (currlvl > 0 && currpos->next[currlvl]->kv.first >= key)) {
--currlvl;
if (currlvl < 0)
return nullptr; //element was not found;
continue;
}
if (currlvl == 0 && currpos->next[currlvl]->kv.first == key) { // element found
currpos = currpos->next[currlvl];
return currpos;
}
currpos = currpos->next[currlvl];
}
return nullptr;
}
int Skiplist::size() const
{
if (head == nullptr) return 0; //special case nothing is build yet
int sz = 0;
Skipnode* currpos = head;
if (currpos->next.empty())
return sz;
while (currpos->next[0] != nullptr) {
++sz;
currpos = currpos->next[0];
}
return sz;
}
void Skiplist::print() const
//prints out all elements
{
Skipnode* currpos = head;
while (currpos != nullptr) {
if(currpos != head)
std::cout << currpos->kv.first<<"/"<< currpos->kv.second << " ";
currpos = currpos->next[0];
}
std::cout << "\n";
}
void Skiplist::debug_print() const
//messy debug routine to print with all available layers
{
Skipnode* currpos = head;
int currlvl = currpos->next.size() - 1;
currpos = currpos->next[currlvl];
if (head->next[0] == nullptr)
return;
while (currlvl >= 0) {
std::cout << "lvl: " << currlvl << "\t";
Skipnode* lastpos = head;
while (currpos != nullptr) {
if (currlvl > 0) {
int void_count = 0;
while (lastpos != nullptr && lastpos->kv.first != currpos->kv.first) {
lastpos = lastpos->next[0];
++void_count;
}
for (int i = 0; i < void_count-1; ++i)
std::cout << "-/-- ";
}
if(currpos != head)
std::cout << currpos->kv.first << "/" << currpos->kv.second << " ";
currpos = currpos->next[currlvl];
}
std::cout << "\n";
--currlvl;
currpos = head;
}
std::cout << "\n";
}
int get_random(int min, int max)
{
static std::random_device rd;
static std::mt19937 mt(rd());
std::uniform_int_distribution<int> distribution(min, max);
return distribution(mt);
}
}
main.cpp
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <ctime>
#include "skiplist.h"
int main()
try {
constexpr int repeats = 30;
constexpr int count_of_elements = 5000000;
std::vector <int> rnd;
std::map <int, int > mp;
for (int i = 0; i < repeats; ++i) {
for (int j = 0; j < count_of_elements; ++j) { //fill vector with 100.000 unique random elements
int in = 0;
while (true) {
in = Skiplist::get_random(1, std::numeric_limits<int>::max());
bool twice = false;
auto it = mp.find(in);
if (it == mp.end())
break;
}
rnd.push_back(in);
mp.insert(std::make_pair(in,i));
}
std::cout << rnd.size() << "\n";
mp.clear();
std::cout << '\n';
//fill map and skiplist and compare
clock_t begin_sk = clock();
Skiplist::Skiplist sk;
for (std::size_t i = 0; i < rnd.size(); ++i)
sk.insert(rnd[i], i);
clock_t end_sk = clock();
std::cout << "skiplist filled. Time:" << double(end_sk - begin_sk) / CLOCKS_PER_SEC << "\n";
clock_t begin_sk_d = clock();
for (std::size_t i = 0; i < rnd.size(); ++i)
sk.erase(rnd[i]);
clock_t end_sk_d = clock();
std::cout << "skiplist deleted. Time:" << double(end_sk_d - begin_sk_d) / CLOCKS_PER_SEC << "\n";
std::cout << '\n';
clock_t begin_mp = clock();
std::map<int, int> mp;
for (std::size_t i = 0; i < rnd.size(); ++i)
mp.insert(std::pair<int, int>(rnd[i], i));
clock_t end_mp = clock();
std::cout << "map filled. Time:" << double(end_mp - begin_mp) / CLOCKS_PER_SEC << "\n";
clock_t begin_mp_d = clock();
for (std::size_t i = 0; i < rnd.size(); ++i)
mp.erase(rnd[i]);
clock_t end_mp_d = clock();
std::cout << "map deleted. Time:" << double(end_mp_d - begin_mp_d) / CLOCKS_PER_SEC << "\n";
std::cout << '\n';
}
std::cin.get();
}
catch (std::runtime_error& e) {
std::cerr << e.what() << "\n";
std::cin.get();
}
catch (...) {
std::cerr << "unknown error\n";
std::cin.get();
}
std::mapis tree based, so O(log n) insertion/deletion. Hash map in C++ would bestd::unordered_map. Also, I don't get where you take O(n log n) from for skip list insertion/deletion, wikipedia lists O(log n) for average case, O(n) for worst case. \$\endgroup\$