扩展方法之二分查找


扩展方法之二分查找

版本:0.1

最后修改:2012-08-08

撰写:李现民


近期项目策划案调整,要求程序按音乐时间及位置等条件迅速定位当前游戏角色正在使用的动作,因为查询会非常频繁,因此决定使用二分查找。


C#类库中有二分查找,分散于Array、List、ArrayList等类中,但接口不太另人满意。对简单的整数数组还好,可以直接使用,但对于复杂的查询,默认的类库使用起来就会比较复杂,比如:

 

class Action
{
     public  string name;
     public  int time;
     public  int position;
}

class TimeComparer : IComparer<Action>
{
     public  int Compare(Action lhs, Action rhs)
    {
         if ( null == lhs)
        {
             return - 1;
        }
         else  if ( null == rhs)
        {
             return  1;
        }

         return lhs.time.CompareTo(rhs.time);
    }
}

class Program
{
     static  int BinarySearchByTime(Action[] actions,  int time)
    {
         var testAction =  new Action() { time = time };
         var index = Array.BinarySearch(actions, testAction,  new TimeComparer());
         if (index <  0)
        {
             return ~index -  1;
        }

         return index;
    }

     static  void Main( string[] args)
    {
         var actions =  new Action[] {
             new Action() { name =  " action1 ", time =  0, position =  0 },
             new Action() { name =  " action2 ", time =  2, position =  60 },
             new Action() { name=  " action3 ", time=  4, position=  90}};

         var time =  3;
         var index = BinarySearchByTime(actions, time);

        Console.ReadKey();
    }

 

示例中给出了一个类,代表游戏中角色的动作,actions数组存储了按先后顺序在每个时间点和位置点需要播放什么动作,要求查询当时间time=4的时刻正在播放的是什么动作。


由代码实现来看,默认类库至少有两个缺点:


  1. 需要自己书写对应的比较器,示例中需要按时间查询,因此实现为TimeComparer类,如果进一步需要按位置查询的话,则需要实现PositionComparer类,这在比较方法多样化的情况下仅比较器的代码量就会非常大。

  2. 比较器要求传入的两个参数类型是相同的,示例中为Action 类,因此,即使我们只需要按时间比较,也不得不构造一个临时的testAction对象。这对我们需要实现的功能而言不应该是必须的,况且有些情况下由于Action类构造函数的限制我们可能得通过非常复杂的方法才能构造出这样一个对象。


另外,由于BinarySearch()分散于多个类中,而代码的使用方式又不太一至(比如Array类中的是静态方法,而是List类中的是成员方法),在一定程度上会造成混淆。


基于以上原因,我希望能够通过某种方式解决以上问题,使得代码的客户能通过简单一致的接口调用二分查找算法。新方案的代码实现如下:


     public  static  int BinarySearch<T>( this IList<T> list,  int key, Func<T,  int> extract)
    {
         if ( null == list)
        {
             throw  new ArgumentNullException( " list is null ");
        }

         int count = list.Count;
         int i = - 1;
         int j = count;
         while (i +  1 != j)
        {
             int mid = i + (j - i >>  1);
             if (extract(list[mid]) < key)
            {
                i = mid;
            }
             else
            {
                j = mid;
            }
        }

         if (j == count || extract(list[j]) != key)
        {
            j = ~j;
        }

         return j;
    }

     static  void Main( string[] args)
    {
         var actions =  new Action[] {
         new Action() { name =  " action1 ", time =  0, position =  0 },
         new Action() { name =  " action2 ", time =  2, position =  60 },
         new Action() { name=  " action3 ", time=  4, position=  90}};

         var time =  3;
         var index = actions.BinarySearch(time, item => item.time);

        Console.ReadKey();

    } 


首先,新的BinarySearch()是一个扩展方法,它的扩展对象是IList<T>接口,无论是Array还是List类都实现了该接口,因此它们可以直接调用新的BinarySearch()方法。示例中actions数组直接调用了BinarySearch()方法。


其次,新的BinarySearch()方法接受一个委托作为回调方法,因此可以使用非常简洁的方式避免构造原始方案中的比较器类。示例中提取的是item.time数据。


再者,新的BinarySearch()方法中用于比较的关键词(命名为key)现在可以直接传入了,而不再需要构造一个临时了testAction对象了。


除此之外,新的BinarySearch()方法返回值的含义与C#内置二分查找算法返回值的含义相同:如果找到了,则返回对应的索引;如果没找到,则返回一个小于0的索引补数,再次求补后的得到的原始索引值则是关键词(命名为key)正确插入到该有序数列中时它应该在的位置。


关于算法实现的详细描述,请参考《编程珠玑 第二版》第9.2节“主要的外科手术—二分查找”一文。


另外,还有一些实现相关的细节问题与改进方案,需要在这里提一下:


第一个问题是:为什么BinarySearch()中的key不使用泛型?这是因为代码实现中用到了两个操作,两个对象之间的小于比较(<)和不等比较(!=),这对整数而言是比较相当然的,但对一般的数据类型则要求用对应的接口去实现。另外,对于浮点数float,不等比较(!=)是需要按比较精度进行处理的,因此也需要单写。出于简单与速度的考虑,这里只使用了int类型。最后,int与float其实应该能覆盖大部分情况了,对吧?


第二个问题是:BinarySearch()扩展的是IList<T>,但对Array而言,从速度上有优化的余地。前文提到,Array实现了IList<T>,这没错,但是,如果通过IList<T>接口调用数组的list.Count与list[mid]的话,实际调用的是类实例的方法,分别是:

callvirt instance int32 class [mscorlib]System.Collections.Generic.ICollection`1<!!T>::get_Count()

callvirt instance !0 class [mscorlib]System.Collections.Generic.IList`1<!!T>::get_Item(int32)


对数组来说,其实是有对应的汇编指令完成这两个操作的,因此,如果对速度有需求的话,为数组单独编写对应的BinarySearch()是有回报的。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM