一 二分查找介紹
二分查找法作為一種常見的查找方法,將原本是線性時間提升到了對數時間范圍,大大縮短了搜索時間,但它有一個前提,就是必須在有序數據中進行查找。
出錯原因主要集中在判定條件和邊界值的選擇上,很容易就會導致越界或者死循環的情況。
譬如數組{1, 2, 3, 4, 5, 6, 7, 8, 9},查找元素6,用二分查找的算法執行的話,其順序為:
1. 第一步查找中間元素,即5,由於5<6,則6必然在5之后的數組元素中,那么就在{6, 7, 8, 9}中查找,
2. 尋找{6, 7, 8, 9}的中位數,為7,7>6,則6應該在7左邊的數組元素中,那么只剩下6,即找到了。
二分查找算法就是不斷將數組進行對半分割,每次拿中間元素和goal進行比較。
二分查找需要注意的問題:
(1)是否檢查參數的有效性
low/high是否相同,數組中是否存在記錄?low/high構成的區間是否有效?
(2)二分查找中值的計算
這是一個經典的話題,如何計算二分查找中的中值?試卷中,大家一般給出了兩種計算方法:
算法一: mid = (low + high) / 2
算法二: mid = low + (high – low)/2
乍看起來,算法一簡潔,算法二提取之后,跟算法一沒有什么區別。但是實際上,區別是存在的。
算法一的做法,當數組比較長的時候,low + high是會溢出的,(low + high)存在着溢出的風險,進而得到錯誤的mid結果,導致程序錯誤。
而算法二能夠保證計算出來的mid,一定大於low,小於high,不存在溢出的問題。
注意:在獲取中間值時,要使用:left + (right - left) / 2,而不是使用(left + right) / 2。這是因為如果left和right都是很大的int時,可能會導致left + right超過Integer.maxValue,導致溢出。
推薦寫法:int mid = (left + right) >>> 1 ;
left 和 high 都是整型最大值的時候,注意,此時 3232 位整型最大值它的二進制表示的最高位是 00,它們相加以后,最高位是 11 ,變成負數,但是再經過無符號右移 >>>(重點是忽略了符號位,空位都以 00 補齊),就能保證使用 + 在整型溢出了以后結果還是正確的。
遞歸調用存在着壓棧/出棧的開銷,其效率是比較低下的
二 基本用法
二分查找最基本的用法是在一個給定的有序數組或者列表中,判斷某個元素是否存在:
//簡單二分查找 public boolean binarySearch(int num,int [] nums){ int left = 0, right = nums.length - 1, mid; while(left <= right){ mid = left + (right - left)/2; if(num == nums[mid]){ return true; }else if(num < nums[mid]){ right = mid - 1; }else{ left = mid + 1; } } return false; }
其中,有幾個要注意的點:
循環的判定條件是:low <= high
為了防止數值溢出,mid = low + (high - low)/2
當 A[mid]
不等於target
時,high = mid - 1
或low = mid + 1
也可以用遞歸來實現:
//簡單二分查找 - 遞歸實現 public boolean binarySearch(int num,int [] nums,int left,int right){ if(left <= right){ int mid = left + (right - left)/2; if(num == nums[mid]){ return true; }else if(num > nums[mid]){ return binarySearch(int num,mid + 1,right); }else { return binarySearch(int num, left,mid - 1); } } return false; }
其查找時間為O(logn)
LeetCode原題:
35. 搜索插入位置:給定一個排序數組和一個目標值,在數組中找到目標值,並返回其索引。如果目標值不存在於數組中,返回它將會被按順序插入的位置。你可以假設數組中無重復元素。
https://leetcode-cn.com/problems/search-insert-position/
根據題意,可得判斷出這個問題等價於如下分析:
如果目標值(嚴格)大於排序數組的最后一個數,返回這個排序數組的長度,否則進入第 2 點。
返回排序數組從左到右,大於或者等於目標值的第 1 個數的索引。
public int searchInsert(int[] nums, int target) { int len = nums.length; if (nums[len - 1] < target) { return len; } int left = 0; int right = len - 1; while (left <= right) { int mid = (left + right) / 2; // 等於的情況最簡單,我們應該放在第 1 個分支進行判斷 if (nums[mid] == target) { return mid; } else if (nums[mid] < target) { // 題目要我們返回大於或者等於目標值的第 1 個數的索引 // 此時 mid 一定不是所求的左邊界, // 此時左邊界更新為 mid + 1 left = mid + 1; } else { // 既然不會等於,此時 nums[mid] > target // mid 也一定不是所求的右邊界 // 此時右邊界更新為 mid - 1 right = mid - 1; } } // 注意:一定得返回左邊界 left, // 如果返回右邊界 right 提交代碼不會通過 // 【注意】下面我嘗試說明一下理由,如果你不太理解下面我說的,那是我表達的問題 // 但我建議你不要糾結這個問題,因為我將要介紹的二分查找法模板,可以避免對返回 left 和 right 的討論 // 理由是對於 [1,3,5,6],target = 2,返回大於等於 target 的第 1 個數的索引,此時應該返回 1 // 在上面的 while (left <= right) 退出循環以后,right < left,right = 0 ,left = 1 // 根據題意應該返回 left, // 如果題目要求你返回小於等於 target 的所有數里最大的那個索引值,應該返回 right return left; }
三 二分查找變形
1 查找目標值區域的左邊界/查找與目標值相等的第一個位置/查找第一個不小於目標值數的位置
例如:
A = [1,3,3,5, 7 ,7,7,7,8,14,14]
target = 7
return 4
//查找目標值區域的左邊界/查找與目標值相等的第一個位置/查找第一個不小於目標值數的位置 public int binarySearch6(int num,int [] nums,int left,int right){ while(left <= right){ int mid = (right + left) >>> 1; if(num <= nums[mid]){//這里用 <= ,因此right最終會指向最近的小於num的元素,而left指向最左的num right = mid - 1; }else if(num > nums[mid]){ left = mid + 1; } } if(nums[left] == num){ return left; }else{ return -1; } } @Test public void test6() { int[] nums = {1, 2, 2,3,3,3, 5, 6, 7}; System.out.println(binarySearch6(3, nums,0,nums.length - 1)); }
在查找時,由於判斷條件是num <= nums[mid],因此right最終會指向等於num的數的前一位,之后left會一直加1,直到left = right+1,即最左邊等於num是位。
2 查找目標值區域的右邊界/查找與目標值相等的最后一個位置/查找最后一個不大於目標值數的位置
A = [1,3,3,5,7,7,7, 7 ,8,14,14]
target = 7
return 7
//查找目標值區域的右邊界/查找與目標值相等的最后一個位置/查找最后一個不大於目標值數的位置 public int binarySearch7(int num,int [] nums,int left,int right){ while(left <= right){ int mid = (right + left) >>> 1; if(num < nums[mid]){ right = mid - 1; }else if(num >= nums[mid]){ left = mid + 1; } } if(nums[right] == num){ return right; }else{ return -1; } } @Test public void test7() { int[] nums = {1, 2, 2,3,3,3, 5, 6, 7}; System.out.println(binarySearch7(3, nums,0,nums.length - 1)); }
此題以可變形為查找第一個大於目標值的數/查找比目標值大但是最接近目標值的數
,我們已經找到了最后一個不大於目標值的數,那么再往后進一位,返回high + 1
,就是第一個大於目標值的數。
劍指offer原題:
題目描述
統計一個數字在排序數組中出現的次數。
解題思路
正常的思路就是二分查找了,我們用遞歸的方法實現了查找k第一次出現的下標,用循環的方法實現了查找k最后一次出現的下標。
除此之外,還有另一種奇妙的思路,因為data中都是整數,所以我們不用搜索k的兩個位置,而是直接搜索k-0.5和k+0.5這兩個數應該插入的位置,然后相減即可。
方法1:按照上面的二分查找法,找到第一個和最后一個

//統計一個數字在排序數組中出現的次數。 public int binarySearch8(int num,int [] nums,int left,int right){ int min = findLeft(num,nums,left,right);//找到最左的 if (min == -1){ return 0; } int max = findRight(num,nums,left,right);//找到最右的 return max - min + 1; } public int findLeft(int num,int [] nums,int left,int right){ int mid; while (left <= right){ mid = (left + right) >>> 1; if (num <= nums[mid]){ right = mid - 1; }else if (num > nums[mid]){ left = mid + 1; } } if (nums[left] == num){ return left; }else { return -1; } } public int findRight(int num,int [] nums,int left,int right){ int mid; while (left <= right){ mid = (left + right) >>> 1; if (num < nums[mid]){ right = mid - 1; }else if (num >= nums[mid]){ left = mid + 1; } } if (nums[right] == num){ return right; }else { return -1; } } @Test public void test8() { int[] nums = {1, 2, 2,3,3,3,3,3, 5, 6, 7}; System.out.println(binarySearch8(3, nums,0,nums.length - 1)); }
方法二:由於都是整數,例如:1,3,4,6,8,9,9,9,11,查找9出現的次數,那么可以查找8.5和9.5兩個數應該插入的位置,因此相減可以得到:

//統計一個數字在排序數組中出現的次數。 public int binarySearch9(int num, int[] nums, int left, int right) { int min = findInsert(num - 0.5, nums, left, right);//找到最左的 int max = findInsert(num + 0.5, nums, left, right);//找到最右的 return max - min; } public int findInsert(double num, int[] nums, int left, int right) { int mid; while (left <= right) { mid = (left + right) >>> 1; if (num < nums[mid]) { right = mid - 1; } else{ left = mid + 1; } } return left; } @Test public void test9() { int[] nums = {1, 3, 4, 6, 8, 9, 9, 9, 9,11}; System.out.println(binarySearch9(9, nums, 0, nums.length - 1)); }
3 查找最后一個小於目標值的數/查找比目標值小但是最接近目標值的數
A = [1,3,3, 5 ,7,7,7,7,8,14,14]
target = 7
return 5
此題以可由第1 題變形而來,我們已經找到了目標值區域的下(左)邊界,那么再往左退一位,即low - 1
,就是最后一個小於目標值的數。其實low - 1
也是退出循環后high
的值,因為此時 high
剛好等於low - 1
,它小於low
,所以 while 循環結束。我們只要判斷high
是否超出邊界即可。
int low = 0, high = n, mid; while(low <= high){ mid = low + (high - low) / 2; if(target <= A[mid]){ high = mid - 1; }else{ low = mid + 1; } } return high < 0 ? -1 : high;
查找第一個大於目標值的數/查找比目標值大但是最接近目標值的數同理是題2的變形。
三 旋轉數組返回最小元素
1 查找旋轉數組的最小元素(假設不存在重復數字)
LeetCode:153. 尋找旋轉排序數組中的最小值 https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,1,2,4,5,6,7] 可能變為 [4,5,6,7,0,1,2] )。
請找出其中最小的元素。
你可以假設數組中不存在重復元素。
示例 1:
輸入: [3,4,5,1,2]
輸出: 1
示例 2:
輸入: [4,5,6,7,0,1,2]
輸出: 0
解析:
解法1:暴力法
搜索整個數組,找到其中的最小元素,這樣的時間復雜度是 O(N)其中 N 是給定數組的大小。

