本文是二分查找算法的總結歸納
更多請參照算法刷題套路和模板的GitHub倉庫
文章目錄
簡介
二分查找也稱折半查找(Binary Search
),它是一種效率較高的查找方法。但是,二分查找要求線性表必須採用順序存儲結構,而且表中元素按關鍵字有序排列。
二分查找算法是典型的「減治思想」的應用,我們使用二分查找將待查找的區間逐漸縮小,以達到「縮減問題規模」的目的。
比如查找升序數組nums
裏的目標值target
(這裏只討論升序數組,降序數組是一樣的道理):
int [] nums = { 0, 1, 2, 3, 4, 5, 6, 7 };
約定
我們把待區間的左邊界下標設爲left
,右邊界下標設爲right
,中間位置下標設爲mid
。
// 待查找區間的左邊界下標
int left = 0;
// 待查找區間的右邊界下標
int right = a.length - 1;
// 中間位置下標取法有兩種
// 下取整,不寫成 (right + left) / 2 是爲了防止溢出
int mid = left + (right - left) / 2;
// 上取整
int mid = left + (right - left + 1) / 2;
// 中間位置下標求值的優化
// 1.使用右移位運算符,右移 1 位相當於除以 2
// 下取整
int mid = left + (right - left) >> 1;
// 上取整
int mid = left + (right - left + 1) >> 1;
// 2.使用無符號右移位運算符(貌似僅 Java 有),參考 JDK 源碼 Arrays.binarySearch() 的寫法,
// left + right 即使是在整型溢出以後,仍然能夠得到正確的結果
// 下取整
int mid = (right + left) >>> 1;
// 上取整
int mid = (right + left + 1) >>> 1;
打印數組的共通ArrayUtil
public class ArrayUtil {
/**
* 打印整型數組
* @param arrays
*/
public static void printArray(int[] arrays) {
StringBuilder sBuilder = new StringBuilder();
sBuilder.append("{ ");
for (int i : arrays) {
sBuilder.append(i + ", ");
}
// 刪除多餘的", "
sBuilder.delete(sBuilder.length() - 2, sBuilder.length());
sBuilder.append(" }");
System.out.println(sBuilder);
}
}
一、模板 1:while (left <= right)
1、思路:在循環體內部查找元素(解決簡單問題時有用),即考慮下一輪目標元素應該在哪個區間
把待查找區間[left, right]
分爲 3 個部分:
mid
位置(只有 1 個元素);[left, mid - 1]
裏的所有元素;[mid + 1, right]
裏的所有元素;
於是,二分查找就是不斷地在區間[left, right]
里根據中間元素nums[mid]
與target
的大小關係來不斷縮小查找區間,最終找到target
的下標:
nums[mid] == target
時,返回mid
;nums[mid] > target
時,由於數組升序,mid
以及mid
右邊的所有元素都大於target
,下一輪目標元素一定在區間[left, mid - 1]
裏,因此設置right = mid - 1
;nums[mid] < target
時,由於數組升序,mid
以及mid
左邊的所有元素都小於target
,下一輪目標元素一定在區間[mid + 1, right]
裏,因此設置left = mid + 1
。
2、圖解
3、代碼實現
public class BinarySearch {
public static void main(String[] args) {
int[] nums = { 0, 1, 2, 3, 4, 5, 6, 7 };
int target = 2;
System.out.print("數組 nums:");
ArrayUtil.printArray(nums);
System.out.println("目標值 target:" + target);
System.out.println("模板 1 下標:" + binarySearch1(nums, target));
}
/**
* 二分查找法<br>
* <li>模板 1:while (left <= right)</li><br>
* @param nums 待查找數組
* @param target 待查找目標值
* @return 目標值在數組中的下標<br>
* 未查找到就返回 -1
*/
public static int binarySearch1(int[] nums, int target) {
// 特殊用例判斷
int len = nums.length;
if (len == 0) {
return -1;
}
// 在 [left, right] 區間裏查找 target
int left = 0;
int right = len - 1;
while (left <= right) {
// 爲了防止 left + right 整形溢出,寫成如下形式
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
// 下一輪查找區間:[left, mid - 1]
right = mid - 1;
} else {
// 此時:nums[mid] < target
// 下一輪查找區間:[mid + 1, right]
left = mid + 1;
}
}
return -1;
}
}
運行結果:
數組 nums:{ 0, 1, 2, 3, 4, 5, 6, 7 }
目標值 target:2
模板 1 下標:2
二、模板 2:while (left < right),推薦使用
1、思路:在循環體內部排除元素(解決複雜問題時非常有用),即考慮中間元素 nums[mid] 在什麼情況下不是目標元素
把待查找區間[left, right]
分爲 2 個部分:
- 不存在目標元素(
if
分支); - 可能存在目標元素(
else
分支,包含mid
);
與模版 1 同樣,二分查找就是不斷地在區間[left, right]
里根據中間元素nums[mid]
與target
的大小關係來不斷縮小查找區間,最終找到target
的下標:
①、中間位置下取整
nums[mid] < target
時,mid
以及mid
左邊元素都小於target
,下一輪目標元素一定在區間[mid + 1, right]
裏,因此設置left = mid + 1
。
②、中間位置上取整
nums[mid] > target
時,mid
以及mid
右邊元素都小於target
,下一輪目標元素一定在區間[left, mid - 1]
裏,因此設置right = mid - 1
;
Tips:先寫if else
分支,再決定是中間位置是上取整(target
在左邊)還是下取整(target
在右邊)。
特徵:
while (left < right)
,這裏使用嚴格小於<
表示的臨界條件是:當區間裏的元素只有 2 個時,依然可以執行循環體。換句話說,退出循環的時候一定有left == right
成立,這一點在定位元素下標的時候極其有用。
2、圖解
3、代碼實現
public class BinarySearch {
public static void main(String[] args) {
int[] nums = { 0, 1, 2, 3, 4, 5, 6, 7 };
int target = 2;
System.out.print("數組 nums:");
ArrayUtil.printArray(nums);
System.out.println("目標值 target:" + target);
System.out.println("模板 2(下取整)下標:" + binarySearch2_floor(nums, target));
System.out.println("模板 2(上取整)下標:" + binarySearch2_ceil(nums, target));
}
/**
* 二分查找法<br>
* <li>模板 2(下取整):while (left < right)</li><br>
* @param nums 待查找數組
* @param target 待查找目標值
* @return 目標值在數組中的下標<br>
* 未查找到就返回 -1
*/
public static int binarySearch2_floor(int[] nums, int target) {
// 特殊用例判斷
int len = nums.length;
if (len == 0) {
return -1;
}
// 在 [left, right] 區間裏查找 target
int left = 0;
int right = len - 1;
while (left < right) {
// 選擇中間位置時下取整
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
// 下一輪查找區間是 [mid + 1, right]
left = mid + 1;
} else {
// 下一輪查找區間是 [left, mid]
right = mid;
}
}
// 退出循環的時候 left == right,程序只剩下一個元素沒有看到。
// 視情況,是否需要單獨判斷 left(或者 right)這個下標的元素是否符合題意。
return nums[left] == target ? left : -1;
}
/**
* 二分查找法<br>
* <li>模板2(上取整):while (left < right)</li><br>
* @param nums 待查找數組
* @param target 待查找目標值
* @return 目標值在數組中的下標<br>
* 未查找到就返回 -1
*/
public static int binarySearch2_ceil(int[] nums, int target) {
// 特殊用例判斷
int len = nums.length;
if (len == 0) {
return -1;
}
// 在 [left, right] 區間裏查找 target
int left = 0;
int right = len - 1;
while (left < right) {
// 選擇中間位置時上取整
int mid = left + (right - left + 1) / 2;
if (nums[mid] > target) {
// 下一輪查找區間是 [left, mid - 1]
right = mid - 1;
} else {
// 下一輪查找區間是 [mid, right]
left = mid;
}
}
// 退出循環的時候 left == right,程序只剩下一個元素沒有看到。
// 視情況,是否需要單獨判斷 left(或者 right)這個下標的元素是否符合題意。
return nums[left] == target ? left : -1;
}
}
運行結果:
數組 nums:{ 0, 1, 2, 3, 4, 5, 6, 7 }
目標值 target:2
模板 2(下取整)下標:2
模板 2(上取整)下標:2
三、模板 3:while (left + 1 < right)
如果已經掌握了模板 2,就無需掌握這個模板,僅作了解。
1、與模版 2 的區別
這一版代碼和模板 2 沒有本質區別,一個顯著的標誌是:循環可以繼續的條件是 while (left + 1 < right)
,這說明在退出循環的時候,一定有 left + 1 == right
成立,也就是退出循環以後,區間有 2 個元素,即 [left, right]
;
2、優缺點
- 優點:不用理解模板 2 在分支出現
left = mid
的時候中間位置上/下取整的行爲; - 缺點:
while (left + 1 < right)
寫法相對於while (left <= right)
和while (left < right)
來說並不自然;由於退出循環以後,區間一定有兩個元素,需要思考哪一個元素纔是需要找的,即「後處理」一定要做,有些時候還會有先考慮left
還是right
的區別。
3、代碼實現
public class BinarySearch {
public static void main(String[] args) {
int[] nums = { 0, 1, 2, 3, 4, 5, 6, 7 };
int target = 2;
System.out.print("數組 nums:");
ArrayUtil.printArray(nums);
System.out.println("目標值 target:" + target);
System.out.println("模板 3 下標:" + binarySearch3(nums, target));
}
/**
* 二分查找法<br>
* <li>模板 3:while (left + 1 < right)</li><br>
* @param nums 待查找數組
* @param target 待查找目標值
* @return 目標值在數組中的下標<br>
* 未查找到就返回 -1
*/
public static int binarySearch3(int[] nums, int target) {
// 特殊用例判斷
int len = nums.length;
if (len == 0) {
return -1;
}
// 在 [left, right] 區間裏查找 target
int left = 0;
int right = len - 1;
while (left + 1 < right) {
// 選擇中間位置時下取整
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
left = mid;
} else {
right = mid;
}
}
if (nums[left] == target) {
return left;
}
if (nums[right] == target) {
return right;
}
return -1;
}
}
運行結果:
數組 nums:{ 0, 1, 2, 3, 4, 5, 6, 7 }
目標值 target:2
模板 3 下標:2