好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

扩展方法之二分查找

扩展方法之二分查找

扩展方法之二分查找

撰写:李现民

 

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

 

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的时刻正在播放的是什么动作。

 

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

 

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

比较器要求传入的两个参数类型是相同的,示例中为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()是有回报的。

 

分类:  C#手记

撰写:李现民

 

涉及内容:unity3d, c#, string

前两日,有同事设计游戏连击特效的功能。因为这个功能在游戏环节中会调用的非常频繁,因此顺手点了开来,发现代码设计尚有优化的余地,于是便有了本次尝试。事后回想开来,发现涉及的东西还颇多,便想着记下来以飨后来者。

 

这是一份unity3d脚本程序,原始脚本文件MBStarCombo.cs代码如下:

 

using UnityEngine;

using System;

class MBStarCombo : MonoBehaviour

{

void Start()

{

_combo = transform.Find("Sprite (U-Combo)").gameObject;

_num1 = transform.Find("sprite_num1").gameObject;

_num2 = transform.Find("sprite_num2").gameObject;

_num3 = transform.Find("sprite_num3").gameObject;

_num4 = transform.Find("sprite_num4").gameObject;

Instance = this;

}

public void UpdateComboNum(int curCombo)

{

if(curCombo > 9999)

curCombo = 9999;

if(0 == curCombo)

{

_combo.SetActiveRecursively(false);

_num1.SetActiveRecursively(false);

_num2.SetActiveRecursively(false);

_num3.SetActiveRecursively(false);

_num4.SetActiveRecursively(false);

}

else if(curCombo > 0 && curCombo < 10)

{

_combo.SetActiveRecursively(true);

_num1.SetActiveRecursively(true);

_num2.SetActiveRecursively(false);

_num3.SetActiveRecursively(false);

_num4.SetActiveRecursively(false);

_num1.GetComponent<UISprite>().spriteName = "U-" + curCombo.ToString();

}

else if(curCombo >= 10 && curCombo < 100)

{

_combo.SetActiveRecursively(true);

_num1.SetActiveRecursively(true);

_num2.SetActiveRecursively(true);

_num3.SetActiveRecursively(false);

_num4.SetActiveRecursively(false);

int num1 = curCombo / 10;

int num2 = curCombo % 10;

_num1.GetComponent<UISprite>().spriteName = "U-" + num1.ToString();

_num2.GetComponent<UISprite>().spriteName = "U-" + num2.ToString();

}

else if(curCombo >= 100 && curCombo < 1000)

{

_combo.SetActiveRecursively(true);

_num1.SetActiveRecursively(true);

_num2.SetActiveRecursively(true);

_num3.SetActiveRecursively(true);

_num4.SetActiveRecursively(false);

int num1 = curCombo / 100;

int num2 = (curCombo - num1 * 100) / 10;

int num3 = curCombo % 10;

_num1.GetComponent<UISprite>().spriteName = "U-" + num1.ToString();

_num2.GetComponent<UISprite>().spriteName = "U-" + num2.ToString();

_num3.GetComponent<UISprite>().spriteName = "U-" + num3.ToString();

}

else if(curCombo >= 1000 && curCombo < 10000)

{

_combo.SetActiveRecursively(true);

_num1.SetActiveRecursively(true);

_num2.SetActiveRecursively(true);

_num3.SetActiveRecursively(true);

_num4.SetActiveRecursively(true);

int num1 = curCombo / 1000;

int num2 = (curCombo - num1 * 1000) / 100;

int num3 = (curCombo - num1 * 1000 - num2 * 100) / 10;

int num4 = curCombo % 10;

_num1.GetComponent<UISprite>().spriteName = "U-" + num1.ToString();

_num2.GetComponent<UISprite>().spriteName = "U-" + num2.ToString();

_num3.GetComponent<UISprite>().spriteName = "U-" + num3.ToString();

_num4.GetComponent<UISprite>().spriteName = "U-" + num3.ToString();

}

}

internal static MBStarCombo Instance { get; set; }

private GameObject _combo;

private GameObject _num1;

private GameObject _num2;

private GameObject _num3;

private GameObject _num4;

}

 

