前面學習了六種排序算法,接着學習搜索,搜索中使用最多一種簡單查找方法就是二分查找。二分查找的特點是,先保證數列是有序排序,然後每次查找可以減少一半的範圍,直到查到或者找不到目標元素爲止。這個也是經常在面試中被要求手寫這個查找代碼,接着要你設計測試用例去測試你寫的代碼。
1.二分查找定義
二分查找又稱折半查找,優點是比較次數少,查找速度快,平均性能好。其缺點就是要求待查表爲有序表,且插入刪除困難。因此,折半查找方法適用於不經常變動而查找頻繁的有序列表。首先,假設表中元素是按升序排列,將表中間位置紀錄的關鍵字與查找關鍵字比較,如果兩者相等,則查找成功;否則利用中間位置紀錄將表分成前後兩個子表。如果中間位置紀錄的關鍵字大於查找關鍵字,則進一步查找前一子表,否則進一步查找後一子表。重複以上過程,直到找到滿足條件的紀錄,使查找成功,或者直到子表不存在爲止,此時查找不成功。
2.二分查找圖解
例如有下面一個數列,二分查找算法如下,上半部分是二分查找,下半部分是順序查找。
二分查找的好處,每次查詢一遍之後,接下來要查找範圍縮小了一般。上圖剛好是二分查找的最壞情況和順序查找的最優情況對比。
3.二分查找代碼實現
Python代碼實現
先來看看遞歸的方式實現
# coding:utf-8
def binary_search(alist, item):
"""二分查找"的遞歸實現"""
n = len(alist)
if n > 0:
mid = n // 2
if alist[mid] == item:
return True
elif item < alist[mid]:
return binary_search(alist[:mid], item)
else:
return binary_search(alist[mid+1:], item)
return False
if __name__ == "__main__":
li = [1, 3, 6, 7, 11, 20, 39]
print(binary_search(li, 39))
print(binary_search(li, 44))
運行結果
True
False
再來看看第二種方式,非遞歸方法
# coding:utf-8
def binary_search(alist, item):
"""二分查找"的非遞歸實現"""
n = len(alist)
first = 0
last = n-1
while first <= last:
mid = (first + last) // 2
if alist[mid] == item:
return alist.index(item)
elif alist[mid] < item:
first = mid + 1
else:
last = mid - 1
return -1
if __name__ == "__main__":
li = [1, 3, 6, 7, 11, 20, 39]
print(binary_search(li, 39))
print(binary_search(li, 44))
上面的設計是如果找到了就返回該元素在數列中的下標也就是索引,找不到返回-1.
運行結果
6
-1
Java代碼實現
第一種遞歸實現
package com.anthony.test;
import java.util.Arrays;
public class BinarySearch {
public static void main(String[] args) {
int[] arr = {1, 3, 6, 7, 11, 20, 39};
System.out.println(binarySeach_01(arr, 39));
System.out.println(binarySeach_01(arr, 44));
}
public static boolean binarySeach_01(int[] arr, int item) {
int n = arr.length;
if(n > 0){
int mid = n / 2;
if ( item == arr[mid]){
return true;
}else if(item < arr[mid]) {
return binarySeach_01(Arrays.copyOfRange(arr,0, mid -1), item);
} else{
return binarySeach_01(Arrays.copyOfRange(arr,mid+1, n), item);
}
}
return false;
}
}
第二種非遞歸實現
package com.anthony.test;
import java.util.Arrays;
public class BinarySearch {
public static void main(String[] args) {
int[] arr = {1, 3, 6, 7, 11, 20, 39};
System.out.println(binarySeach_02(arr, 39));
System.out.println(binarySeach_02(arr, 44));
}
public static int binarySeach_02(int[] arr, int item) {
//1.定義最小索引,最大索引,中間索引的標記
int max = arr.length - 1;
int min = 0;
int mid = (min+max)/2;
//2 當中間值不等於要找的值,就開始循環
while (arr[mid] != item) {
if(arr[mid] < item) {
// 說明目標元素在右半部分,最小的索引改變
min = mid + 1;
}else if (arr[mid] > item) {
// 說明目標元素在左側半部分,最大的索引改變
max = mid - 1;
}
// 由於上面min或者max發生了改變,所以mid需要重新獲取新的值
mid = (min + max)/2;
// 如果最小索引大於最大索引就沒有查找的可能性,返回-1
if(min > max) {
return -1;
}
}
return mid;
}
}
運行結果
6
-1
4.針對上面java版本非遞歸方法的單元測試
這個題目,我在滴滴面試過程中遇到過,當時每考慮全測試點。
測試點1:100%語句覆蓋
因爲是白盒測試,這裏先來一個百分百語句覆蓋的測試用例。我們二分查找的思路就是,先和中間元素比較,這是一個代碼分支,然後比較左半部分,這是第二個代碼分支測試點,然後是右半部分列表去查找,這是第三個代碼分支測試點。所以,我們先來一個只有三個元素的數列,然後分別去查找三次,第一次查找第一個元素代表左半部分代碼路徑覆蓋,第二次查找中間元素,這個時候剛好覆蓋arr[mid]== item這個代碼分支,第三次查找第三個元素,模擬右半部分數列的二分查找。,第四次查找模擬查找不到的情況。三次查找,四個測試用例,我們在一個junit的方法中覆蓋。
把Java第二種方法的二分查找寫到一個類中,作爲靜態工具類使用。
package test;
import org.junit.Test;
public class TestBinarySearch {
@Test
public void test1() {
System.out.println("100%代碼路徑覆蓋測試");
int[] arr = {1, 2, 3};
int item1 = 1;
int item2 = 2;
int item3 = 3;
int item4 = 4;
System.out.println(BinarySearch.binarySeach_02(arr, item1));
System.out.println(BinarySearch.binarySeach_02(arr, item2));
System.out.println(BinarySearch.binarySeach_02(arr, item3));
System.out.println(BinarySearch.binarySeach_02(arr, item4));
}
}
運行結果
100%代碼路徑覆蓋測試
0
1
2
-1
測試點2:分支覆蓋測試
我們這裏代碼分支,有兩個,一個是元素在左半部分,第二個是元素在右半部分。所以,這裏我們測試用例設計沒有上面這個用例考慮全面,這裏我們只是測試if -else這兩個分支,下面用例代表左半部分元素查找和右半部分查找的使用場景。
@Test
public void test2() {
System.out.println("分支覆蓋測試");
int[] arr = {1, 2, 3, 4, 5, 6, 7, 9};
int item1 = 2;
int item2 = 7;
System.out.println(BinarySearch.binarySeach_02(arr, item1));
System.out.println(BinarySearch.binarySeach_02(arr, item2));
}
測試點3:謂詞完全覆蓋測試
這個謂詞覆蓋,我也是第一次聽說,這種概念的東西,其實不實用。簡單來說代碼中謂詞就是 !=, > <這樣的代碼。所以,下面設計用例其實和上面分支覆蓋是一樣的用例。
@Test
public void test3() {
System.out.println("謂詞覆蓋測試");
int[] arr = {1, 2, 3, 4, 5, 6, 7, 9};
int item1 = 0;
int item2 = 6;
System.out.println(BinarySearch.binarySeach_02(arr, item1));
System.out.println(BinarySearch.binarySeach_02(arr, item2));
}
上面查找0,覆蓋了while 中的!=這個判斷,查找6覆蓋了分支中大於和小於的判斷。
測試點4:缺陷測試(沒有完整覆蓋路徑)
缺陷就是用例只覆蓋了代碼中一部分代碼,例如一個列表,我們只查找一個元素,肯定一次執行不能覆蓋全部代碼。
@Test
public void test4() {
System.out.println("有缺陷");
int[] arr = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18};
int item1 = 11;
System.out.println(BinarySearch.binarySeach_02(arr, item1));
}
爲了解決這個缺陷問題,我們可以寫一個依次查找列表中每一個元素和一個不存在的元素,也能覆蓋全部代碼路徑。
@Test
public void test5() {
System.out.println("沒缺陷的覆蓋查詢");
int[] arr = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18};
int item1 = 0;
System.out.println(BinarySearch.binarySeach_02(arr, item1));
for (int i : arr) {
System.out.println(BinarySearch.binarySeach_02(arr, i));
}
}