超詳細講解“二分查找”,你看不懂算我笨!

二分查找有着查找速度快,平均性能好等優點,但僅當列表是有序的時候,二分查找才管用。面試比較常考,今天我們具體看一下二分查找。


0. 我們先從一個場景開始瞭解吧

有一天,小明心血來潮去圖書館借了N本書,結果出圖書館的時候,警報響了,於是門衛大爺把小明攔下,要檢查一下哪本書沒有登記出借。小明正準備把每一本書在報警器下過一下,以找出引發警報的書,但是大爺露出不屑的眼神:你連二分查找都不會嗎?於是大爺把書分成兩堆,讓第一堆過一下報警器,報警器響;於是再把這堆書分成兩堆…… 最終,檢測了 logN 次之後,大爺成功的找到了那本引起警報的書,露出了得意和嘲諷的笑容。於是小明揹着剩下的書走了,心想:大爺果然還是我大爺!

是不是覺得二分查找很簡單?!其實思想是很簡單,但是有不少細節需要注意。正如Knuth 大佬(發明 KMP 算法的那位)所說:

Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky…

大致意思就是:思路很簡單,細節是魔鬼。

接下來我就帶大家來分析需要注意的細節,以及二分查找的巧妙運用。

1. 基本的二分查找

根據二分查找的思想,我們將其數學化,符號和意義如下:
二分查找(前提條件:數組有序)
nums:查找數組
t:待查找目標元素
初始化 left = 0,左邊界
right = nums.length -1,右邊界
mid = (left + right)/ 2,查找的中間位置
查找區間 [left, right]:while(left <= right)
查找目標元素的位置,無則返回-1

算法方程可表示如下:
在這裏插入圖片描述
有了基本思路,我們廢話少說,直接放碼過來!

Java實現代碼如下:

private int binarySearchWithR(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1; // 注意 [left, right]

        while(left <= right) { // 注意
            int mid = (right - left) / 2 + left; // mid = (left + right) / 2 的優化形式,防止溢出!
            if(nums[mid] == target)
                return mid;
            else if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid - 1; // 注意
        }
        return -1;
    }

需要注意的細節就是代碼中註明的地方,其實這幾個地方是相關的。
因爲初始化 right = 數組的長度 - 1;即最後一個元素,是可以取到的。此時的查找區間爲 [left,right],所以決定了 while(left <= right),判斷條件是可以加等號的。同時也決定了後續的 right = mid - 1;需要減1,否則可能導致下標越界。
其實也可以初始化 right = 數組的長度;是不可以取到的。此時的查找區間爲 [left,right),所以決定了 while(left < right),判斷條件是不可以加等號的。同時也決定了後續的 right = mid;不需要減1。
不取右邊界情況的代碼如下:

private int binarySearchWithoutR(int[] nums, int target) {
        int left = 0;
        int right = nums.length; // 注意 [left, right)

        while(left < right) { // 注意
            int mid = (right - left) / 2 + left;
            if(nums[mid] == target)
                return mid;
            else if (nums[mid] < target)
                left = mid + 1;
            else if (nums[mid] > target)
                right = mid; // 注意
        }
        return -1;
    }

所以,明白了二分查找的思路,注意這些細節,也就不會導致各種情況的混亂。

爲了更深入的理解二分查找的過程,下面將運行過程圖形化(做這些圖花了我一晚上,強迫症的我太不容易了T_T)

以下情況爲初始化 right = 數組長度 - 1 ,查找區間爲 [left, right]
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

2. 尋找左側邊界的二分查找

二分查找的巧妙運用一就是尋找一個數的左側邊界,如尋找 [1,2,4,4,5,6] 中,4 第一次出現的位置?也就是尋找 4 的左側邊界。

尋找左側邊界的二分查找(前提條件:數組有序)
nums:查找數組
t:待查找目標元素
初始化 left = 0,左邊界
right = nums.length -1,右邊界
mid = (left + right)/ 2,查找的中間位置
查找區間:while(left <= right)
查找目標元素第一次出現的位置(左側邊界),無則返回比它大的數的左側邊界

算法方程可表示如下:
在這裏插入圖片描述
Java實現

private int leftBound(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;

        while (left <= right) {
            int mid = (right - left) / 2 + left;
            if (nums[mid] == target) {
                right = mid -1; // 注意
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid -1;
            }
        }
        return left; // 注意
    }

可以看出,此代碼與二分查找唯一的不同就是注意的地方。當找到目標元素時,並不是直接返回,而是收緊右側邊界,繼續查找,以鎖定左側邊界。最後返回左側邊界
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