其中,Start()方法只会调用一次,而UpdateComboNum()将在游戏过程中被反复调用,因此后者将是代码优化的重点。我们将新代码保存至MBStarCombo2.cs文件中,同时进行调用,并使用Profiler比较改进的结果。

 

程序逻辑比较简单清晰:根据当前传入的分数,最大限制到9999,分情况将分数的各个数字取出来,并设置对应UISprite的纹理项为对应的分数数值。代码中用到了NGUI,因此只要简单的按名称设置就可以更改UISprite的纹理。

 

当前目所能及的地方包括:SetActiveRecursively()与GetComponent()。

 

首先是SetActiveRecursively(),这是一个开销比较大的调用,它会循环遍历gameObject的所有子对象并设置它们的active状态,看不到源代码实现,但估计是深度优先之类的搜索遍历。其实,在本次代码中,我们只需要简单的设置UISprite组件的enabled属性就可以实现隐藏或显示该组件的功能。

 

其次是GetComponent()。原始代码中,程序保存了_num1到_num4共4个gameObject的对象引用。程序频繁的调用了GetComponent()方法去获取其UISprite组件,这其实是一个开销比较大的操作。Unity3d开发文档的性能优化指南中有相关介绍,对频繁使用的组件应该缓存其引用。这不仅仅是指GetComponent()调用,包括transform, gameObject等内置变量如果需要频繁使用的话,同样应该通过定义一个私有变量缓存其引用,因为它们本质其实是类似的。

 

另外,对于if()调用中边界条件的判断,curCombo > 0,curCombo > =10等其实是没有必要的,可以直接去除之。

 

修改完成后的代码如下:

 

class MBStarCombo2 : MonoBehaviour

{

void Start()

{

_combo = transform.Find("Sprite (U-Combo)").gameObject.GetComponent<UISprite>();

_num1 = transform.Find("sprite_num1").gameObject.GetComponent<UISprite>();

_num2 = transform.Find("sprite_num2").gameObject.GetComponent<UISprite>();

_num3 = transform.Find("sprite_num3").gameObject.GetComponent<UISprite>();

_num4 = transform.Find("sprite_num4").gameObject.GetComponent<UISprite>();

Instance = this;

}

public void UpdateComboNum(int curCombo)

{

if(curCombo > 9999)

curCombo = 9999;

if(0 == curCombo)

{

_combo.enabled = false;

_num1.enabled = false;

_num2.enabled = false;

_num3.enabled = false;

_num4.enabled = false;

}

else if(curCombo < 10)

{

_combo.enabled = true;

_num1.enabled = true;

_num2.enabled = false;

_num3.enabled = false;

_num4.enabled = false;

_num1.spriteName = "U-" + curCombo.ToString();

}

else if(curCombo < 100)

{

_combo.enabled = true;

_num1.enabled = true;

_num2.enabled = true;

_num3.enabled = false;

_num4.enabled = false;

int num1 = curCombo / 10;

int num2 = curCombo % 10;

_num1.spriteName = "U-" + num1.ToString();

_num2.spriteName = "U-" + num2.ToString();

}

else if(curCombo < 1000)

{

_combo.enabled = true;

_num1.enabled = true;

_num2.enabled = true;

_num3.enabled = true;

_num4.enabled = false;

int num1 = curCombo / 100;

int num2 = (curCombo - num1 * 100) / 10;

int num3 = curCombo % 10;

_num1.spriteName = "U-" + num1.ToString();

_num2.spriteName = "U-" + num2.ToString();

_num3.spriteName = "U-" + num3.ToString();

}

else if(curCombo < 10000)

{

_combo.enabled = true;

_num1.enabled = true;

_num2.enabled = true;

_num3.enabled = true;

_num4.enabled = true;

int num1 = curCombo / 1000;

int num2 = (curCombo - num1 * 1000) / 100;

int num3 = (curCombo - num1 * 1000 - num2 * 100) / 10;

int num4 = curCombo % 10;

_num1.spriteName = "U-" + num1.ToString();

_num2.spriteName = "U-" + num2.ToString();

_num3.spriteName = "U-" + num3.ToString();

_num4.spriteName = "U-" + num3.ToString();

}

}

internal static MBStarCombo2 Instance { get; set; }

private UISprite _combo;

private UISprite _num1;

private UISprite _num2;

private UISprite _num3;

private UISprite _num4;

}

 

