【算法】二分查找(Java 版)

本文是二分查找算法的總結歸納
更多請參照算法刷題套路和模板的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、圖解

nums[mid] == target
nums[mid] > target
nums[mid] < target

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、圖解

模板2:while (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("模板 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章