概念
二分查找,又稱折半查找。
- 基本思想:減小查找序列的長度,分而治之地進行關鍵字的查找。
- 前提:該序列必須是有序的。
- 查找過程:在有序表中,取中間的記錄作為比較關鍵字,若給目標值與中間記錄的關鍵字相等,則查找成功;若目標值小於中間記錄的關鍵字,則在中間記錄的左半區間繼續查找;若目標值大於中間記錄的關鍵字,則在中間記錄的右半區間繼續查找;不斷重復這個過程,直到查找成功,否則查找失敗。
- 實現:通常設置3個指針: low, high, mid 。二分查找要求數組必須是有序的,可以是升序,也可以是降序。
基礎實現
// 二分查找 int half_search(int *num, int len, int tar) { int left = 0, right = len - 1; int mid = 0; while (left <= right) { mid = left + (right - left) / 2; if (num[mid] == tar) return mid + 1; else { if (num[mid] > tar) right = mid - 1; else left = mid + 1; } } return 0; }
時間復雜度
實際上,二分查找的過程可以繪制成一棵二叉樹,每次二分查找的過程就相當於把原來的樹划分為兩棵子樹,所以每次二分之后下次就只需要查找其中一半的數據就可以了。在最好的情況下,只需要查找一次就可以了,因為這時候中間記錄的關鍵字與要查找的tar是相等,自然一次就夠了。在最壞的情況下是從根節點查找到最下面的葉子結點,這個過程需要的時間復雜度是$O(log n)$。
優缺點
需要注意的是,雖然二分查找算法的效率很高(這也是二分查找算法被廣泛應用的原因),但是仍然是有使用條件的:有序。所以在需要頻繁進行插入或者刪除操作的數據記錄中使用二分查找算法不太划算,因為要維持數據的有序還需要額外的排序開銷。
變種:循環右(左)移
題目描述
升序數組a經過循環右移后,使用二分查找目標元素x。
如$a = {1,2,3,4,5,6,7}$,則循環移動后$a = {5,6,7,1,2,3,4}$。
解題思路
(1)類似正常的二分查找,變化是不斷移動左右邊界,所以判斷條件更加復雜一點。
(2)每次計算中間元素mid,和左邊界的元素left比較,總能確定有一邊的區間是升序的;
(3)然后對升序那邊進行分析,若這個區間不可能包含x,則不再考慮這個區間;若可能包含x,則將查找范圍限制在這個區間。即每次都可以排除一半區間。
代碼實現(C)
int b_search(int *num, int len, int tar) { int left = 0, right = len - 1; int mid = 0; while (left <= right) { mid = left + (right - left) / 2; if (num[mid] == tar) return mid + 1; if (num[mid] > num[left]) { //左邊是升序 if (num[left] > tar) left = mid + 1; else { if (num[mid] > tar) right = mid - 1; else left = mid + 1; } } else { //右邊是升序 if (num[right] < tar) right = mid - 1; else { if (num[mid] < tar) left = mid + 1; else right = mid - 1; } } } return 0; }
優化一:插值查找算法
可以發現二分查找每次都是選取中間的記錄關鍵字作為划分依據的,而在有些情況下,使用二分查找算法並不是最合適的。舉個例子:在1~1000中,一共有1000個關鍵字,如果要查找目標值10,按照二分查找算法,需要從500開始划分,這樣的話效率就比較低了,所以有人提出了插值查找算法。說白了就是改變划分的比例,比如三分或者四分。
插值查找算法對二分查找算法的改進主要體現在mid的計算公式上,其計算公式為:$$mid = left + \frac{tar - num[left]}{num[right] - num[left]}(right - left)$$
而原來的二分查找公式為:$$mid = left +\frac{1}{2}(right - left)$$
主要變化的地方是$\farc{1}{2}$這個比例系數。其思想可以總結為:插值查找是根據要查找的目標值與查找表中最大最小記錄的關鍵字比較之后的查找算法。核心是上述mid的計算公式。由於大體框架與二分查找算法是一致的,所以時間復雜度仍然是$O(log n)$。
優化二:斐波那契查找算法
從前面的分析中可以看到,無論划分的關鍵字太大或者太小都不合適,所以又有人提出了斐波那契查找算法,其利用了黃金分割比原理來實現。
一個數列如果滿足$F(n) = F(n - 1) + F(n - 2)$,則稱這個數列為斐波那契數列。在斐波那契查找算法中計算mid的公式如下:$$mid = left + F(k - 1) - 1$$
斐波那契查找的前提是待查找的查找表必須順序存儲並且有序。
波那契查找與折半查找很相似,根據斐波那契序列的特點對有序表進行分割。要求待查找數組的長度為某個斐波那契數:$len = Fk - 1$。則
首先將tar值與第$F(k - 1)$位置的記錄進行比較,即 mid = low + F(k - 1) - 1 。比較的結果分為三種:
<1> tar == num[mid] ,mid位置的元素即為所求;
<2> tar > num[mid] ,則 low = mid + 1, k -= 2; 。前者說明待查找的元素在[mid + 1, high]范圍內,后者說明范圍[mid + 1, high]內的元素個數為$len - F(k - 1) = Fk - 1 - F(k - 1) = Fk - F(k - 1) - 1 = F(k - 2) - 1$個,所以可以遞歸地應用斐波那契查找;
<3> key < num[mid] ,則 high = mid - 1, k -= 1 。前者說明待查找的元素在[low, mid - 1]范圍內,后者說明范圍[low, mid - 1]內的元素個數為$F(k - 1) - 1$ 個,所以可以遞歸地應用斐波那契查找;
代碼實現(C)
void fib_arr(int *F, int n) { F[0] = 0; F[1] = 1; for (int i = 2; i < n; i++) F[i] = F[i - 1] + F[i - 2]; return; } int fib_search(int *num, int len, int tar, int *F) { int left = 0, right = len - 1; int mid = 0; int *F = (int *)malloc(20 * sizeof(int)); fib_arr(F, 20); // 構造一個長度為20的斐波拉契數列 int k = 0; while (F[k] - 1 < len) k++; // 根據待查找數組的長度len確定k的值 //將數組num擴展到F[k]-1的長度 int *temp = (int *)malloc((F[k] - 1) * sizeof(int)); memcpy(temp, num, sizeof(temp)); for (int i = len; i < F[k] - 1; i++) temp[i] = num[len - 1]; while (left <= right) { mid = left + F[k - 1] - 1; if (temp[mid] == tar) { if (mid < len) return mid + 1; //若相等, 則說明mid即為查找到的位置 else return len; //若mid >= len, 則說明是擴展的數值,返回len } else if (temp[mid] > tar) { right = mid - 1; k -= 1; } else { left = mid + 1; k -= 2; } } free(temp); free(F); return 0; }
【注意:斐波拉契數列第26個突破十萬,第31個突破一百萬,第36個突破一千萬,第40個突破一億,第45突破10億,第50突破100億。(數列從坐標1開始計數)】
斐波那契查找的核心是:
- 當 tar = num[mid] 時,查找成功;
- 當 tar < num[mid] 時,新的查找范圍是第left個到第mid - 1個,此時范圍個數為$F[k - 1] - 1$個,即數組左邊的長度,所以要在[low, F[k - 1] - 1]范圍內查找;
- 當 tar > num[mid] 時,新的查找范圍是第mid + 1個到第right個,此時范圍個數為$F[k - 2] - 1$個,即數組右邊的長度,所以要在[F[k - 2] - 1]范圍內查找。
關鍵點1:
關於斐波那契查找, 如果要查找的記錄在右側,則左側的數據都不用再判斷了,不斷反復進行下去,對處於當中的大部分數據,其工作效率要高一些。所以盡管
斐波那契查找的時間復雜度也為$O(log n)$,但就平均性能來說,斐波那契查找要優於折半查找。可惜如果是最壞的情況,比如這里tar = 1,那么始終都處於左側在查找,則查找效率低於折半查找。
關鍵點2:
(1)折半查找是進行加法與除法運算:mid = left + (right - left) / 2;
(2)插值查找則進行更復雜的四則運算:mid = left + (right - left) * ((tar- num[left]) / (num[left] - num[left]));
(3)而斐波那契查找只進行最簡單的加減法運算:mid = left + F[k-1] - 1,在海量數據的查找過程中,這種細微的差別可能會影響最終的效率。
(整理自網絡)
參考資料:
https://blog.csdn.net/hacker00011000/article/details/48252131