通过Profiler观察,可以发现优化后的代码占用CPU大概少一个百分点,如图:

 

优化力度没有我想象中的那么大 -______-

 

进一步观察,我们发现如下代码:

int num1 = curCombo / 1000;

int num2 = (curCombo - num1 * 1000) / 100;

int num3 = (curCombo - num1 * 1000 - num2 * 100) / 10;

int num4 = curCombo % 10;

_num1.spriteName = "U-" + num1.ToString();

_num2.spriteName = "U-" + num2.ToString();

_num3.spriteName = "U-" + num3.ToString();

_num4.spriteName = "U-" + num3.ToString();

 

其实就是获取了连击分数的各位数值,如果能想办法把它们去掉,至少在代码复杂性上会减少很多。于是,我想到了其实c#的string其实是有索引器的,于是它们可以改写为:

var text = curCombo.ToString();

_num1.spriteName = "U-" + text[0];

_num2.spriteName = "U-" + text[1];

_num3.spriteName = "U-" + text[2];

_num4.spriteName = "U-" + text[3];

 

进一步的,很容易发现,修正_numX的个数与text的长度其实密切相关,只要将_numX改写为一个数组,可以很容易的统一提取成一个循环,如下:

if (curCombo > 0)

{

var text = curCombo.ToString();

var length = text.Length;

for (int i = 0; i < length; ++i)

{

_nums[i].spriteName = "U-" + text[i];

}

}

 

这样,MBStarCombo2.cs的所有代码还包括:

class MBStarCombo2 : MonoBehaviour

{

void Start()

{

_combo = transform.Find("Sprite (U-Combo)").gameObject.GetComponent<UISprite>();

_num1 = transform.Find("sprite_num1").gameObject.GetComponent<UISprite>();

_num2 = transform.Find("sprite_num2").gameObject.GetComponent<UISprite>();

_num3 = transform.Find("sprite_num3").gameObject.GetComponent<UISprite>();

_num4 = transform.Find("sprite_num4").gameObject.GetComponent<UISprite>();

_nums = new UISprite[] { _num1, _num2, _num3, _num4};

Instance = this;

}

public void UpdateComboNum(int curCombo)

{

if (curCombo > 9999)

{

curCombo = 9999;

}

if(0 == curCombo)

{

_combo.enabled = false;

_num1.enabled = false;

_num2.enabled = false;

_num3.enabled = false;

_num4.enabled = false;

}

else if(curCombo < 10)

{

_combo.enabled = true;

_num1.enabled = true;

_num2.enabled = false;

_num3.enabled = false;

_num4.enabled = false;

}

else if(curCombo < 100)

{

_combo.enabled = true;

_num1.enabled = true;

_num2.enabled = true;

_num3.enabled = false;

_num4.enabled = false;

}

else if(curCombo < 1000)

{

_combo.enabled = true;

_num1.enabled = true;

_num2.enabled = true;

_num3.enabled = true;

_num4.enabled = false;

}

else if(curCombo < 10000)

{

_combo.enabled = true;

_num1.enabled = true;

_num2.enabled = true;

_num3.enabled = true;

_num4.enabled = true;

}

if (curCombo > 0)

{

var text = curCombo.ToString();

var length = text.Length;

for (int i = 0; i < length; ++i)

{

_nums[i].spriteName = "U-" + text[i];

}

}

}

internal static MBStarCombo2 Instance { get; set; }

private UISprite _combo;

private UISprite _num1;

private UISprite _num2;

private UISprite _num3;

private UISprite _num4;

private UISprite[] _nums;

}

 

