簡單介紹
二分查找 也稱 折半查找(Binary Search),它是一種效率較高的查找方法。但是,折半查找要求線性表必須采用順序存儲結構,而且表中元素按關鍵字有序排列,說簡單點就是要求查找的數組是有序的。
思路分析
-
搜索過程從數組(有序的)的中間元素開始,如果中間元素正好是要查找的元素,則搜索過程結束;
-
如果要查找元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,而且跟開始一樣從中間元素開始比較。
-
如果在某一步驟數組為空,則代表找不到。這種搜索算法每一次比較都使搜索范圍縮小一半。
看動圖體驗一下,下面的動圖是二分查找與順序查找的對比:
上面的思路如果看不懂,下面舉個例子並代碼實現。
請對一個 有序數組 進行二分查找 {1,8, 10, 89, 1000, 1234}
,輸入一個數查找該數組是否存在此數,並且輸出下 標,如果沒有就提示「沒有這個數」。
二分查找可以使用 遞歸 和 非遞歸 實現,這里使用遞歸方式實現。
查找步驟:
-
首先確定該數組的中間下標
int mid = (left + right)/2
-
然后讓需要查找的數
findVal
和arr[mid]
比較findVal > arr[i]
,說明要查找的數在數組 右 邊findVal < arr[i]
,說明要查找的數在數組 左 邊findVal == arr[i]
,說明已經找到,就返回
什么時候結束遞歸呢?
-
找到則結束遞歸
-
未找到,則結束遞歸
當
left > right
時,表示整個數組已經遞歸完,說明沒有找到,結束遞歸。這里要動腦筋思考一下,它往左或往右查找卻沒有找到目標數,left
和right
的情況,腦子里走一遍過程。{1,8, 10, 89, 1000, 1234} 共 5 個 查找 -1 第一輪: int mid = (0 + 5)/2 = 2 arr[mid] = 10 -1 < 10,往左邊查找 第二輪:下面為什么是 - 1,而不是 - 2或其他的呢,是因為 arr[mid] 如果等於要查找的數就返回了,已經判斷過了,不需要再判斷 mid = (0 + 1)/2 = 0 arr[mid] = 10 -1 < 1,往左邊 第三輪:同理,這時 left = 0,right = -1 left 就大於 right 了
代碼實現
/**
* 二分查找
*/
public class BinarySearchTest {
@Test
public void binaryTest() {
int[] arr = new int[]{1, 8, 10, 89, 1000, 1234};
int findVal = 89;
int result = binary(arr, 0, arr.length - 1, findVal);
System.out.println("查找值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));
findVal = -1;
result = binary(arr, 0, arr.length - 1, findVal);
System.out.println("查找值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));
findVal = 123456;
result = binary(arr, 0, arr.length - 1, findVal);
System.out.println("查找值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));
findVal = 1;
result = binary(arr, 0, arr.length - 1, findVal);
System.out.println("查找值 " + findVal + ":" + (result == -1 ? "未找到" : "找到值,索引為:" + result));
}
/**
* @param arr
* @param left 左邊索引
* @param right 右邊索引
* @param findVal 要查找的值
* @return 未找到返回 -1,否則返回該值的索引
*/
private int binary(int[] arr, int left, int right, int findVal) {
// 當找不到時,則返回 -1
if (left > right) {
return -1;
}
int mid = (left + right) / 2;//數組中間值的下標
int midVal = arr[mid];//數組中間值
// 相等則找到
if (midVal == findVal) {
return mid;
}
// 判斷值是否在右邊,如果要查找的值在右邊,則右遞歸
if (findVal > midVal) {
// mid 的值,就是當前對比的值,所以不需要判定
return binary(arr, mid + 1, right, findVal);//動腦筋
}
//否則向左查找,左遞歸
return binary(arr, left, mid - 1, findVal);
}
}
測試輸出
查找值 89:找到值,索引為:3
查找值 -1:未找到
查找值 123456:未找到
查找值 1:找到值,索引為:0
可以看到,這個算法已經實現了,但是還有一個問題,仔細觀察,你就會發現上面的代碼實現的算法有個缺點,那就是如果數組中要查找的數存在多個,那么它只能返回第一個查找到的數的下標。
下面我們就來優化這個缺點。
查找出所有符合要求的值
請對一個 有序數組 進行二分查找 {1,8, 10, 89, 1000, 1000,1234}
,輸入一個數查找該數組是否存在此數,並且求出所有下標,如果沒有就提示「沒有這個數」。
增加難度:返回該值所有下標
@Test
public void binary2Test() {
int[] arr = new int[]{1, 8, 10, 89, 1000, 1000, 1234};
int findVal = 89;
List<Integer> result = binary2(arr, 0, arr.length - 1, findVal);
System.out.println("查找值 " + findVal + ":" + (result == null ? "未找到" : "找到值,索引為:" + result));
findVal = -1;
result = binary2(arr, 0, arr.length - 1, findVal);
System.out.println("查找值 " + findVal + ":" + (result == null ? "未找到" : "找到值,索引為:" + result));
findVal = 123456;
result = binary2(arr, 0, arr.length - 1, findVal);
System.out.println("查找值 " + findVal + ":" + (result == null ? "未找到" : "找到值,索引為:" + result));
findVal = 1;
result = binary2(arr, 0, arr.length - 1, findVal);
System.out.println("查找值 " + findVal + ":" + (result == null ? "未找到" : "找到值,索引為:" + result));
findVal = 1000;
result = binary2(arr, 0, arr.length - 1, findVal);
System.out.println("查找值 " + findVal + ":" + (result == null ? "未找到" : "找到值,索引為:" + result));
}
/**
* 查找所有符合條件的下標
*
* @param arr
* @param left 左邊索引
* @param right 右邊索引
* @param findVal 要查找的值
* @return 未找到返回 null,否則返回該值的索引集合
*/
private List<Integer> binary2(int[] arr, int left, int right, int findVal) {
// 當找不到時,則返回 null
if (left > right) {
return null;
}
int mid = (left + right) / 2;
int midVal = arr[mid];
// 相等則找到
if (midVal == findVal) {
//定義一個集合,保存滿足要求的數的下標
List<Integer> result = new ArrayList<>();
// 如果已經找到,則先不要退出
// 因為二分查找的前提是:對一個有序的數組進行查找
// 所以,我們只需要,繼續挨個的往左邊和右邊查找目標值就好了
int tempIndex = mid - 1;//這里是第一個滿足條件的數的下標的左邊一個數的下標
result.add(mid); //先把當前找到的下標添加進集合
// 先往左邊找
while (true) {
// 當左邊的數組已經找完
// 或 找到一個不與目標值相等的值,就可以跳出左邊查找。 這里動一下腦筋
if (tempIndex < 0 || arr[tempIndex] != midVal) {
break;
}
result.add(tempIndex);
tempIndex--;//找到了,繼續往左一個
}
// 再往右邊查找
tempIndex = mid + 1;//這里是第一個滿足條件的數的下標的右邊一個數的下標
while (true) {
// 這里也跟上面一樣,當右邊的數組已經找完
// 或 找到一個不與目標值相等的值,就可以跳出右邊查找。 這里動一下腦筋
if (tempIndex >= arr.length || arr[tempIndex] != midVal) {
break;
}
result.add(tempIndex);
tempIndex++;//找到了,繼續往右一個
}
//找完了返回下標集合
return result;
}
// 判斷值是否在右邊,如果要查找的值在右邊,則右遞歸
if (findVal > midVal) {
// mid 的值,就是當前對比的值,所以不需要判定
return binary2(arr, mid + 1, right, findVal);
}
//否則向左查找,左遞歸
return binary2(arr, left, mid - 1, findVal);
}
測試輸出信息
查找值 89:找到值,索引為:[3]
查找值 -1:未找到
查找值 123456:未找到
查找值 1:找到值,索引為:[0]
查找值 1000:找到值,索引為:[5, 4]
非遞歸形式
二分查找法只適用於從 有序 的數列中查找(比如數字和字母等),將數列 **排序后 **再進行查找。
二分查找法的運行時間為對數時間 O(log2 n)
,即查找到目標位置最多只需要 log2 n
步,假設從 0~99
的隊列(100 個數,即 n = 100),中旬到目標數 30,則需要查找的步數為 log2 100
,即最多需要查找 7 次(26 < 100 < 27,100 介於 2 的 6、7 次方之間,次方則是尋找的步數)
代碼實現
/**
* 二分查找:非遞歸
*/
public class BinarySearchNoRecur {
@Test
public void fun() {
int[] arr = new int[]{1, 3, 8, 10, 11, 67, 100};
int target = 1;
int result = binarySearch(arr, target);
System.out.printf("查找 %d ,找位置為 %d \n", target, result);
target = 11;
result = binarySearch(arr, target);
System.out.printf("查找 %d ,找位置為 %d \n", target, result);
target = 100;
result = binarySearch(arr, target);
System.out.printf("查找 %d ,找位置為 %d \n", target, result);
target = -1;
result = binarySearch(arr, target);
System.out.printf("查找 %d ,找位置為 %d \n", target, result);
target = 200;
result = binarySearch(arr, target);
System.out.printf("查找 %d ,找位置為 %d \n", target, result);
}
/**
* 二分查找:非遞歸
*
* @param arr 數組,前提:升序排列
* @return 找到則返回下標,找不到則返回 -1
*/
public int binarySearch(int[] arr, int target) {
int left = 0;
int right = arr.length;
int mid = 0;
// 表示還可以進行查找
while (left <= right) {
mid = (left + right) / 2;
if (mid >= arr.length // 查找的值大於數組中的最大值
) {
// 防止越界
return -1;
}
if (arr[mid] == target) {
return mid;
}
// 升序:目標值比中間值大,則向左查找
if (target > arr[mid]) {
left = mid + 1;
} else {
// 否則:向右查找
right = mid - 1;
}
}
return -1;
}
}
測試輸出
查找 1 ,找位置為 0
查找 11 ,找位置為 4
查找 100 ,找位置為 6
查找 -1 ,找位置為 -1
查找 200 ,找位置為 -1
tip:這個算法很簡單,但是很實用。必須要掌握。