二分查找有着查找速度快,平均性能好等優點,但僅當列表是有序的時候,二分查找才管用。面試比較常考,今天我們具體看一下二分查找。
0. 我們先從一個場景開始瞭解吧
有一天,小明心血來潮去圖書館借了N本書,結果出圖書館的時候,警報響了,於是門衛大爺把小明攔下,要檢查一下哪本書沒有登記出借。小明正準備把每一本書在報警器下過一下,以找出引發警報的書,但是大爺露出不屑的眼神:你連二分查找都不會嗎?於是大爺把書分成兩堆,讓第一堆過一下報警器,報警器響;於是再把這堆書分成兩堆…… 最終,檢測了 logN 次之後,大爺成功的找到了那本引起警報的書,露出了得意和嘲諷的笑容。於是小明揹着剩下的書走了,心想:大爺果然還是我大爺!
是不是覺得二分查找很簡單?!其實思想是很簡單,但是有不少細節需要注意。正如Knuth 大佬(發明 KMP 算法的那位)所說:
“
Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky…
大致意思就是:思路很簡單,細節是魔鬼。
接下來我就帶大家來分析需要注意的細節,以及二分查找的巧妙運用。
1. 基本的二分查找
根據二分查找的思想,我們將其數學化,符號和意義如下:
二分查找(前提條件:數組有序)
nums:查找數組
t:待查找目標元素
初始化 left = 0,左邊界
right = nums.length -1,右邊界
mid = (left + right)/ 2,查找的中間位置
查找區間 [left, right]:while(left <= right)
查找目標元素的位置,無則返回-1
算法方程可表示如下:
有了基本思路,我們廢話少說,直接放碼過來!
Java實現代碼如下:
private int binarySearchWithR(int[] nums, int target) {
int left = 0;
int right = nums.length - 1; // 注意 [left, right]
while(left <= right) { // 注意
int mid = (right - left) / 2 + left; // mid = (left + right) / 2 的優化形式,防止溢出!
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid - 1; // 注意
}
return -1;
}
需要注意的細節就是代碼中註明的地方,其實這幾個地方是相關的。
因爲初始化 right = 數組的長度 - 1;即最後一個元素,是可以取到的。此時的查找區間爲 [left,right],所以決定了 while(left <= right),判斷條件是可以加等號的。同時也決定了後續的 right = mid - 1;需要減1,否則可能導致下標越界。
其實也可以初始化 right = 數組的長度;是不可以取到的。此時的查找區間爲 [left,right),所以決定了 while(left < right),判斷條件是不可以加等號的。同時也決定了後續的 right = mid;不需要減1。
不取右邊界情況的代碼如下:
private int binarySearchWithoutR(int[] nums, int target) {
int left = 0;
int right = nums.length; // 注意 [left, right)
while(left < right) { // 注意
int mid = (right - left) / 2 + left;
if(nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid; // 注意
}
return -1;
}
所以,明白了二分查找的思路,注意這些細節,也就不會導致各種情況的混亂。
爲了更深入的理解二分查找的過程,下面將運行過程圖形化(做這些圖花了我一晚上,強迫症的我太不容易了T_T)
以下情況爲初始化 right = 數組長度 - 1 ,查找區間爲 [left, right]
2. 尋找左側邊界的二分查找
二分查找的巧妙運用一就是尋找一個數的左側邊界,如尋找 [1,2,4,4,5,6] 中,4 第一次出現的位置?也就是尋找 4 的左側邊界。
尋找左側邊界的二分查找(前提條件:數組有序)
nums:查找數組
t:待查找目標元素
初始化 left = 0,左邊界
right = nums.length -1,右邊界
mid = (left + right)/ 2,查找的中間位置
查找區間:while(left <= right)
查找目標元素第一次出現的位置(左側邊界),無則返回比它大的數的左側邊界
算法方程可表示如下:
Java實現
private int leftBound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = (right - left) / 2 + left;
if (nums[mid] == target) {
right = mid -1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid -1;
}
}
return left; // 注意
}
可以看出,此代碼與二分查找唯一的不同就是注意的地方。當找到目標元素時,並不是直接返回,而是收緊右側邊界,繼續查找,以鎖定左側邊界。最後返回左側邊界
3. 尋找右側邊界的二分查找
同理,如尋找 [1,2,4,4,5,6] 中,4 最後一次出現的位置?也就是尋找 4 的右側邊界。
尋找右側邊界的二分查找(前提條件:數組有序)
nums:查找數組
t:待查找目標元素
初始化 left = 0,左邊界
right = nums.length -1,右邊界
mid = (left + right)/ 2,查找的中間位置
查找區間:while(left <= right)
查找目標元素最後一次出現的位置(右側邊界),無則返回比它小的數的右側邊界
算法方程可表示如下:
Java實現
private int rightBound(int[] nums, int target) {
int left = 0, right = nums.length-1;
while (left <= right) {
int mid = (right - left) / 2 + left;
if (nums[mid] == target) {
left = mid + 1; // 注意
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
}
}
return right; // 注意
}
可以看出,此代碼與二分查找唯一的不同也是注意的地方。當找到目標元素時,並不是直接返回,而是收緊在側邊界,繼續查找,以鎖定右側邊界。最後返回右側邊界
4. 總結
第一個,最基本的二分查找算法:
因爲我們初始化 right = nums.length - 1
所以決定了我們的「搜索區間」是 [left, right]
所以決定了 while (left <= right)
同時也決定了 left = mid+1 和 right = mid-1
因爲我們只需找到一個 target 的索引即可
所以當 nums[mid] == target 時可以立即返回
第二個,尋找左側邊界的二分查找:
因爲我們初始化 right = nums.length - 1
所以決定了我們的「搜索區間」是 [left, right]
所以決定了 while (left <= right)
同時也決定了 left = mid+1 和 right = mid-1
因爲我們需找到 target 的最左側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊右側邊界以鎖定左側邊界
第三個,尋找右側邊界的二分查找:
因爲我們初始化 right = nums.length - 1
所以決定了我們的「搜索區間」是 [left, right]
所以決定了 while (left <= right)
同時也決定了 left = mid+1 和 right = mid-1
因爲我們需找到 target 的最右側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊左側邊界以鎖定右側邊界
如果以上內容你都能理解,那麼恭喜你,二分查找算法的細節不過如此。
5. 最後我們來用一道LeetCode的算法題驗收一下成果吧
牛客網鏈接
題目描述
統計一個數字在排序數組中出現的次數。
Input:
nums = 1, 2, 4, 4, 5, 6
K = 4
Output:
2
解題思路
我們可以找出目標值的左邊界和右邊界,然後用右邊界 - 左邊界 + 1 ,如 4 的左邊界爲 2,右邊界爲 3。但是這樣左右邊界的方法都要編寫,有一個巧妙的方法,可以偷懶只寫一個方法即可。
找出 4 的左邊界1(或右邊界1),再找出 4+1 的左邊界2(或4 - 1的右邊界2),用左邊界2 - 左邊界1 即可(或右邊界2 - 右邊界1)
我的解題代碼: 已通過
// 根據二分查找的思路,修改爲尋找一個數的左側邊界。
public class Solution {
public int GetNumberOfK(int [] array , int k) {
if(array == null || array.length == 0) return 0;
// 這個數第一次出現的位置(即左邊界),無此值則返回比它大的數的左側邊界
int first = myleft_bound(array, k);
// 這個數最後一次出現的位置(用目標值+1的左邊界),無此值則返回比它大的數的左側邊界
int last = myleft_bound(array, k+1);
return last - first;
}
// 尋找target的左側邊界,無則返回比它大的數的左側邊界
private int myleft_bound(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while (left <= right) {
int mid = (right - left) / 2 + left;
if (nums[mid] == target) {
// 關鍵!!!基本的二分查找若找到是返回下標
//因爲我們需找到 target 的最左側索引
//所以當 nums[mid] == target 時不要立即返回
//而要收緊右側邊界以鎖定左側邊界
right = mid -1;
} else if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid -1;
}
}
// 返回左側邊界
return left;
}
}
6. 參考
以上。記錄一下這次學習的二分查找,分享給大家順便整理下思路。碼字做圖不易,若有幫助,點個贊鼓勵一下鴨~