//LeetCode:153. 尋找旋轉排序數組中的最小值 public int findMin(int [] nums){ if (nums == null || nums.length == 0){ return 0; } int min = nums[0]; for(int i = 1;i<nums.length;i++){ if(nums[i] < min){ return nums[i]; } } return min; } @Test public void testFindMin() { int[] nums = {4,5,6,1,2,3}; System.out.println(findMin(nums)); }
解法二:二分查找
一個非常棒的解決該問題的辦法是使用二分搜索。在二分搜索中,我們找到區間的中間點並根據某些條件決定去區間左半部分還是右半部分搜索。
由於給定的數組是有序的,我們就可以使用二分搜索。然而,數組被旋轉了,所以簡單的使用二分搜索並不可行。
在這個問題中,我們使用一種改進的二分搜索,判斷條件與標准的二分搜索有些不同。
我們希望找到旋轉排序數組的最小值,如果數組沒有被旋轉呢?如何檢驗這一點呢?
如果數組沒有被旋轉,是升序排列,就滿足 last element > first element。
因此,如果nums[0] < nums[nems.length - 1],即數組沒有旋轉,因此返回nums[0]即可。
上面的例子中 3 < 4,因此數組旋轉過了。這是因為原先的數組為 [2, 3, 4, 5, 6, 7],通過旋轉較小的元素 [2, 3] 移到了后面,也就是 [4, 5, 6, 7, 2, 3]。因此旋轉數組中第一個元素 [4] 變得比最后一個元素大。
這意味着在數組中你會發現一個變化的點,這個點會幫助我們解決這個問題,我們稱其為變化點。
在這個改進版本的二分搜索算法中,我們需要找到這個點。下面是關於變化點的特點:
所有變化點左側元素 > 數組第一個元素
所有變化點右側元素 < 數組第一個元素
算法
找到數組的中間元素 mid。
如果中間元素 > 數組第一個元素,我們需要在 mid 右邊搜索變化點。
如果中間元素 < 數組第一個元素,我們需要在 mid 左邊搜索變化點。
上面的例子中,中間元素 6 比第一個元素 4 大,因此在中間點右側繼續搜索。
當我們找到變化點時停止搜索,當以下條件滿足任意一個即可:
nums[mid] > nums[mid + 1],因此 mid+1 是最小值。
nums[mid - 1] > nums[mid],因此 mid 是最小值。
在上面的例子中,標記左右區間端點。中間元素為 2,之后的元素是 7 滿足 7 > 2 也就是 nums[mid - 1] > nums[mid]。因此找到變化點也就是最小元素為 2。
public int findMin1(int [] nums){ if(nums == null || nums.length == 0){ return -1; } if(nums[0] < nums[nums.length - 1]){ return nums[0]; } //以下操作說明數組已經被翻轉 int left = 0,right = nums.length - 1; int mid ; while(left <= right){ mid = (left + right) >>> 1; if(nums[mid] > nums[mid + 1]){ return nums[mid + 1]; } if(nums[mid - 1] > nums[mid]){ return nums[mid]; } if(nums[mid] > nums[0]){//中間值大於最左邊值,則從右邊查找 left = mid + 1; }else{ right = mid - 1; } } return -1; }
復雜度分析
時間復雜度:和二分搜索一樣 O(\log N)
空間復雜度:O(1)
2 查找旋轉數組的最小元素(假設存在重復值)
LeetCode:154. 尋找旋轉排序數組中的最小值 II https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,1,2,4,5,6,7] 可能變為 [4,5,6,7,0,1,2] )。
請找出其中最小的元素。
注意數組中可能存在重復的元素。
示例 1:
輸入: [1,3,5]
輸出: 1
示例 2:
輸入: [2,2,2,0,1]
輸出: 0
說明:
這道題是 尋找旋轉排序數組中的最小值 的延伸題目。
允許重復會影響算法的時間復雜度嗎?會如何影響,為什么?
思路:
旋轉排序數組 nums 可以被拆分為 2 個排序數組 nums1 , nums2 ,並且 nums1任一元素 >= nums2任一元素;因此,考慮二分法尋找此兩數組的分界點 nums[i](即第 2 個數組的首個元素)。
設置 left, right 指針在 nums 數組兩端,mid 為每次二分的中點:
當 nums[mid] > nums[right]時,mid 一定在第 1 個排序數組中,i 一定滿足 mid < i <= right,因此執行 left = mid + 1;
當 nums[mid] < nums[right] 時,mid 一定在第 2 個排序數組中,i 一定滿足 left < i <= mid,因此執行 right = mid;
當 nums[mid] == nums[right] 時,是此題對比 153題 的難點(原因是此題中數組的元素可重復,難以判斷分界點 i 指針區間);
例如 [1, 0, 1, 1, 1] 和 [1, 1, 1, 0, 1],在 left = 0, right = 4, mid = 2 時,無法判斷 mid 在哪個排序數組中。
我們采用 right = right - 1 解決此問題,證明:
此操作不會使數組越界:因為迭代條件保證了 right > left >= 0;
此操作不會使最小值丟失:假設 nums[right]是最小值,有兩種情況:
若 nums[right]是唯一最小值:那就不可能滿足判斷條件 nums[mid] == nums[right],因為 mid < right(left != right 且 mid = (left + right) // 2 向下取整);
若 nums[right]不是唯一最小值,由於 mid < right 而 nums[mid] == nums[right],即還有最小值存在於 [left, right - 1] 區間,因此不會丟失最小值。
以上是理論分析,可以代入以下數組輔助思考:
[1, 2, 3]
[1, 1, 0, 1]
[1, 0, 1, 1, 1]
[1, 1, 1, 1]
時間復雜度 O(logN),在特例情況下會退化到 O(N)(例如 [1, 1, 1, 1])。
圖解:
public int findMin2(int [] nums){ if(nums == null || nums.length == 0){ return -1; } if(nums[0] < nums[nums.length - 1]){ return nums[0]; } //以下操作說明數組已經被翻轉 int left = 0,right = nums.length - 1; int mid = 0; while(left <= right){ mid = (left + right) >>> 1; if(nums[mid] > nums[right]){ left = mid + 1; } if(nums[mid] < nums[right]){ right = mid; }else{ right = right - 1; } } return left; } @Test public void testFindMin2() { int[] nums = {1,1,0,1,1,1,1,1,1,1,1,1,1,1,1}; System.out.println(findMin2(nums)); }
四 在旋轉排序數組中搜索
1 在旋轉排序數組中搜索(假設沒有重復項)
LeetCode:33. 搜索旋轉排序數組 https://leetcode-cn.com/problems/search-in-rotated-sorted-array/?utm_source=LCUS&utm_medium=ip_redirect_q_uns&utm_campaign=transfer2china
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,1,2,4,5,6,7] 可能變為 [4,5,6,7,0,1,2] )。
搜索一個給定的目標值,如果數組中存在這個目標值,則返回它的索引,否則返回 -1 。
你可以假設數組中不存在重復的元素。
你的算法時間復雜度必須是 O(log n) 級別。
示例 1:
輸入: nums = [4,5,6,7,0,1,2], target = 0
輸出: 4
示例 2:
輸入: nums = [4,5,6,7,0,1,2], target = 3
輸出: -1
解析:
方法一:
可分為如下兩步實現:
(1)通過3.1解法找到旋轉點,即數組中最小值,因此可以確定目標元素所在的數組。
(2)確定所在的數組后通過二分查找找到目標元素

