数据搜索算法
排序和搜索是数据结构和算法学习中的两个最基本的操作。关于排序,我在上一篇已经做了比较详细的介绍,请参考
http://www.cnblogs.com/chenxizhang/archive/2009/04/22/1441209.html
这一篇我们来关注一下搜索。我们同样把目光放在Array这个最基础的数据类型上面。我们从几个实例来讲解怎么利用.NET的内部机制实现检索
1. Array.Exists
这个方法是判断是否在指定数组中存在某个成员。让我们来看看这个方法的定义
该方法返回一个bool值,这很好理解。它的第一个参数是一个Array,这也很好理解。(就是我们要搜索的数据源),而第二个参数是一个所谓的Predicate类型。我们展开来看一下这个东西吧
这是一个委托。也就是说它是一个指针,需要我们告诉它如何判断数据存在的依据。那么怎么给出这个参数呢?
传统的做法,我们准备一个方法,这个方法是满足该委托的签名的(有一个参数,而且返回一个bool),然后通过该委托去调用该方法。(假设我们这里判断存在的依据是该数字等于2)
static bool MatchInt(int input)
{
return input == 2;
}
然后,在Exists方法中使用委托来调用这个MatchInt方法去进行判断。(有两种写法)
static void Main(string[] args)
{
int[] numbers = new int[] { 3, 7, 2, 4, 10, 1 };
Console.WriteLine(Array.Exists<int>(numbers, new Predicate<int>(MatchInt)));//这是原始写法
Console.WriteLine(Array.Exists<int>(numbers, MatchInt));//这是上面一句代码的简写
Console.Read();
}
从上面的代码可以看出,为了做这个判断,我们专门写了一个方法,这在很多时候会增加代码阅读的麻烦,因为阅读者需要从一个方法跳到另外一个方法,不断反复。
所以,在C# 2.0中,提出了匿名方法的概念,也就是对于此类不太需要单独封装的地方,可以直接将方法体合并在委托声明中。也就是说,没有必要单独写MatchInt这个方法。
Console.WriteLine(Array.Exists<int>(numbers, delegate(int i) { return i == 2; }));
这一句代码就可以完成所有事情了。其实也很直观。
而在最新的C# 3.0中,针对这这种问题,又有了改进,就是所谓的Lambda表达式,大家来看一下代码是如何写的
Console.WriteLine(Array.Exists<int>(numbers, i => i == 2));
你可能一下子还不理解Lambda,但只要大致看一下C# 3.0的新语法例子,其实还是比较通俗的。
如果有兴趣的朋友,可以通过IL代码看到,第二种方式和第三种方式其实是一模一样的。匿名方法和Lambda是语言之上的改进,编译的结果还是需要通过delegate来实现的。你也可以说,它们与第一种没有根本区别。
但在事实上,他们确实更加直观和简洁。
2. Array.Find, Array.FindLast
如果我们需要搜索一个数组中的某个元素,而不光是判断它是否存在。我们大致的写法如下
Console.WriteLine(Array.Find<int>(numbers, i => i == 2));
注意,如果找到了,则返回2这个数值。Find方法是找到一个即停止。而如果要找最后一个匹配的值,就需要用FindLast
3. Array.FindAll
这个方法是搜索所有匹配的数据,返回一个数组。
int[] foundnumbers = Array.FindAll<int>(numbers, i => i % 2 == 0);//搜索所有的偶数
foreach (var item in foundnumbers)
{
Console.WriteLine(item);
}
有意思的是,上面的代码可以简写为下面一句代码
Array.ForEach<int>(Array.FindAll<int>(numbers, i => i % 2 == 0), i => Console.WriteLine(i));
4. Array.FindIndex,Array.FindLastIndex
这个方法是检索相应的值在数组中的索引号
Console.WriteLine(Array.FindIndex<int>(numbers, i => i == 1));
Console.WriteLine(Array.FindLastIndex<int>(numbers, i => i == 1));
5. Array.BinarySearch
上面的Find方法,基本上都是基于顺序的。也就是,如果某个数字很不凑巧在最后面,那么搜索程序就不得不将每个数字都检查一次,比较他们。这样,在很多时候效率是不够高的。当然,如果数据本身没有规律,是随机分布的,这也是无法避免的。
如果说数据本身有顺序,那么就可以利用二进制搜索来提高速度。二进制搜索其实是一个折半搜索算法。也就是说,既然数据本身有顺序,就可以不要一个一个比较,而是可以在某个范围内比较
从这个原理可以知道,二进制搜索是依赖数据本身排序的。事实上,如果数据没有排序,则该方法也不会出错,但是会返回一个负数或者一些奇怪的结果。
我们用例子来比较一下二进制搜索和顺序搜索的差别
static void Main(string[] args)
{
//准备一个数组,随机填充10000000个数字
int[] numbers = new int[10000000];
Random rnd = new Random();
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] = rnd.Next(10000000);
}
//采用顺序搜索的方式,查找里面100这个数值(如果运气比较好,正好有100的话)
Stopwatch watch = Stopwatch.StartNew();
Console.WriteLine(Array.Find<int>(numbers, i => i == 100));
watch.Stop();
Console.WriteLine("顺序搜索使用的时间为:{0}毫秒", watch.ElapsedMilliseconds);
//采用二进制搜索的方式,查找里面100这个数值
watch.Reset();
watch.Start();
Array.Sort<int>(numbers);//先排序
watch.Stop();
Console.WriteLine("排序的时间:{0}毫秒", watch.ElapsedMilliseconds);
watch.Reset();
watch.Start();
Console.WriteLine("该数字所在的索引号是:{0}",Array.BinarySearch<int>(numbers, 100));
watch.Stop();
Console.WriteLine("二进制搜索的时间:{0}毫秒", watch.ElapsedMilliseconds);
Console.Read();
}
我们发现,二进制搜索几乎不需要时间,这太神奇了。但是我们也发现排序是要花很长时间的。
当然,如果我们需要多次在一个数组中去频繁地检索,那么一次排序的代价相比较多次搜索而言,可能是微不足道的
另外,需要知道的是,BinarySearch的结果是一个索引号,而不是具体的数值