首先什么是skiplist,在往下看之前先看一下下面几篇文章吧
了解skiplist后我们说道排行榜,排行榜实际上就是一种排序算法,常见的排序算法就是二叉树系列,红黑树,最小堆,最大堆等等,虽然他们可以提供排序功能,它们要找到一个值也很快,但是要想知道这个值排名第几,似乎只能按照先序遍历的方式来统计排在前面的值个数,跳表本身提供的功能类似于平衡二叉树以及高级变种,可以对目标值进行快速查找,时间复杂度为O(lgN)。跳表的实现原理比实现一颗高效的平衡二叉树(比如红黑树)要简单太多,这是跳表非常大的一个优势。关键在于,跳表计算某个score的排名次序,与在跳表中找到这个score的时间复杂度是一样的,仍旧是O(lgN)。
原生的skiplist虽然可以快速的查找一个值的排名,但是如果查找某个排名的值,或者说查找摸个排名范围内的值也只能通过先序遍历的方式,排行榜经常会有查找一定排名范围内的需求,比如需要直到20-50名的所有信息,这就需要在节点中额外维护一个排名信息,另外排行榜中有时候还需要找倒数排名而skiplist从实现上是一个单向链表想要满足需求必须改成双向链表,所以想要更快更丰富的的查找,就需要进一步的修改skiplist的实现。
这种数据结构其实就是 redis 里的zset实现,底层其实涉及2个关键数据结构:
哈希表:维护uid -> score的映射关系
跳表:维护score的从小到的有序关系
下面我们看一下具体的实现,基本上是基于redis 里的zset实现的,稍有不同抽出其中的需要的部分封装成一个可以做排行榜的一个模版类。
template <typename SCORE_TYPE,typename MEMBER_TYPE>
struct skiplistNode
{
skiplistNode(int level)
{
this->backward = NULL;
this->level = level;
this->levels = new zskiplistLevel[level];
for (int i = 0;i < level;i++)
{
this->levels[i].forward = NULL;
this->levels[i].span = 0;
}
}
~skiplistNode()
{
if (levels) delete[] levels;
levels = NULL;
backward = NULL;
}
SCORE_TYPE score; //排序key,必须是一个可以比较的类型,必须实现对象的比较操作符(一般位数值类型(long,int,double等))
MEMBER_TYPE ele; //分数对应的对象信息,必须是一个可以比较的类型,必须实现对象的比较操作符
skiplistNode *backward; //第0层的回溯指针
//每层的节点信息
struct zskiplistLevel
{
skiplistNode<SCORE_TYPE,MEMBER_TYPE> *forward; //当前层当前节点下一个节点
unsigned long span; //该层中当前节点与下一结点之间相隔几个节点(在level[0]中的结点数,level[0]节点是连续的该值为0)
};
int level;
zskiplistLevel *levels;
};
template <typename SCORE_TYPE,typename MEMBER_TYPE>
struct skiplist
{
skiplistNode<SCORE_TYPE,MEMBER_TYPE> *header, *tail; //header和tail分别指向头结点和尾结点,
unsigned long length; //结点数量,
int level; //level为表中结点的最高等级。
};
相关操作
template <typename SCORE_TYPE,typename MEMBER_TYPE>
struct skiplist
{
skiplistNode<SCORE_TYPE,MEMBER_TYPE> *header, *tail; //header和tail分别指向头结点和尾结点,
unsigned long length; //结点数量,
int level; //level为表中结点的最高等级。
};
template <typename SCORE_TYPE,typename MEMBER_TYPE>
class CRankSkipList
{
public:
/**
* 注意初始化的时候调表的头部指针已经存在但是长度为0
*/
CRankSkipList(unsigned long maxLen = 0)
{
mskiplist.level = 1;
mskiplist.length = 0;
mskiplist.header = new skiplistNode<SCORE_TYPE,MEMBER_TYPE>(ZSKIPLIST_MAXLEVEL);
mskiplist.tail = NULL;
mrankMap.clear();
mrankMaxLen = maxLen;
}
~CRankSkipList()
{
skiplistNode<SCORE_TYPE,MEMBER_TYPE> *node = mskiplist.header->levels[0].forward, *next;
delete mskiplist.header;
while(node) {
next = node->levels[0].forward;
delete node;
node = next;
}
}
/**
* 插入一个新的节点如果ele已存在,则更新积分
* @param zsl
* @param score
* @param ele
* @return 插入的skiplistNode
*/
skiplistNode<SCORE_TYPE,MEMBER_TYPE> *InsertOrUpdateNode(SCORE_TYPE score, MEMBER_TYPE ele);
/**
* 删除一个节点
* @param score
* @param ele
* @return 1 ok 0 not found
*/
int DeleteNode(MEMBER_TYPE ele);
/**
*
* @param start
* @param end
* @param dict
* @return 删除rank排名的节点
*/
unsigned long DeleteRangeByRank(unsigned long rank);
/**
*
* @param start
* @param end
* @param dict
* @return 删除排名范围内的节点[start,end]
*/
unsigned long DeleteRangeByRank(unsigned long start, unsigned long end);
/**
* update一个节点
* @param curscore
* @param ele
* @param newscore
* @return 插入的skiplistNode
*/
skiplistNode<SCORE_TYPE,MEMBER_TYPE> *UpdateScore(SCORE_TYPE newscore,MEMBER_TYPE ele);
/**
* 获取排名
* @param ele
* @return 0 不存在 [1,2^64] rank
*/
unsigned long Rank(MEMBER_TYPE ele);
/**
*获取指定排名的节点info
* @param zsl
* @param rank
* @return
*/
skiplistNode<SCORE_TYPE,MEMBER_TYPE>* GetNodeByRank(unsigned long rank);
/**
*获取指定范围排名的节点info
* @param zsl
* @param rank
* @return rank[start,end]
*/
std::list<skiplistNode<SCORE_TYPE,MEMBER_TYPE>*> GetRangeNodesByRank(unsigned long start, unsigned long end);
private:
/**
* 创建一个节点
* @param level
* @param score
* @param value
* @return
*/
skiplistNode<SCORE_TYPE,MEMBER_TYPE>* CreateSkipListNode(int level,SCORE_TYPE score,MEMBER_TYPE value);
/**
* 删除节点
* @param deleteNode
* @param update
*/
void DeleteNode(skiplistNode<SCORE_TYPE,MEMBER_TYPE> *deleteNode, skiplistNode<SCORE_TYPE,MEMBER_TYPE> **update);
//随机跳表的层数
int RandomLevel(void)
{
int level = 1;
while ((random() & 0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
private:
typedef typename std::unordered_map<MEMBER_TYPE,skiplistNode<SCORE_TYPE,MEMBER_TYPE>*>::iterator rank_it;
//tips:最初想把rank也缓存在map里,但是发现插入一个节点很可能会导致很多甚至所有的节点的排名都发生变化,缓存失效的概率太大
std::unordered_map<MEMBER_TYPE,skiplistNode<SCORE_TYPE,MEMBER_TYPE>*> mrankMap;
skiplist<SCORE_TYPE,MEMBER_TYPE> mskiplist;
unsigned long mrankMaxLen;//排行榜最大长度 0 不限制
};
由于篇幅长度原因,直接给项目地址,相关操作的具体实现和测试代码以及测试结果都有,redis的zset是存字符串的,我这里把他改成了对象,用模版类来封装,还有redis默认是排序是从小到大,我改成了从大到小,排行榜一般都是从小到大,另外加了一些错误的打印和异常保护。
项目地址:基于skiplist实现的排行榜模板(参考redis zset)
参考文章: