TopN算法与排行榜

在系统中,我们经常会遇到这样的需求:将大量(比如几十万、甚至上百万)的对象进行排序,然后只需要取出最Top的前N名作为排行榜的数据,这即是一个TopN算法。常见的解决方案有三种:

(1)直接使用List的Sort方法进行处理。

(2)使用排序二叉树进行排序,然后取出前N名。

(3)使用最大堆排序,然后取出前N名。

第一种方案的性能是最差的,后两种方案性能会好一些,但是还是不能满足我们的需求。最主要的原因在于使用二叉树和最大堆排序时,都是对所有的对象进行排序,而不是将代价花费在我们需要的少数的TopN上。为此,我自己实现了TopNOrderedContainer来解决这个问题。

思路是这样的,使用一个长度为N的数组,来存放最Top的N个对象,越Top的对象其在数组中的Index就越小。这样,每次加入一个对象时,就与Index最大的那个对象比较,如果比其更Top,则交换两个对象的位置。如果被交换的对象是数组中的最后一个对象(Index最大),则该对象会被抛弃。如此,可以保证容器中始终保持的都是最Top的N个对象。

接下来我们看具体的实现。

如果一个对象要参与TopN排行榜,则其必须实现IOrdered接口,表明其可以被Top排序。

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />--> /// <summary>
/// IOrdered参与排行榜排序的对象必须实现的接口。
/// </summary>
/// <typeparamname="TOrderedObj"> 参与排行榜排序的对象的类型 </typeparam>
public interface IOrdered < TOrderedObj >
{
bool IsTopThan(TOrderedObjother);
}

之所以使用泛型参数TOrderedObj,是为了避免派生类在实现IsTopThan方法时,需要将参数other进行向下转换。

接下来是TopNOrderedContainer实现的源码:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />--> /// <summary>
/// TopNOrderedContainer用于始终保持排行榜前N名的Object。该实现是线程安全的。
/// zhuweisky2009.05.23
/// </summary>
/// <typeparamname="TID"> 被排名的对象的标志类型 </typeparam>
/// <typeparamname="TObj"> 被排名的对象类型 </typeparam>
public class TopNOrderedContainer < TObj > where TObj:IOrdered < TObj >
{
private TObj[]orderedArray = null ;
private int validObjCount = 0 ;
private SmartRWLockersmartRWLocker = new SmartRWLocker();

#region TopNumber
private int topNumber = 10 ;
public int TopNumber
{
get { return topNumber;}
set {topNumber = value;}
}
#endregion

#region Ctor
public TopNOrderedContainer(){}
public TopNOrderedContainer( int _topNumber)
{
this .topNumber = _topNumber;
}
#endregion

#region Initialize
public void Initialize()
{
if ( this .topNumber < 1 )
{
throw new Exception( " ThevalueofTopNumbermustgreaterthan0 " );
}

this .orderedArray = new TObj[ this .topNumber];
}
#endregion

#region AddList
public void Add(IList < TObj > list)
{
if (list == null )
{
return ;
}

using ( this .smartRWLocker.Lock(AccessMode.Write))
{
foreach (TObjobj in list)
{
this .DoAdd(obj);
}
}
}
#endregion

#region Add
public void Add(TObjobj)
{
using ( this .smartRWLocker.Lock(AccessMode.Write))
{
this .DoAdd(obj);
}
}
#endregion

#region GetTopN
public TObj[]GetTopN()
{
using ( this .smartRWLocker.Lock(AccessMode.Read))
{
return (TObj[]) this .orderedArray.Clone();
}
}
#endregion

#region Private
#region DoAdd
private void DoAdd(TObjobj)
{
if (obj == null )
{
return ;
}

if ( this .validObjCount < this .topNumber)
{
this .orderedArray[ this .validObjCount] = obj;
this .Adjust( this .validObjCount);

++ this .validObjCount;
return ;
}

if ( this .orderedArray[ this .topNumber - 1 ].IsTopThan(obj))
{
return ;
}

this .orderedArray[ this .topNumber - 1 ] = obj;
this .Adjust( this .topNumber - 1 );
}
#endregion

#region Adjust
/// <summary>
/// Adjust调整posIndex处的对象到合适的位置。
/// 与相邻前一个对象比较,如果当前对象更加Top,则与前一个对象交换位置。
/// </summary>
private void Adjust( int posIndex)
{
TObjobj
= this .orderedArray[posIndex];
for ( int index = posIndex;index > 0 ;index -- )
{
if (obj.IsTopThan( this .orderedArray[index - 1 ]))
{
TObjtemp
= this .orderedArray[index - 1 ];
this .orderedArray[index - 1 ] = obj;
this .orderedArray[index] = temp;
}
else
{
break ;
}
}
}
#endregion
#endregion
}

源码面前毫无秘密。

但是有几点我还是需要说明一下:

(1)ESBasic.ObjectManagement.TopNOrderedContainer位于我的ESBasic.dll类库中,其实现时用到的SmartRWLocker是一个读写锁,也是ESBasic.dll类库中的一员。你可以从这里下载ESBasic.dll直接试用。

(2)为何不将TopN排序直接实现为一个静态方法,如:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />--> public static TObj[]GetTopN < TObj > (IList < TObj > list) where TObj:IOrdered < TObj >

如果要是这样实现,那我们就没有办法继续动态的Add新的TObj对象进来,如果要达到这样的目的,就只有构造新的list,再次调用static GetTopN方法,如此会重复做一些工作。

最后,我们来测试一下TopNOrderedContainer与List.Sort方法的性能比较,测试的对象数目为500000个,取出Top20。测试代码如下:

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />--> public class UserData:IOrdered < UserData >
{
#region UserID
private string userID;
public string UserID
{
get { return userID;}
set {userID = value;}
}
#endregion

#region Score
private int score;
public int Score
{
get { return score;}
set {score = value;}
}
#endregion

public UserData( string _userID, int _score)
{
this .userID = _userID;
this .score = _score;
}

#region IOrdered<string>成员

public bool IsTopThan(UserDataother)
{
return this .Score > other.Score;
}

public override string ToString()
{
return this .score.ToString();
}
#endregion
}

<!--<br /><br />Code highlighting produced by Actipro CodeHighlighter (freeware)<br />http://www.CodeHighlighter.com/<br /><br />--> private void button4_Click( object sender,EventArgse)
{
List
< UserData > list = new List < UserData > ();
for ( int i = 0 ;i < 500000 ;i ++ )
{
list.Add(
new UserData( " User " + i.ToString(),i * i * i - 3 * i * i + 4 * i + 8 ));
}

List
< UserData > list2 = new List < UserData > ();
for ( int i = 0 ;i < 500000 ;i ++ )
{
list2.Add(
new UserData( " User " + i.ToString(),i * i * i - 3 * i * i + 4 * i + 8 ));
}

Stopwatchstopwatch
= new Stopwatch();
stopwatch.Start();
list.Sort(
this );
stopwatch.Stop();
long ms1 = stopwatch.ElapsedMilliseconds;

stopwatch.Reset();
stopwatch.Start();
TopNOrderedContainer
< UserData > container = new TopNOrderedContainer < UserData > ( 20 );
container.Initialize();
container.Add(list2);
UserData[]res
= container.GetTopN();
stopwatch.Stop();
long ms2 = stopwatch.ElapsedMilliseconds;
}

#region IComparer<UserData>成员
public int Compare(UserDatax,UserDatay)
{
return (y.Score - x.Score);
}
#endregion

测试的结果显示,使用List.Sort方法需要1287ms,而TopNOrderedContainer只花了78ms。

你可能感兴趣的:(top)