现在我们来玩一个猜数的游戏,假设有一个人要我们猜0-99之间的一个数。那么最好的方法就是从0-99的中间数49开始猜。如果要猜的数小于49,就猜24(0-48的中间数);如果要猜的数大于49,就猜74(50-99的中间数)。重复这个过程来缩小猜测的范围,直到猜出正确的数字。二分查找的工作方法类似于此。
二分查找操作的数据集是一个有序的数据集。开始时,先找出有序集合中间的那个元素。如果此元素比要查找的元素大,就接着在较小的一个半区进行查找;反之,如果此元素比要找的元素小,就在较大的一个半区进行查找。在每个更小的数据集中重复这个查找过程,直到找到要查找的元素或者数据集不能再分割。
二分查找能应用于任何类型的数据,只要能将这些数据按照某种规则进行排序。然而,正因为它依赖于一个有序的集合,这使得它在处理那些频繁插入和删除操作的数据集时不太高效。这是因为,对于插入和操作来说,为了保证查找过程正常进行,必须保证数据集始终有序。相对于查找来说,维护一个有序数据集的代价更高。此外,元素必须存储在连续的空间中。因此,当待搜索的集合是相对静态的数据集时,此时使用二分查找是最好的选择。
二分查找的接口定义
bisearch
int bisearch(void *sorted, void *target, int size, int esize, int (compare *)(const void *key1, const void *key2);
返回值:如果查找成功返回目标的索引值;否则返回-1。
描述:利用二分查找定位有序元素数组sorted中target。数组中的元素个数由size决定,每个元素的大小由esize决定。函数指针compare指向一个用户自定义的比较函数。如果key1大于key2,函数返回1,如果key1=key2,函数返回0,如果key1小于key2,函数返回-1。
复杂度:O(lg n),n为要查找的元素个数。
二分查找的实现与分析
二分查找法实质上是不断地将有序数据集进行对半分割,并检查每个分区的中间元素。在以下介绍的实现方法中,有序数据集存放在sorted中,sorted是一块连续的存储空间。参数target是要查找的数据。
此实现过程的实施是通过变量left和right控制一个循环来查找元素(其中left和right是正在查找的数据集的两个边界值)。首先,将left和right分别设置为0和size-1。在循环的每次迭代过程中,将middle设置为left和right之间区域的中间值。如果处于middle的元素比目标值小,将左索引值移动到middle后的一个元素的位置上。即下一组要搜索的区域是当前数据集的上半区。如果处于middle的元素比目标元素大,将右索引值移动到middle前一个元素的位置上。即下一组要搜索的区域是当前数据集的下半区。随着搜索的不断进行,left从左向右移,right从右向左移。一旦在middle处找到目标,查找将停止;如果没有找到目标,left和right将重合。下图显示了此过程。
二分查找的时间复杂度取决于查找过程中分区数可能的最大值。对于一个有n个元素的数据集来说,最多可以进行lg n次分区。对于二分查找,这表示最终可能在最坏的情况下执行的检查的次数:例如,在没有找到目标时。所以二分查找的时间复杂度为O(lg n)。
示例:二分查找的实现
#include <stdlib.h> #include <string.h> #include "search.h" /*bisearch 二分查找函数*/ int bisearch(void *sorted, const void *target, int size, int esize, int (*compare)(const void *key1, const void key2)) { int left, middle, right; /*初始化left和right为边界值*/ left = 0; right = size - 1; /*循环查找,直到左右两个边界重合*/ while(left<=right) { middle = (left + right) / 2; switch(compare(((char *)sorted + (esize * middle)),target)) { case -1: /*middle小于目标值*/ /*移动到middle的右半区查找*/ left = middle + 1; break; case 1: /*middle大于目标值*/ /*移动到middle的左半区查找*/ right = middle - 1; break; case 0: /*middle等于目标值*/ /*返回目标的索引值middle*/ return middle; } } /*目标未找到,返回-1*/ return -1; }
二分查找的例子:拼写检查器
拼写检查器在各种各样的文档中已经成为一种默认的工具。从计算机的角度来看,一个基本的拼写检查器的工作原理就是简单地将文本字符串中的单词与字典中的单词进行比对。字典包含可接受的单词集合。
在些介绍的一个例子,它包含一个函数spell。spell一次检查一个文本字符串中的单词。它接受三个参数:dictionary是一个可接受的有序字符串数组;size是字典中字符串的个数;word是将要被检查的单词。此函数调用bisearch在dictionary中查找word。如果单词找到,那么拼写正确。
函数spell的时间复杂度为O(lg n),与bisearch相同,其中n是dictionary中的单词的个数。检查整个文档的时间复杂度是O(m lg n),m是文档中要检查的单词个数。
示例:拼写检查器的头文件
/*spell.h*/ #ifdef SPELL_H #define SPELL_H /*定义字典单词的最大字节数*/ #define SPELL_SIZE 31 /*公共接口*/ int spell(char(*dictionary)[SPELL_SIZE],int size, const char *word); #endif // SPELL_H
示例:拼写检查器的实现
#include <string.h> #include "search.h" #include "spell.h" /*字义字符串比较函数*/ static int compare_str(const void *str1, const void *str2) { int retval; if((retval = strcmp((const char*)str1,(const char*)str2))>0) return 1; else if(retval<0) return -1; else return 0; } /*spell 函数*/ int spell(char(*dictionary)[SPELL_SIZE],int size,const void *word) { /*查找单词*/ if(bisearch(dictionary, word, size, SPELL_SIZE, compare_str)>=0) return 1; else return 0; }