此时,_combo与_numX的设置规律已经比较明显了,可以统一按text的长度修改,再稍微整理一下,去除一些冗余代码可以得到如下结果:

class MBStarCombo2 : MonoBehaviour

{

void Start()

{

_combo = GetSprite("Sprite (U-Combo)");

_nums = new UISprite[] { GetSprite("sprite_num1"), GetSprite("sprite_num2"), GetSprite("sprite_num3"), GetSprite("sprite_num4") };

Instance = this;

}

UISprite GetSprite(string name)

{

return transform.Find(name).gameObject.GetComponent<UISprite>();

}

public void UpdateComboNum(int curCombo)

{

if (curCombo > 9999)

{

curCombo = 9999;

}

if(0 == curCombo)

{

_combo.enabled = false;

Array.Clear(_nums, 0, _nums.Length);

}

else

{

var text = curCombo.ToString();

var length = text.Length;

_combo.enabled = true;

_nums[0].enabled = length >= 1;

_nums[1].enabled = length >= 2;

_nums[2].enabled = length >= 3;

_nums[3].enabled = length >= 4;

for (int i = 0; i < length; ++i)

{

_nums[i].spriteName = "U-" + text[i];

}

}

}

internal static MBStarCombo2 Instance { get; set; }

private UISprite _combo;

private UISprite[] _nums;

}

 

相对于原始代码,这简洁了不少,并且相对于第一版的优化效率也略有提升:

 

进一步对Profiler进行观察,可以发现String.Concat()与Int32.ToString()其实占了大头,如果能够把它们优化去掉,那世界一定会变得美好的多。因此考虑设计一个字符数组,直接对字符数组的数据进行修改而不是做Concat()操作:

for (int i = 0; i < length; ++i)

{

_spriteName[2] = text[i];

_nums[i].spriteName = new string(_spriteName);

}

 

从Profiler的结果来看,新代码的CPU占用率已经不足原始代码的一半:

 

然而,正如您所看到的那样,最大的优化竟然来自于对字符串操作的优化,这实在另人有些意外。不过仔细想想也有些道理,前一版中的”U-”+num.ToString()至少涉及到至少2次内存分配:一次给num.ToString(),一次给连接后的结果字符串。而使用字符串数组则仅仅用到一次内存copy,显然会快一些。

 

优化后的通篇代码如下:

 

class MBStarCombo : MonoBehaviour

{

void Start()

{

_combo = _GetSprite("Sprite (U-Combo)");

_nums = new UISprite[] { _GetSprite("sprite_num1"), _GetSprite("sprite_num2"), _GetSprite("sprite_num3"), _GetSprite("sprite_num4") };

Instance= this;

}

UISprite _GetSprite(string name)

{

return transform.Find(name).gameObject.GetComponent<UISprite>();

}

public void UpdateComboNum(int curCombo)

{

if (curCombo > 9999)

{

curCombo = 9999;

}

var isComboEnabled = curCombo > 0;

_combo.enabled = isComboEnabled;

if(isComboEnabled)

{

var text = curCombo.ToString();

var length = text.Length;

for(int i= 0; i < _nums.Length; ++i)

{

_nums[i].enabled = length > i;

}

for (int i = 0; i < length; ++i)

{

_spriteName[2] = text[i];

_nums[i].spriteName = new string(_spriteName);

}

}

else

{

Array.ForEach(_nums, num=>num.enabled= false);

}

}

internal static MBStarCombo Instance { get; set; }

private UISprite _combo;

private UISprite[] _nums;

private char[] _spriteName = new char[]{'U', '-', '\0'};

}

 

 

 

 

 

 

分类:  Unity3d ,  C#手记

作者: Leo_wl

    

出处: http://www.cnblogs.com/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于扩展方法之二分查找的详细内容...

  阅读:48次