3. 尋找右側邊界的二分查找

同理,如尋找 [1,2,4,4,5,6] 中,4 最後一次出現的位置?也就是尋找 4 的右側邊界。

尋找右側邊界的二分查找(前提條件:數組有序)
nums:查找數組
t:待查找目標元素
初始化 left = 0,左邊界
right = nums.length -1,右邊界
mid = (left + right)/ 2,查找的中間位置
查找區間:while(left <= right)
查找目標元素最後一次出現的位置(右側邊界),無則返回比它小的數的右側邊界

算法方程可表示如下:
在這裏插入圖片描述
Java實現

private int rightBound(int[] nums, int target) {
        int left = 0, right = nums.length-1;

        while (left <= right) {
            int mid = (right - left) / 2 + left;
            if (nums[mid] == target) {
                left = mid + 1; // 注意
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            }
        }
        return right; // 注意
    }

可以看出,此代碼與二分查找唯一的不同也是注意的地方。當找到目標元素時,並不是直接返回,而是收緊在側邊界,繼續查找,以鎖定右側邊界。最後返回右側邊界
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

4. 總結

第一個,最基本的二分查找算法:

因爲我們初始化 right = nums.length - 1
所以決定了我們的「搜索區間」是 [left, right]
所以決定了 while (left <= right)
同時也決定了 left = mid+1 和 right = mid-1

因爲我們只需找到一個 target 的索引即可
所以當 nums[mid] == target 時可以立即返回

第二個,尋找左側邊界的二分查找:

因爲我們初始化 right = nums.length - 1
所以決定了我們的「搜索區間」是 [left, right]
所以決定了 while (left <= right)
同時也決定了 left = mid+1 和 right = mid-1

因爲我們需找到 target 的最左側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊右側邊界以鎖定左側邊界

第三個,尋找右側邊界的二分查找:

因爲我們初始化 right = nums.length - 1
所以決定了我們的「搜索區間」是 [left, right]
所以決定了 while (left <= right)
同時也決定了 left = mid+1 和 right = mid-1

因爲我們需找到 target 的最右側索引
所以當 nums[mid] == target 時不要立即返回
而要收緊左側邊界以鎖定右側邊界

如果以上內容你都能理解,那麼恭喜你,二分查找算法的細節不過如此。

5. 最後我們來用一道LeetCode的算法題驗收一下成果吧

牛客網鏈接
題目描述
統計一個數字在排序數組中出現的次數。
Input:
nums = 1, 2, 4, 4, 5, 6
K = 4

Output:
2

解題思路
我們可以找出目標值的左邊界和右邊界,然後用右邊界 - 左邊界 + 1 ,如 4 的左邊界爲 2,右邊界爲 3。但是這樣左右邊界的方法都要編寫,有一個巧妙的方法,可以偷懶只寫一個方法即可。

找出 4 的左邊界1(或右邊界1),再找出 4+1 的左邊界2(或4 - 1的右邊界2),用左邊界2 - 左邊界1 即可(或右邊界2 - 右邊界1)

我的解題代碼: 已通過

// 根據二分查找的思路,修改爲尋找一個數的左側邊界。
public class Solution {
    public int GetNumberOfK(int [] array , int k) {
       if(array == null || array.length == 0)    return 0;
       // 這個數第一次出現的位置(即左邊界),無此值則返回比它大的數的左側邊界
       int first = myleft_bound(array, k);
        // 這個數最後一次出現的位置(用目標值+1的左邊界),無此值則返回比它大的數的左側邊界
       int last = myleft_bound(array, k+1);
       return last - first;
    }
    
    // 尋找target的左側邊界,無則返回比它大的數的左側邊界
    private int myleft_bound(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;

        while (left <= right) {
            int mid = (right - left) / 2 + left;
            if (nums[mid] == target) {
                // 關鍵!!!基本的二分查找若找到是返回下標
                //因爲我們需找到 target 的最左側索引
                //所以當 nums[mid] == target 時不要立即返回
                //而要收緊右側邊界以鎖定左側邊界
                right = mid -1; 
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid -1; 
            }
        }
        // 返回左側邊界
        return left;
    }
}

6. 參考

二分查找算法詳解


以上。記錄一下這次學習的二分查找,分享給大家順便整理下思路。碼字做圖不易,若有幫助,點個贊鼓勵一下鴨~

發佈了8 篇原創文章 · 獲贊 3 · 訪問量 7319
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章