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排序。

    ///   <summary>
    
///  IOrdered 参与排行榜排序的对象必须实现的接口。
    
///   </summary>
    
///   <typeparam name="TOrderedObj"> 参与排行榜排序的对象的类型 </typeparam>
     public   interface  IOrdered < TOrderedObj >
    {
        
bool  IsTopThan(TOrderedObj other);
    }

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

      接下来是TopNOrderedContainer实现的源码:

    ///   <summary>
    
///  TopNOrderedContainer 用于始终保持排行榜前N名的Object。该实现是线程安全的。
    
///  zhuweisky 2009.05.23
    
///   </summary>
    
///   <typeparam name="TID"> 被排名的对象的标志类型 </typeparam>
    
///   <typeparam name="TObj"> 被排名的对象类型 </typeparam>
     public   class  TopNOrderedContainer < TObj >   where  TObj : IOrdered < TObj >
    {
        
private  TObj[] orderedArray  =   null ;
        
private   int  validObjCount  =   0 ;
        
private  SmartRWLocker smartRWLocker  =   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( " The value of TopNumber must greater than 0  " );
            }

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

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

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

        
#region  Add
        
public   void  Add(TObj obj)
        {
            
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(TObj obj)
        {
            
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)
        {
            TObj obj 
=   this .orderedArray[posIndex];
            
for  ( int  index  =  posIndex; index  >   0 ; index -- )
            {
                
if  (obj.IsTopThan( this .orderedArray[index  -   1 ]))
                {
                    TObj temp 
=   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排序直接实现为一个静态方法,如:

      public   static  TObj[] GetTopN < TObj > (IList < TObj >  list)  where  TObj : IOrdered < TObj >

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

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

    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(UserData other)
        {
            
return   this .Score  >  other.Score;
        }

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

 

        private   void  button4_Click( object  sender, EventArgs e)
        {
            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 ));
            }

            Stopwatch stopwatch 
=   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(UserData x, UserData y)
        {
            
return  (y.Score  -  x.Score);
        }
        
#endregion

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

 

 

你可能感兴趣的:(TopN算法与排行榜)