一、什麼是二分查找?
二分查找針對的是一個有序的數據集合,每次通過跟區間中間的元素對比,將待查找的區間縮小爲之前的一半,直到找到要查找的元素,或者區間縮小爲0。
二、時間複雜度分析?
1.時間複雜度
假設數據大小是n,每次查找後數據都會縮小爲原來的一半,最壞的情況下,直到查找區間被縮小爲空,才停止。所以,每次查找的數據大小是:n,n/2,n/4,…,n/(2^k),…,這是一個等比數列。當n/(2^k)=1時,k的值就是總共縮小的次數,也是查找的總次數。而每次縮小操作只涉及兩個數據的大小比較,所以,經過k次區間縮小操作,時間複雜度就是O(k)。通過n/(2^k)=1,可求得k=log2n,所以時間複雜度是O(logn)。
2.認識O(logn)
①這是一種極其高效的時間複雜度,有時甚至比O(1)的算法還要高效。爲什麼?
②因爲logn是一個非常“恐怖“的數量級,即便n非常大,對應的logn也很小。比如n等於2的32次方,也就是42億,而logn才32。
③由此可見,O(logn)有時就是比O(1000),O(10000)快很多。
三、如何實現二分查找?
1.循環實現
代碼實現:
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = (low + high) / 2;
if (a[mid] == value) {
return mid;
} else if (a[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
注意事項:
①循環退出條件是:start<=end,而不是start<end。
②mid的取值,使用mid=start + (end - start) / 2,而不用mid=(start + end)/2,因爲如果start和end比較大的話,求和可能會發生int類型的值超出最大範圍。爲了把性能優化到極致,可以將除以2轉換成位運算,即start + ((end - start) >> 1),因爲相比除法運算來說,計算機處理位運算要快得多。
③start和end的更新:start = mid - 1,end = mid + 1,若直接寫成start = mid,end=mid,就可能會發生死循環。
2.遞歸實現
// 二分查找的遞歸實現
public int bsearch(int[] a, int n, int val) {
return bsearchInternally(a, 0, n - 1, val);
}
private int bsearchInternally(int[] a, int low, int high, int value) {
if (low > high) return -1;
int mid = low + ((high - low) >> 1);
if (a[mid] == value) {
return mid;
} else if (a[mid] < value) {
return bsearchInternally(a, mid+1, high, value);
} else {
return bsearchInternally(a, low, mid-1, value);
}
}
四、使用條件(應用場景的侷限性)
1.二分查找依賴的是順序表結構,即數組。
2.二分查找針對的是有序數據,因此只能用在插入、刪除操作不頻繁,一次排序多次查找的場景中。
3.數據量太小不適合二分查找,與直接遍歷相比效率提升不明顯。但有一個例外,就是數據之間的比較操作非常費時,比如數組中存儲的都是長度超過300的字符串,那這是還是儘量減少比較操作使用二分查找吧。
4.數據量太大也不是適合用二分查找,因爲數組需要連續的空間,若數據量太大,往往找不到存儲如此大規模數據的連續內存空間。
五、課後思考
1.如何在1000萬個整數中快速查找某個整數?
①1000萬個整數佔用存儲空間爲40MB,佔用空間不大,所以可以全部加載到內存中進行處理;
②用一個1000萬個元素的數組存儲,然後使用快排進行升序排序,時間複雜度爲O(nlogn)
③在有序數組中使用二分查找算法進行查找,時間複雜度爲O(logn)
2.如何編程實現“求一個數的平方根”?要求精確到小數點後6位?
/**
* 使用二分查找實現平方根函數,要求精確到小數點後6位
*/
public float sqrt_search(float n){
float mid = 0.0f;
if(n < -1e-6){
// 小於0,拋異常
throw new IllegalArgumentException();
}else if(Math.abs(n) >= -1e-6 && Math.abs(n) <= 1e-6){
return mid;
}else{
// 逐次逼近,默認平方根的不會超過n的一半
float high = n / 2.0f;
float low = 0.0f;
while(Math.abs(high - low) > 1e-6){
// 首先找到中間值
mid = low + (high - low) / 2;
float tmp = mid * mid;
// 比較並更新 high和low
if((tmp - n) > 1e-6){
high = mid;
}else if((tmp -n) < -1e-6){
low = mid;
}else{
return mid;
}
}
}
return mid;
}