轉載http://www.cppblog.com/converse/archive/2009/10/05/97905.html
二分查找算法基本思想
二分查找算法的前置條件是,一個已經排序好的序列(在本篇文章中為了說明問題的方便,假設這個序列是升序排列的),這樣在查找所要查找的元素時,首先與序列中間的元素進行比較,如果大於這個元素,就在當前序列的后半部分繼續查找,如果小於這個元素,就在當前序列的前半部分繼續查找,直到找到相同的元素,或者所查找的序列范圍為空為止.
用偽代碼來表示, 二分查找算法大致是這個樣子的:
1 left = 0, right = n -1 2 while (left <= right) 3 mid = (left + right) / 2 4 case 5 x[mid] < t: left = mid + 1; 6 x[mid] = t: p = mid; break; 7 x[mid] > t: right = mid -1; 8 9 return -1;
第一個正確的程序
根據前面給出的算法思想和偽代碼, 我們給出第一個正確的程序,但是,它還有一些小的問題,后面會講到
int search(int array[], int n, int v)
{
int left, right, middle;
left = 0, right = n - 1;
while (left <= right)
{
middle = (left + right) / 2;
if (array[middle] > v)
{
right = middle;
}
else if (array[middle] < v)
{
left = middle;
}
else
{
return middle;
}
}
return -1;
}
下面,講講在編寫二分查找算法時可能出現的一些問題.
邊界錯誤造成的問題
二分查找算法的邊界,一般來說分兩種情況,一種是左閉右開區間,類似於[left, right),一種是左閉右閉區間,類似於[left, right].需要注意的是, 循環體外的初始化條件,與循環體內的迭代步驟, 都必須遵守一致的區間規則,也就是說,如果循環體初始化時,是以左閉右開區間為邊界的,那么循環體內部的迭代也應該如此.如果兩者不一致,會造成程序的錯誤.比如下面就是錯誤的二分查找算法:
這個算法的錯誤在於, 在循環初始化的時候,初始化right=n,也就是采用的是左閉右開區間,而當滿足array[middle] > v的條件是, v如果存在的話應該在[left, middle)區間中,但是這里卻把right賦值為middle - 1了,這樣,如果恰巧middle-1就是查找的元素,那么就會找不到這個元素.
下面給出兩個算法, 分別是正確的左閉右閉和左閉右開區間算法,可以與上面的進行比較:
(下面這兩個算法是正確的)
死循環
上面的情況還只是把邊界的其中一個寫錯, 也就是右邊的邊界值寫錯, 如果兩者同時都寫錯的話,可能會造成死循環,比如下面的這個程序:
1 int search_bad2(int array[], int n, int v) 2 { 3 int left, right, middle; 4 5 left = 0, right = n - 1; 6 7 while (left <= right) 8 { 9 middle = (left + right) / 2; 10 if (array[middle] > v) 11 { 12 right = middle; 13 } 14 else if (array[middle] < v) 15 { 16 left = middle; 17 } 18 else 19 { 20 return middle; 21 } 22 } 23 24 return -1; 25 }
這個程序采用的是左閉右閉的區間.但是,當array[middle] > v的時候,那么下一次查找的區間應該為[middle + 1, right], 而這里變成了[middle, right];當array[middle] < v的時候,那么下一次查找的區間應該為[left, middle - 1], 而這里變成了[left, middle].兩個邊界的選擇都出現了問題, 因此,有可能出現某次查找時始終在這兩個范圍中輪換,造成了程序的死循環.
溢出
前面解決了邊界選擇時可能出現的問題, 下面來解決另一個問題,其實這個問題嚴格的說不屬於算法問題,不過我注意到很多地方都沒有提到,我覺得還是提一下比較好.
在循環體內,計算中間位置的時候,使用的是這個表達式:
middle = (left + right) / 2;
假如,left與right之和超過了所在類型的表示范圍的話,那么middle就不會得到正確的值.
所以,更穩妥的做法應該是這樣的:
middle = left + (right - left) / 2;
更完善的算法
前面我們說了,給出的第一個算法是一個"正確"的程序, 但是還有一些小的問題.
首先, 如果序列中有多個相同的元素時,查找的時候不見得每次都會返回第一個元素的位置, 比如考慮一種極端情況:序列中都只有一個相同的元素,那么去查找這個元素時,顯然返回的是中間元素的位置.
其次, 前面給出的算法中,每次循環體中都有三次情況,兩次比較,有沒有辦法減少比較的數量進一步的優化程序?
<<編程珠璣>>中給出了解決這兩個問題的算法,結合前面提到溢出問題我對middle的計算也做了修改:
1 int search4(int array[], int n, int v) 2 { 3 int left, right, middle; 4 5 left = -1, right = n; 6 7 while (left + 1 != right)//這個循環維持的條件是left<right && array[left]<v<=array[right],所以到最后的時候, 8 {//如果可以找到目標,則只剩下兩個數,並且滿足 array[left]<v<=array[right],是要查找的數是right 9 middle = left + (right - left) / 2; 10 11 if (array[middle] < v)//必須保證array[left]<v<=array[right],所以left = middle; 12 {//如果left =middle+1,則有可能出現 array[left]<=v的情況 13 left = middle; 14 } 15 else 16 { 17 right = middle; 18 } 19 } 20 21 if (right >= n || array[right] != v) 22 { 23 right = -1; 24 } 25 26 return right; 27 }
這個算法是所有這里給出的算法中最完善的一個,正確,精確且效率高.
但是這個算法的還是不能很好的理解
可以用下面的算法,可以找出滿足條件的數
1 int Bi_Search(int a[],int n,int b)// 2 {//返回等於b的第一個 3 if(n==0) 4 return -1; 5 int low = 0; 6 int high = n-1; 7 int last = -1;//用last記錄上一次滿足條件的下標 8 while (low<=high) 9 { 10 int mid = low +(high-low)/2; 11 if (a[mid]==b) 12 { 13 last = mid; 14 high = mid -1; 15 } 16 else if(a[mid]>b) 17 high = mid -1; 18 else 19 low = mid +1; 20 } 21 22 return last; 23 24 } 25 int Bi_Search1(int a[],int n,int b)//大於b的第一個數 26 { 27 if(n<=0) 28 return -1; 29 int last = -1; 30 int low = 0; 31 int high = n-1; 32 while (low<=high) 33 { 34 int mid = low +(high - low)/2; 35 if(a[mid]>b) 36 { 37 last = mid; 38 high = mid -1; 39 } 40 else if (a[mid]<=b) 41 { 42 low =mid +1; 43 } 44 } 45 46 return last; 47 } 48 int Bi_Search(int a[],int n,int b)// 49 {//返回等於b的第一個 50 if(n==0) 51 return -1; 52 int low = 0; 53 int high = n-1; 54 int last = -1;//用last記錄上一次滿足條件的下標 55 while (low<=high) 56 { 57 int mid = low +(high-low)/2; 58 if (a[mid]==b) 59 { 60 last = mid; 61 high = mid -1; 62 } 63 else if(a[mid]>b) 64 high = mid -1; 65 else 66 low = mid +1; 67 } 68 69 return last; 70 71 } 72 int Bi_Search1(int a[],int n,int b)//大於b的第一個數 73 { 74 if(n<=0) 75 return -1; 76 int last = -1; 77 int low = 0; 78 int high = n-1; 79 while (low<=high) 80 { 81 int mid = low +(high - low)/2; 82 if(a[mid]>b) 83 { 84 last = mid; 85 high = mid -1; 86 } 87 else if (a[mid]<=b) 88 { 89 low =mid +1; 90 } 91 } 92 93 return last; 94 }View Code
