赞
踩
\quad 跳表的理论参考这个PPT,本文主要介绍跳表的实现,以及跳表的性能随着概率因子变化而变化的情况,从而发现最优概率因子的范围。
\quad 跳表节点需要两个指针,一个指向右侧节点,一个指向下方节点,节点定义如下:
struct Node{
int val;
Node *next; // 右边节点
Node *down; // 下方节点
Node(int x){
val = x;
next = down = NULL;
}
};
\quad 定义跳表类SkipList,跳表需要一个头节点,表示最高层最左侧的节点,该节点初始化为最小值。跳表还需要指定一个超参数——概率因子prob,表明以prob的概率向上生成新的一层,相同节点数目下,prob越大跳表深度越深。
class SkipList{
public:
Node *head;
double prob; // 以prob的概率向上生成新的一层,prob越大跳表深度越深
SkipList(double _prob=0.5){
prob = _prob;
head = new Node(INT32_MIN); // 初始化头节点为最小值
}
}
\quad 接下来完善跳表类中的查询、删除和插入代码,如下:
bool search(int x){ // 查看跳表中是否存在值为x的节点 Node *p = head; while (p){ if(p->val == x) return true; else if(p->next == NULL) p = p->down; // 右侧节点为空,只能往下走 else if(p->next->val > x) p = p->down; // 右侧节点值大于x,往下走 else p = p->next; // 右侧节点值小于x,往右走 } return false; // p为空还没找到,则不存在 } void erase(int x){ // 删除跳表中值为x的节点 Node *p = head; while (p){ if(p->next == NULL) p = p->down; else if(p->next->val > x) p = p->down; else if(p->next->val == x){ p->next = p->next->next; // 删除右侧值为x的节点 p = p->down; }else p = p->next; } } void insert(int x){ // 往跳表中插入节点 if(search(x)) return; // 避免重复插入 stack<Node *> st; // 记录向下走过的节点,这一列节点后面可以插入x Node *p = head; while (p){ if(p->next == NULL || p->next->val > x){ st.push(p); // 右侧没有节点或者右侧节点值更大, 说明新节点应该加在当前节点与右侧节点之间 p = p->down; }else p = p->next; } Node *down = NULL; // 保存最后一个插入的节点,如果最后需要额外新增一层时需要用到 bool f = true; // 用于保证最下方一层一定能插入新节点 while (!st.empty()){ Node *pre = st.top(); // 当前节点右侧用于插入新节点 st.pop(); double num = 1.0 * rand() / RAND_MAX; // 取一个[0, 1)之间的随机数作为概率 if(f || num < prob){ // 如果是最下面一层或者概率超过prob,则插入节点 f = false; Node *t = new Node(x); if(pre->next == NULL) pre->next = t; else{ t->next = pre->next; // 插入新节点 pre->next = t; } t->down = down; down = t; }else return; } // 通过随机数判断是否再上升一层 double num = 1.0 * rand() / RAND_MAX; if(num < prob){ // 上升一层需要更新新的head节点 Node *t = new Node(x); Node *minNode = new Node(INT32_MIN); minNode->down = head; minNode->next = t; t->down = down; head = minNode; } }
\quad 接着可以给出一些辅助方法,即打印出该跳表和统计该跳表的最大深度、节点数和平均深度等信息,用于后续性能测试:
void show(){ Node *p = head; while (p){ Node *node = p->next; cout << "head"; while (node){ cout << "-->" << node->val; node = node->next; } cout << "-->NULL" << endl; p = p->down; } } void info(){ // 统计跳表最大深度,节点个数和节点平均深度 int maxDepth = 0; // 最大深度 unordered_map<Node*, int> level; // 所有节点的深度 Node *p = head; while (p) { maxDepth ++; Node *node = p->next; while (node){ level[node] = maxDepth; node = node->next; } p = p->down; } cout << "max depth=" << maxDepth << endl; int NodeNum = level.size(); // 节点数目 int s = 0; for(auto it: level) s += it.second; double avgLevel = s * 1.0 / NodeNum; // 平均深度 unordered_set<int> H; for(auto it: level) H.insert(it.first->val); double repeatRatio = 1.0 * NodeNum / H.size(); cout << "node num=" << NodeNum << ", avg level=" << avgLevel << endl; cout << "different val num=" << H.size() << ", repeat ratio=" << repeatRatio << endl; }
\quad 我们随机产生100万个不同的整数插入进跳表,再依次查询这100万个数,统计在不同概率因子下程序耗时,完整地代码如下:
#include <iostream> #include <stack> #include <unordered_map> #include <unordered_set> #include <ctime> #include <vector> using namespace std; struct Node{ int val; Node *next; // 右边节点 Node *down; // 下方节点 Node(int x){ val = x; next = down = NULL; } }; class SkipList{ public: Node *head; double prob; // 以prob的概率向上生成新的一层,prob越大跳表深度越深 SkipList(double _prob=0.5){ prob = _prob; head = new Node(INT32_MIN); // 初始化头节点为最小值 } bool search(int x){ // 查看跳表中是否存在值为x的节点 Node *p = head; while (p){ if(p->val == x) return true; else if(p->next == NULL) p = p->down; // 右侧节点为空,只能往下走 else if(p->next->val > x) p = p->down; // 右侧节点值大于x,往下走 else p = p->next; // 右侧节点值小于x,往右走 } return false; // p为空还没找到,则不存在 } void erase(int x){ // 删除跳表中值为x的节点 Node *p = head; while (p){ if(p->next == NULL) p = p->down; else if(p->next->val > x) p = p->down; else if(p->next->val == x){ p->next = p->next->next; // 删除右侧值为x的节点 p = p->down; }else p = p->next; } } void insert(int x){ // 往跳表中插入节点 if(search(x)) return; // 避免重复插入 stack<Node *> st; // 记录向下走过的节点,这一列节点后面可以插入x Node *p = head; while (p){ if(p->next == NULL || p->next->val > x){ st.push(p); // 右侧没有节点或者右侧节点值更大, 说明新节点应该加在当前节点与右侧节点之间 p = p->down; }else p = p->next; } Node *down = NULL; // 保存最后一个插入的节点,如果最后需要额外新增一层时需要用到 bool f = true; // 用于保证最下方一层一定能插入新节点 while (!st.empty()){ Node *pre = st.top(); // 当前节点右侧用于插入新节点 st.pop(); double num = 1.0 * rand() / RAND_MAX; // 取一个[0, 1)之间的随机数作为概率 if(f || num < prob){ // 如果是最下面一层或者概率超过prob,则插入节点 f = false; Node *t = new Node(x); if(pre->next == NULL) pre->next = t; else{ t->next = pre->next; // 插入新节点 pre->next = t; } t->down = down; down = t; }else return; } // 通过随机数判断是否再上升一层 double num = 1.0 * rand() / RAND_MAX; if(num < prob){ // 上升一层需要更新新的head节点 Node *t = new Node(x); Node *minNode = new Node(INT32_MIN); minNode->down = head; minNode->next = t; t->down = down; head = minNode; } } void show(){ Node *p = head; while (p){ Node *node = p->next; cout << "head"; while (node){ cout << "-->" << node->val; node = node->next; } cout << "-->NULL" << endl; p = p->down; } } void info(){ // 统计跳表最大深度,节点个数和节点平均深度 int maxDepth = 0; // 最大深度 unordered_map<Node*, int> level; // 所有节点的深度 Node *p = head; while (p) { maxDepth ++; Node *node = p->next; while (node){ level[node] = maxDepth; node = node->next; } p = p->down; } cout << "max depth=" << maxDepth << endl; int NodeNum = level.size(); // 节点数目 int s = 0; for(auto it: level) s += it.second; double avgLevel = s * 1.0 / NodeNum; // 平均深度 unordered_set<int> H; for(auto it: level) H.insert(it.first->val); double repeatRatio = 1.0 * NodeNum / H.size(); cout << "node num=" << NodeNum << ", avg level=" << avgLevel << endl; cout << "different val num=" << H.size() << ", repeat ratio=" << repeatRatio << endl; } }; void testTime(vector<int> &nums, double prob){ clock_t start, end; start = clock(); auto sp = new SkipList(prob); for(int num: nums){ sp->insert(num); } for(int num: nums){ sp->search(num); } end = clock(); cout << "test size=" << nums.size() << ", prob=" << prob << endl; cout << "cost time=" << (double)(end - start) / CLOCKS_PER_SEC << "s" << endl; sp->info(); cout << "--------------------------------" << endl; } int main(){ int n = 1000000; unordered_set<int> nums; while (nums.size() < n){ nums.insert(rand() * rand()); } vector<int> arr(nums.begin(), nums.end()); for(int i = 1; i <= 9; i ++ ){ testTime(arr, i * 1.0 / 10); } }
\quad
如上图所示,当概率因子为0.2时耗费时间3.4s,效率最高,此时跳表最大深度为6,平均深度为5.8892,跳表中节点个数为111w,意味着平均每个数被重复1.11次。下表给出了各个概率因子下的各具体参数:
概率因子 | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 |
---|---|---|---|---|---|---|---|---|---|
耗费时间(s) | 4.229 | 3.402 | 3.458 | 3.664 | 3.833 | 3.996 | 4.376 | 5.224 | 7.481 |
跳表最大深度 | 6 | 9 | 11 | 17 | 18 | 27 | 35 | 55 | 109 |
跳表中节点数目 | 1111198 | 1250773 | 1428339 | 1667249 | 1998415 | 2500761 | 3330720 | 4994432 | 9997933 |
跳表中节点的平均深度 | 5.8892 | 8.74889 | 10.5719 | 16.332 | 17.0013 | 25.4991 | 32.6704 | 51.0072 | 99.9984 |
跳表中节点的重复率 | 1.1112 | 1.25077 | 1.42834 | 1.66725 | 1.99842 | 2.50076 | 3.33072 | 4.99443 | 9.99793 |
\quad
经过更细粒度的实验,得到概率因子最佳值为0.27,此时耗时3s左右,最大深度11,平均深度10.63。
\quad
使用相同的数据和测试方法,使用set耗时2.2s,使用unordered_set耗时0.6s,使用map耗时2s,使用unordered_map耗时0.7s。由此可见,c++自带的set和map性能更优,原因在于其不存在重复节点的情况。跳表主要是实现简单,其实性能和空间占用都比c++ stl更差。
import java.util.*; public class SkipList { private class Node{ int val; Node next; Node down; Node(int x){ this.val = x; this.next = this.down = null; } } private Node head; private double prob; // 概率因子 SkipList(double prob){ this.prob = prob; this.head = new Node(Integer.MIN_VALUE); } public boolean search(int x){ Node p = head; while (p != null){ if(p.val == x) return true; if(p.next == null || p.next.val > x) p = p.down; else p = p.next; } return false; } public void erase(int x){ Node p = head; while (p != null){ if(p.next == null || p.next.val > x) p = p.down; else if(p.next.val == x){ p.next = p.next.next; p = p.down; }else p = p.next; } } public void insert(int x){ if(search(x)) return; Stack<Node> st = new Stack<>(); Node p = head; while (p != null){ if(p.next == null || p.next.val > x){ st.add(p); p = p.down; }else p = p.next; } Node down = null; boolean f = true; while(!st.isEmpty()){ Node pre = st.pop(); double num = Math.random(); if(f || num < this.prob){ f = false; Node t = new Node(x); if(pre.next == null) pre.next = t; else{ t.next = pre.next; pre.next = t; } t.down = down; down = t; }else return; } double num = Math.random(); if(num < this.prob){ Node t = new Node(x); Node minNode = new Node(Integer.MIN_VALUE); minNode.down = head; minNode.next = t; t.down = down; head = minNode; } } public void print(){ Node p = head; while (p != null){ Node node = p.next; System.out.print("head"); while (node != null){ System.out.print("-->" + node.val); node = node.next; } System.out.println("-->NULL"); p = p.down; } } public static void main(String[] args) { int n = 1000000; Set<Integer> st = new HashSet<>(); while (st.size() < n){ st.add((int)(Math.random() * 1000000000)); } ArrayList<Integer> arr = new ArrayList<>(); for(Integer num: st){ arr.add(num); } long start = System.currentTimeMillis(); SkipList sp = new SkipList(0.27); for(Integer num: arr){ sp.insert(num); } for(Integer num: arr){ sp.search(num); } long end = System.currentTimeMillis(); System.out.println("SkipList cost time=" + (end - start) / 1000.0 + "s"); start = System.currentTimeMillis(); Set<Integer> set = new TreeSet<>(); for(Integer num: arr){ set.add(num); } for(Integer num: arr){ set.contains(num); } end = System.currentTimeMillis(); System.out.println("TreeSet cost time=" + (end - start) / 1000.0 + "s"); } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。