現在我們來玩一個猜數的游戲,假設有一個人要我們猜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; }
