數據結構和算法-15-二分查找

前面學習了六種排序算法,接着學習搜索,搜索中使用最多一種簡單查找方法就是二分查找。二分查找的特點是,先保證數列是有序排序,然後每次查找可以減少一半的範圍,直到查到或者找不到目標元素爲止。這個也是經常在面試中被要求手寫這個查找代碼,接着要你設計測試用例去測試你寫的代碼。

 

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));
	}
}

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章