數據結構與算法——查找算法-二分查找


簡單介紹

二分查找 也稱 折半查找(Binary Search),它是一種效率較高的查找方法。但是,折半查找要求線性表必須采用順序存儲結構,而且表中元素按關鍵字有序排列,說簡單點就是要求查找的數組是有序的

思路分析

  • 搜索過程從數組(有序的)的中間元素開始,如果中間元素正好是要查找的元素,則搜索過程結束;

  • 如果要查找元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,而且跟開始一樣從中間元素開始比較

  • 如果在某一步驟數組為空,則代表找不到。這種搜索算法每一次比較都使搜索范圍縮小一半

    看動圖體驗一下,下面的動圖是二分查找與順序查找的對比:

上面的思路如果看不懂,下面舉個例子並代碼實現。

請對一個 有序數組 進行二分查找 {1,8, 10, 89, 1000, 1234},輸入一個數查找該數組是否存在此數,並且輸出下 標,如果沒有就提示「沒有這個數」。

二分查找可以使用 遞歸非遞歸 實現,這里使用遞歸方式實現。

查找步驟:

  1. 首先確定該數組的中間下標

    int  mid = (left + right)/2
    
  2. 然后讓需要查找的數 findValarr[mid] 比較

    1. findVal > arr[i],說明要查找的數在數組
    2. findVal < arr[i] ,說明要查找的數在數組
    3. findVal == arr[i],說明已經找到,就返回

什么時候結束遞歸呢?

  1. 找到則結束遞歸

  2. 未找到,則結束遞歸

    left > right 時,表示整個數組已經遞歸完,說明沒有找到,結束遞歸。這里要動腦筋思考一下,它往左或往右查找卻沒有找到目標數, leftright的情況,腦子里走一遍過程。

    {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:這個算法很簡單,但是很實用。必須要掌握。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM