排名系统的设计

转载请注明出处:http://blog.youkuaiyun.com/h010332722/article/details/19984843

问题

我们需要设计一个排名系统,它的功能是提供实时排名。并且规定,对于一名玩家,他参与排序的数值只会上升,不会下降的。

比如,在一个游戏中,获取前100名玩家的经验排名;如果一个玩家在这100名之内,我们可以准确获取到他的名次。由于玩家的经验是时刻在变化的,所以排名也就时刻发生改变,所以我们要用最小时间代价来获取维护排名系统。

思路

第1种方案,我们似乎可以用小顶堆来维护这个排名系统。但是现实是,堆很难查找出玩家在这个堆中的位置。如果这个玩家不在堆中,则只要比较堆顶元素,然后压堆就行了;如果这个玩家在堆中,就麻烦了。至于准确获取名次的需求,就几乎很难靠堆的灵活性来满足了。

 

第2种方案,我们也可以用一个大小为100的数组来保存玩家的排名 权重为(经验,玩家id),用一个字典来存储玩家id和玩家经验值的映射。

  • 当一个玩家的经验值发生改变时,我们优先查找字典,是否这玩家在排名中,如果在,则替换掉数值;如果不在,则和最小经验值的玩家比较。
  • 如果最终的结果是需要重新排序,则可以使用变形的二分查找(此时被替换掉的元素是乱序的,但是其他元素是有序的,可以对该元素前后的数组二分查找来定位),或者快速排序。只是这样会引起数组的插入(这个代价意味着需要拷贝数组),这样最终的复杂度还是维持在O(N)。
  • 对于获取精确玩家的名次,我们可以通过权重(玩家经验值,id),在数组中二分查找获取到。
  • 随机获取第k名的玩家,意味着获取数组中第k个元素,复杂度为O(1)

这种设计实际上满足我们的要求,空间复杂度低,稳定性好。对于排名总数少的系统,并且玩家获取所有排名频率高的可以使用。这种设计意味着,每次排名发生改变,系统都会重新排所有名次。

 

第3种方案,使用节点大小平衡二叉树(Size Balanced Tree,SBTree),我非常喜欢的一种平衡二叉树。因为涉及到排序,所以平衡因子Size正适合。我们的权重还是(经验,玩家id),类似于第2中方案,它也需要一个字典来保存玩家id和玩家经验值的映射。因为是二叉树,所以删除和添加节点复杂度为O(logN)。

  • 如果经验值改变的玩家在字典中存在,则删除掉此玩家在树中的旧节点;如果不存在,则比较最小权重节点。
  • 如果需要修改,则只是将新的权重值放入树中就行了,复杂度为O(LogN)。
  • 获取所有的排名意味着树的遍历,复杂度大概为O(NlogN)。当然我们可以优化这一块,可以将遍历后的结果保存在一个数组中,这样,当我们的排名没有发生变更的时候,玩家可以从缓存中读取所有排名。
  • 对于精准玩家的名次,我们通过权重查找节点时,将其路径中的左子树Size累加就可以了(参加SBTree中的实现),复杂度为O(LogN)。
  • 随机获取第k名的玩家,参考Python中的__getitem__操作,复杂度为O(LogN),如果有优化处理,在缓存有效的情况下,可以从缓存中获取

这种设计,对于数值变化频率高,但是玩家查看所有排名频率少的系统非常适用。这种设计意味着,每次排名发生改变,我们只是维护它的变更信息。当玩家查看所有排名的时候,而此时缓存失效,则对树进行遍历。它的实质是将方案2中的排序拆为了两步。

实现

这里我只提供第3种方案的Python实现。

使用的SBTree是我的博客http://blog.youkuaiyun.com/h010332722/article/details/19981165中涉及的。

这里我没有使用缓存来优化

#!/usr/bin/env python
# -*- coding:UTF-8 -*-
# Author: Random
#===============================================================================
# 注释
#===============================================================================
from Tree import SBTree
import random

class SortTree(object):
	def __init__(self, uLimit):
		self.sbTree = SBTree.SBTTree()
		self.sortDict = {}
		self.uLimit = uLimit
	
	def Trigger(self, key, value, info = None):
		'''
		参与排名
		@param key:
		@param value:
		@param info:
		'''
		item = self.sortDict.get(key)
		if item:
			#更新操作
			oldValue = item[0]
			if value <= oldValue:
				return
			oldkey2 = (oldValue, key)
			newKey2 = (value, key)
			if self.sbTree.Delete(oldkey2):
				self.sbTree.Insert(newKey2)
		else:
			if len(self.sortDict) >= self.uLimit:
			# 插入操作
			# 查找到最小的值
				minItem = self.sbTree.Head()
				if minItem:
					minValue = minItem.key[0]
					if value <= minValue:
						return
					minKey = minItem.key[1]
					del self.sortDict[minKey]
					self.sbTree.PopHead()
			newKey2 = (value, key)
			self.sbTree.Insert(newKey2)
		self.sortDict[key] = (value, info)
		
	def GetRank(self, key):
		item = self.sortDict.get(key)
		if not item:
			return 0
		value = item[0]
		mk = (value, key)
		return self.sbTree.Rank(mk)
	
	def Print(self):
		ls = []
		for key, item in self.sortDict.iteritems():
			value = item[0]
			mk = (value, key)
			ls.append((self.sbTree.LastRank(mk), value, key))
		ls.sort(key = lambda k : k[0])
		print ls
			
if __name__ == "__main__":
	s = SortTree(10)
	for i in xrange(1, 100):
		key = random.randint(1, 30)
		value = random.randint(1, 300)
		print "---------------------------------"
		s.Trigger(key, value, "%s"%(i))
		print "TRIGGER", key, value, len(s.sortDict)
		s.sbTree.Print()
		s.Print()
		


结果截取一部分:

---------------------------------
TRIGGER 14 236 10
		(268, 11), 2
			(269, 8), 1
	(279, 14), 6
			(279, 30), 1
		(283, 26), 3
			(287, 13), 1
 (290, 16), 10
		(291, 2), 1
	(292, 3), 3
		(297, 1), 1
[(1, 297, 1), (2, 292, 3), (3, 291, 2), (4, 290, 16), (5, 287, 13), (6, 283, 26), (7, 279, 30), (8, 279, 14), (9, 269, 8), (10, 268, 11)]
---------------------------------
TRIGGER 1 39 10
		(268, 11), 2
			(269, 8), 1
	(279, 14), 6
			(279, 30), 1
		(283, 26), 3
			(287, 13), 1
 (290, 16), 10
		(291, 2), 1
	(292, 3), 3
		(297, 1), 1
[(1, 297, 1), (2, 292, 3), (3, 291, 2), (4, 290, 16), (5, 287, 13), (6, 283, 26), (7, 279, 30), (8, 279, 14), (9, 269, 8), (10, 268, 11)]
---------------------------------

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值