//LeetCode:33. 搜索旋轉排序數組 https://leetcode-cn.com/problems/search-in-rotated-sorted-array/?utm_source=LCUS&utm_medium=ip_redirect_q_uns&utm_campaign=transfer2china public int findNum(int[] nums,int num){ if(nums == null || nums.length == 0){ return -1; } if(nums[0] < nums[nums.length - 1]){//表示沒有旋轉 -- 直接使用二分查找 return binarySearch10(nums,num,0,nums.length - 1); } if(num > nums[0]){//表示目標元素在左邊數組,即大數組 return binarySearch10(nums,num,0,reverseBinarySearch(nums) - 1); }else{//表示目標元素在右邊數組,即小數組 return binarySearch10(nums,num,reverseBinarySearch(nums), nums.length - 1); } } //二分查找 public int binarySearch10(int[] nums,int num,int left,int right){ if(nums == null || nums.length == 0){ return -1; } int mid = 0; while(left <= right){ mid = (left + right) >>> 1; if(num == nums[mid]){ return mid; }else if(num > nums[mid]){ left = mid + 1; }else{ right = mid - 1; } } return -1; } //旋轉數組二分查找 -- 找到最小的元素,即旋轉點 public int reverseBinarySearch(int[] nums){ int left = 0,right = nums.length - 1,mid = 0; while(left <= right){ mid = (left + right) >>> 1; if(nums[mid - 1] > nums[mid]){ return mid; } if(nums[mid] > nums[mid + 1]){ return mid + 1; } if(nums[mid] > nums[0]){ left = mid + 1; }else{ right = mid - 1; } } return 0; } @Test public void testFindNum() { int[] nums = {6,7,9,0,1,2,3,4,5}; System.out.println(findNum(nums,7)); }
方法二:
既然數組是遞增數組發生的旋轉,那么最左邊的元素一定是數組的中間大小的值,
拿示例來看,我們從 6 這個位置分開以后數組變成了 [4, 5, 6] 和 [7, 0, 1, 2] 兩個部分,其中左邊 [4, 5, 6] 這個部分的數組是有序的,其他也是如此。
這啟示我們可以在常規二分搜索的時候查看當前 mid 為分割位置分割出來的兩個部分 [left, mid] 和 [mid + 1, right] 哪個部分是有序的,並根據有序的那個部分確定我們該如何改變二分搜索的上下界,因為我們能夠根據有序的那部分判斷出 target 在不在這個部分:
如果 [left, mid - 1] 是有序數組,且 target 的大小滿足nums[left] <= target < nums[mid - 1],則我們應該將搜索范圍縮小至 [left, mid - 1],否則在 [mid + 1, right] 中尋找。
如果 [mid, right] 是有序數組,且 target 的大小滿足nums[mid + 1] < target <= nums[right],則我們應該將搜索范圍縮小至 [mid + 1, right],否則在 [left, mid - 1] 中尋找。
public static int search(int[] nums, int target) { // 邊界條件 if (nums == null || nums.length == 0) { return -1; } // 方法2:通過兩個指針查找 // int left = 0,right = nums.length - 1; while (left <= right){ int mid = left + (right - left)/2; if (nums[mid] == target){ return mid; } // 如果nums[mid] >= nums[0] 說明 0 - mid 是遞增的,及大數組 if (nums[mid] >= nums[0]){ // 如果nums[mid] > target && nums[0] < target 說明target在大數組里 if (nums[mid] > target && nums[0] <= target){ right = mid - 1; }else { left = mid+1; } }else { // 如果nums[mid] > target && nums[0] < target 說明target在小數組里 if (nums[mid] < target && nums[nums.length - 1] >= target){ left = mid+1; }else { right = mid - 1; } } } return -1; }
2 在旋轉排序數組中搜索(假設有重復項)
LeetCode:81. 搜索旋轉排序數組 II https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/?utm_source=LCUS&utm_medium=ip_redirect_q_uns&utm_campaign=transfer2china
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。
( 例如,數組 [0,0,1,2,2,5,6] 可能變為 [2,5,6,0,0,1,2] )。
編寫一個函數來判斷給定的目標值是否存在於數組中。若存在返回 true,否則返回 false。
示例 1:
輸入: nums = [2,5,6,0,0,1,2], target = 0
輸出: true
示例 2:
輸入: nums = [2,5,6,0,0,1,2], target = 3
輸出: false
解析:
(1)如果nums[mid] == target,返回true
(2)當數組為[1,2,1,1,1],nums[mid] == nums[left] == nums[right] != target,需要left++, right --;
(3)當nums[left] <= nums[mid],說明是在左半邊的遞增區域
a. nums[left] <= target < nums[mid],說明target在left和mid之間,我們令right = mid - 1;
b. 不在之間, 我們令 left = mid + 1;
(4)當nums[mid] < nums[right],說明是在右半邊的遞增區域
a. nums[mid] < target <= nums[right],說明target在mid 和right之間,我們令left = mid + 1
b. 不在之間,我們令right = mid - 1;
public boolean binarySearch11(int[] nums,int num){ if(nums == null || nums.length == 0){ return false; } int left = 0,right = nums.length - 1 ,mid = 0; while(left <= right){ mid = (left + right) >>> 1; if(nums[mid] == num){ return true; } if(nums[mid] == nums[left]){ left ++; }else if(nums[mid] > nums[left]){//說明左邊遞增 if(num < nums[mid] && num > nums[left]){//表示在左邊數組 right = mid - 1; }else{ left = mid + 1; } } if(nums[mid] == nums[right]){ right --; }else if(nums[mid] < nums[right]){//說明右邊遞增 if(num > nums[mid] && num < nums[right]){//表示在右邊數組 left = mid + 1; }else{ right = mid - 1; } } } return false; } @Test public void testBinarySearch11() { int[] nums = {1,3,1,1,1,1,1,1}; System.out.println(binarySearch11(nums,3)); }