雁过请留痕...
代码改变世界

一个关于字典查找引发的思考——BinarySearch

2012-08-02 12:20  xiashengwang  阅读(1983)  评论(0编辑  收藏  举报

  最近在一个e文网站的提问区看到一个非常有意思的问题,内容如下:

有几组信号范围(range),每个范围对应一个位置(Position):

Signal Strenth             Position
0-9                            1
10-19                          2
20-29                          3

要求很简单,就是比如输入一个15,找到它的Position为2。但要求不能用IF语句进行简单的判定,最好用类似于字典的查找。

看见这个问题,你脑海中有什么样的方案?Dictionary?

在实际工作中还是经常遇到类似的要求。比如:0~5级为小菜,6~10级为大鸟,11~15为牛人,16~20为专家,20~30为大师。

我们来看看这些回答者的思路吧,往往我们能从中受到一定的启发。

1 ,方法一

int position = signalStrength / 10 + 1;

仅针对当前这个问题,用一个小巧的函数,比类似Dictionary会高效太多。

2,方法二

Dictionary<int, int> values;

values = new Dictionary<int, int>();

values[0] = 1;
values[1] = 1;
...
values[29] = 3;

然后再查找这个字典:

Console.WriteLine(values[15].ToString());

3,方法三

两个Dictionary嵌套,后面一个Dictionary是范围

PS:这种方式感觉很别扭,还要额外去遍历Dictionary才能找到对应的key

dictionary<string,dictionary<int,int>>

4,方法四

定义一个简单的List,每一个元素是一个自定义范围类

public class Controller
{
   List m_positions = new List();

   public void LoadPositions()
   {
      m_positions.Add(new Range(0, 9));
      m_positions.Add(new Range(10, 19));
      m_positions.Add(new Range(20, 29));
   }

   public int GetPosition (int signal)
   {
      Range range = m_positions.Single(a => IsBetween(signal, a.Min, a.Max));

      return m_positions.IndexOf(range);
   }

   private static bool IsBetween (int target, int min, int max)
   {
      return target>=min && target<=max;
   }
}
public class Range
{
   public Range(int min, int max)
   {
      this.Min = min;
      this.Max = max;
   }

   public int Min
   {
      get;
      private set;
   }

   public int Max
   {
      get;
      private set;
   }
}

PS:这种方式可读性很好,但查询效率会很低。

5,方法五

PS:这个效率很高,用了一个折半查找。

class SignalStrengthPositionMapper
{
    private static readonly int[] signalStrength = { Int32.MinValue, 0, 5, 11, 15, 20, 27, 35 };
    public static int GetPosition(int strength)
    {
        return StrengthSearch(0, signalStrength.Length, strength);
    }

    // modified binary search
    private static int StrengthSearch(int start, int end, int strength)
    {
        int mid = 0;
        while (start <= end)
        {
            mid = (start + end) / 2;

            if (strength >= signalStrength[mid])         // lower bound check
            {
                start = mid + 1;
                if (strength < signalStrength[start])    // upper bound check
                    return mid;
            }
            else if (strength < signalStrength[mid])     // upper bound check
            {
                end = mid - 1;
                if (strength >= signalStrength[end])     // lower bound check
                    return mid;
            }
        }
        return 0;
    }
}

6,方法六

这个方法,是提问者采纳的方法。

// Definition of ranges
int[] ranges = new int[] { 9, 19, 29 };

// Lookup
int position = Array.BinarySearch(ranges, 15);
if (position < 0)
    position = ~position;

// Definition of range names
string[] names = new string[] { "home", "street", "city", "far away" };

Console.WriteLine("Position is: {0}", names[position]);

该方法采用了FCL提供的BinarySearch方法,它实际上是一个二分查找算法,要求被查找的序列是有序的。如果不是有序,可事先对它进行排序后再查找。

7,发挥你的才能,欢迎提出不同的观点。

附:关于BinarySearch方法

原型如下

public static int BinarySearch<T>(
    T[] array,
    T value,
    IComparer<T> comparer
)

返回值(摘自MSDN):

  • 如果找到 value,则为指定 array 中的指定 value 的索引。 
  • 如果找不到 value 且 value 小于 array 中的一个或多个元素,则为一个负数,该负数是大于 value 的第一个元素的索引的按位求补。 如果找不到 value 且 value 大于 array 中的任何元素,则为一个负数,该负数是(最后一个元素的索引加 1)的按位求补。

PS:对于这里的按位求补(bitwise complement ),和我们传统意义上的求补码不是一回事,它实际就是对索引值取反,C#中就是~运算。所以,如果我们判断返回的是一个负数,就对这个负数再求一次反,得到的就是原索引值。

看看下面的测试代码:

            int[] range = { 9, 19, 29 };

            int index1 = Array.BinarySearch(range, 7);
            int index2 = Array.BinarySearch(range, 9);
            int index3 = Array.BinarySearch(range, 15);
            int index4 = Array.BinarySearch(range, 30);
            if (index1 < 0) {
                 Console.WriteLine("index1 is negative:{0}", index1);
                 index1 = ~index1;
            }
            if (index2 < 0) { 
                 Console.WriteLine("index2 is negative:{0}", index2);
                 index2 = ~index2;
            }
            if (index3 < 0) { 
                 Console.WriteLine("index3 is negative:{0}", index3);
                 index3 = ~index3;
            }
            if (index4 < 0) { 
                Console.WriteLine("index4 is negative:{0}", index4);
                index4 = ~index4; 
            }

            Console.WriteLine("index1 is {0}", index1);
            Console.WriteLine("index2 is {0}", index2);
            Console.WriteLine("index3 is {0}", index3);
            Console.WriteLine("index4 is {0}", index4);

输出结果:

index1 is negative:-1
index3 is negative:-2
index4 is negative:-4
index1 is 0
index2 is 0
index3 is 1
index4 is 3