須要更多算法動圖詳解,能夠微信搜索[袁廚的算法小屋]java
今天給你們帶來的是二分查找及其變種的總結,你們必定要看到最後呀,用心滿滿,廢話很少說,讓導演幫咱們把鏡頭切到袁記菜館吧!程序員
袁記菜館內。。。。算法
店小二:掌櫃的,您進貨回來了呀,喲!今天您買這魚挺大呀!數組
袁廚:那是,這是我今天從我們江邊買的,以前一直去菜市場買,那裏的老貴了,你猜猜我今天買的多少錢一條。微信
店小二:以前的魚,30個銅板一條,今天的我猜26個銅板。3d
袁廚:貴了。指針
店小二:還貴呀!那我猜20個銅板!code
袁廚:仍是貴了。blog
店小二:15個銅板。排序
袁廚:便宜了
店小二:18個銅板
袁廚:恭喜你猜對了
上面的例子就用到了咱們的二分查找思想,若是你玩過相似的遊戲,那二分查找理解起來確定很輕鬆啦,下面咱們一塊兒征服二分查找吧!
二分查找也稱折半查找(Binary Search),是一種在有序數組中查找某一特定元素的搜索算法。咱們能夠從定義可知,運用二分搜索的前提是數組必須是有序的,這裏須要注意的是,咱們的輸入不必定是數組,也能夠是數組中某一區間的起始位置和終止位置
經過上面二分查找的定義,咱們知道了二分查找算法的做用及要求,那麼該算法的具體執行過程是怎樣的呢?
下面咱們經過一個例子來幫助咱們理解。咱們須要在 nums 數組中,查詢元素 8 的索引
int[ ] nums = {1,3,4,5,6,8,12,14,16}; target = 8
(1)咱們須要定義兩個指針分別指向數組的頭部及尾部,這是咱們在整個數組中查詢的狀況,當咱們在數組
某一區間進行查詢時,能夠輸入數組,起始位置,終止位置進行查詢。
(2)找出mid,該索引爲 mid =(left + right)/ 2,可是這樣寫有可能溢出,因此咱們須要改進一下寫成
mid = left +(right - left)/ 2 或者 left + ((right - left ) >> 1) 二者做用是同樣的,都是爲了找到兩指針的中
間索引,使用位運算的速度更快。那麼此時的 mid = 0 + (8-0) / 2 = 4
(3)此時咱們的 mid = 4,nums[mid] = 6 < target,那麼咱們須要移動咱們的 left 指針,讓left = mid + 1,下次則能夠在新的 left 和 right 區間內搜索目標值,下圖爲移動前和移動後
(4)咱們須要在 left 和 right 之間計算 mid 值,mid = 5 + (8 - 5)/ 2 = 6 而後將 nums[mid] 與 target 繼續比較,進而決定下次移動left 指針仍是 right 指針,見下圖
(5)咱們發現 nums[mid] > target,則須要移動咱們的 right 指針, 則 right = mid - 1;則移動事後咱們的 left 和 right 會重合,這裏是咱們的一個重點你們須要注意一下,後面會對此作詳細敘述。
(6)咱們須要在 left 和 right 之間繼續計算 mid 值,則 mid = 5 +(5 - 5)/ 2 = 5 ,見下圖,此時咱們將 nums[mid] 和 target 比較,則發現兩值相等,返回 mid 便可 ,若是不相等則跳出循環,返回 -1。
二分查找的執行過程以下
1.從已經排好序的數組或區間中,取出中間位置的元素,將其與咱們的目標值進行比較,判斷是否相等,若是相等
則返回。
2.若是 nums[mid] 和 target 不相等,則對 nums[mid] 和 target 值進行比較大小,經過比較結果決定是從 mid
的左半部分仍是右半部分繼續搜索。若是 target > nums[mid] 則右半區間繼續進行搜索,即 left = mid + 1; 若
target < nums[mid] 則在左半區間繼續進行搜索,即 right = mid -1;
動圖解析
下面咱們來看一下二分查找的代碼,能夠認真思考一下 if 語句的條件,每一個都沒有簡寫。
public static int binarySearch(int[] nums,int target,int left, int right) { //這裏須要注意,循環條件 while (left <= right) { //這裏須要注意,計算mid int mid = left + ((right - left) >> 1); if (nums[mid] == target) { return mid; }else if (nums[mid] < target) { //這裏須要注意,移動左指針 left = mid + 1; }else if (nums[mid] > target) { //這裏須要注意,移動右指針 right = mid - 1; } } //沒有找到該元素,返回 -1 return -1; }
二分查找的思路及代碼已經理解了,那麼咱們來看一下實現時容易出錯的地方
1.計算 mid 時 ,不能使用 (left + right )/ 2,不然有可能會致使溢出
2.while (left < = right) { } 注意括號內爲 left <= right ,而不是 left < right ,咱們繼續回顧剛纔的例子,若是咱們設置條件爲 left < right 則當咱們執行到最後一步時,則咱們的 left 和 right 重疊時,則會跳出循環,返回 -1,區間內不存在該元素,可是不是這樣的,咱們的 left 和 right 此時指向的就是咱們的目標元素 ,可是此時 left = right 跳出循環
3.left = mid + 1,right = mid - 1 而不是 left = mid 和 right = mid。咱們思考一下這種狀況,見下圖,當咱們的target 元素爲 16 時,而後咱們此時 left = 7 ,right = 8,mid = left + (right - left) = 7 + (8-7) = 7,那若是設置 left = mid 的話,則會進入死循環,mid 值一直爲7 。
下面咱們來看一下二分查找的遞歸寫法
public static int binarySearch(int[] nums,int target,int left, int right) { if (left <= right) { int mid = left + ((right - left) >> 1); if (nums[mid] == target) { //查找成功 return mid; }else if (nums[mid] > target) { //新的區間,左半區間 return binarySearch(nums,target,left,mid-1); }else if (nums[mid] < target) { //新的區間,右半區間 return binarySearch(nums,target,mid+1,right); } } //不存在返回-1 return -1; }
給定一個排序數組和一個目標值,在數組中找到目標值,並返回其索引。若是目標值不存在於數組中,返回它將會被按順序插入的位置。
你能夠假設數組中無重複元素。
示例 1:
輸入: [1,3,5,6], 5
輸出: 2
示例 2:
輸入: [1,3,5,6], 2
輸出: 1
示例 3:
輸入: [1,3,5,6], 7
輸出: 4
示例 4:
輸入: [1,3,5,6], 0
輸出: 0
這個題目徹底就和我們的二分查找同樣,只不過有了一點改寫,那就是將我們的返回值改爲了 left,具體實現過程見下圖
class Solution { public int searchInsert(int[] nums, int target) { int left = 0, right = nums.length-1; //注意循環條件 while (left <= right) { //求mid int mid = left + ((right - left ) >> 1); //查詢成功 if (target == nums[mid]) { return mid; //右區間 } else if (nums[mid] < target) { left = mid + 1; //左區間 } else if (nums[mid] > target) { right = mid - 1; } } //返回插入位置 return left; } }
上面咱們說了如何使用二分查找在數組或區間裏查出特定值的索引位置。可是咱們剛纔數組裏面都沒有重複值,查到返回便可,那麼咱們思考一下下面這種狀況
此時咱們數組裏含有多個 5 ,咱們查詢是否含有 5 能夠很容易查到,可是咱們想獲取第一個 5 和 最後一個 5 的位置應該怎麼實現呢?哦!咱們可使用遍歷,當查詢到第一個 5 時,咱們設立一個指針進行定位,而後到達最後一個 5 時返回,這樣咱們就能求的第一個和最後一個五了?由於咱們這個文章的主題就是二分查找,咱們可不能夠用二分查找來實現呢?固然是能夠的。
給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置。
若是數組中不存在目標值 target,返回 [-1, -1]。
示例 1:
輸入:nums = [5,7,7,8,8,10], target = 8
輸出:[3,4]
示例 2:
輸入:nums = [5,7,7,8,8,10], target = 6
輸出:[-1,-1]
示例 3:
輸入:nums = [], target = 0
輸出:[-1,-1]
這個題目很容易理解,咱們在上面說了如何使用遍歷解決該題,可是這個題目的目的就是讓咱們使用二分查找,咱們來逐個分析,先找出目標元素的下邊界,那麼咱們如何找到目標元素的下邊界呢?
咱們來重點分析一下剛纔二分查找中的這段代碼
if (nums[mid] == target) { return mid; }else if (nums[mid] < target) { //這裏須要注意,移動左指針 left = mid + 1; }else if (nums[mid] > target) { //這裏須要注意,移動右指針 right = mid - 1; }
咱們只需在這段代碼中修改便可,咱們再來剖析一下這塊代碼,nums[mid] == target 時則返回,nums[mid] < target 時則移動左指針,在右區間進行查找, nums[mid] > target時則移動右指針,在左區間內進行查找。
那麼咱們思考一下,若是此時咱們的 nums[mid] = target ,可是咱們不能肯定 mid 是否爲該目標數的左邊界,因此此時咱們不能夠返回下標。例以下面這種狀況。
此時 mid = 4 ,nums[mid] = 5,可是此時的 mid 指向的並非第一個 5,因此咱們須要繼續查找 ,由於咱們要找
的是數的下邊界,因此咱們須要在 mid 的值的左區間繼續尋找 5 ,那咱們應該怎麼作呢?咱們只需在
target <= nums[mid] 時,讓 right = mid - 1便可,這樣咱們就能夠繼續在 mid 的左區間繼續找 5 。是否是聽着有點繞,咱們經過下面這組圖進行描述。
其實原理很簡單,就是咱們將小於和等於合併在一塊兒處理,當 target <= nums[mid] 時,咱們都移動右指針,也就是 right = mid -1,還有一個須要注意的就是,咱們計算下邊界時最後的返回值爲 left ,當上圖結束循環時,left = 3,right = 2,返回 left 恰好時咱們的下邊界。咱們來看一下求下邊界的具體執行過程。
動圖解析
計算下邊界代碼
int lowerBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { //這裏須要注意,計算mid int mid = left + ((right - left) >> 1); if (target <= nums[mid]) { //當目標值小於等於nums[mid]時,繼續在左區間檢索,找到第一個數 right = mid - 1; }else if (target > nums[mid]) { //目標值大於nums[mid]時,則在右區間繼續檢索,找到第一個等於目標值的數 left = mid + 1; } } return left; }
計算上邊界時算是和計算上邊界時條件相反,
計算下邊界時,當 target <= nums[mid] 時,right = mid -1;target > nums[mid] 時,left = mid + 1;
計算上邊界時,當 target < nums[mid] 時,right = mid -1; target >= nums[mid] 時 left = mid + 1;恰好和計算下邊界時條件相反,返回right。
計算上邊界代碼
int upperBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { //求mid int mid = left + ((right - left) >> 1); //移動左指針狀況 if (target >= nums[mid]) { left = mid + 1; //移動右指針狀況 }else if (target < nums[mid]) { right = mid - 1; } } return left; }
題目完整代碼
class Solution { public int[] searchRange (int[] nums, int target) { int upper = upperBound(nums,target); int low = lowerBound(nums,target); //不存在狀況 if (upper < low) { return new int[]{-1,-1}; } return new int[]{low,upper}; } //計算下邊界 int lowerBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { //這裏須要注意,計算mid int mid = left + ((right - left) >> 1); if (target <= nums[mid]) { //當目標值小於等於nums[mid]時,繼續在左區間檢索,找到第一個數 right = mid - 1; }else if (target > nums[mid]) { //目標值大於nums[mid]時,則在右區間繼續檢索,找到第一個等於目標值的數 left = mid + 1; } } return left; } //計算上邊界 int upperBound(int[] nums, int target) { int left = 0, right = nums.length - 1; while (left <= right) { int mid = left + ((right - left) >> 1); if (target >= nums[mid]) { left = mid + 1; }else if (target < nums[mid]) { right = mid - 1; } } return right; } }
咱們在上面的變種中,描述瞭如何找出目標元素在數組中的上下邊界,而後咱們下面來看一個新的變種,如何從數組或區間中找出第一個大於或最後一個小於目標元素的數的索引,例 nums = {1,3,5,5,6,6,8,9,11} 咱們但願找出第一個大於 5的元素的索引,那咱們須要返回 4 ,由於 5 的後面爲 6,第一個 6 的索引爲 4,若是但願找出最後一個小於 6 的元素,那咱們則會返回 3 ,由於 6 的前面爲 5 最後一個 5 的索引爲 3。好啦題目咱們已經瞭解,下面咱們先來看一下如何在數組或區間中找出第一個大於目標元素的數吧。
找出第一個大於目標元素的數,大概有如下幾種狀況
1.數組包含目標元素,找出在他後面的第一個元素
2.目標元素不在數組中,數組內的部分元素大於它,此時咱們須要返回第一個大於他的元素
3.目標元素不在數組中,且數組中的全部元素都大於它,那麼咱們此時返回數組的第一個元素便可
4.目標元素不在數組中,且數組中的全部元素都小於它,那麼咱們此時沒有查詢到,返回 -1 便可。
既然咱們已經分析完全部狀況,那麼這個題目對我們就沒有難度了,下面咱們描述一下案例的執行過程
nums = {1,3,5,5,6,6,8,9,11} target = 7
上面的例子中,咱們須要找出第一個大於 7 的數,那麼咱們的程序是如何執行的呢?
上面的例子咱們已經弄懂了,那麼咱們看一下,當 target = 0時,程序應該怎麼執行呢?
OK!咱們到這一步就能把這個變種給整的明明白白的了,下面咱們看一哈程序代碼吧,也是很是簡單的。
public static int lowBoundnum(int[] nums,int target,int left, int right) { while (left <= right) { //求中間值 int mid = left + ((right - left) >> 1); //大於目標值的狀況 if (nums[mid] > target) { //返回 mid if (mid == 0 || nums[mid-1] <= target) { return mid; } else{ right = mid -1; } } else if (nums[mid] <= target){ left = mid + 1; } } //全部元素都小於目標元素 return -1; }
經過上面的例子咱們應該能夠徹底理解了那個變種,下面咱們繼續來看如下這種狀況,那就是如何找到最後一個小於目標數的元素。仍是上面那個例子
nums = {1,3,5,5,6,6,8,9,11} target = 7
查找最後一個小於目標數的元素,好比咱們的目標數爲 7 ,此時他前面的數爲 6,最後一個 6 的索引爲 5,此時咱們返回 5 便可,若是目標數元素爲 12,那麼咱們最後一個元素爲 11,仍小於目標數,那麼咱們此時返回 8,便可。這個變種其實算是上面變種的相反狀況,上面的會了,這個也徹底能夠搞定了,下面咱們看一下代碼吧。
public static int upperBoundnum(int[] nums,int target,int left, int right) { while (left <= right) { int mid = left + ((right - left) >> 1); //小於目標值 if (nums[mid] < target) { //看看是否是當前區間的最後一位,若是當前小於,後面一位大於,返回當前值便可 if (mid == right || nums[mid+1] >= target) { return mid; } else{ left = mid + 1; } } else if (nums[mid] >= target){ right = mid - 1; } } //沒有查詢到的狀況 return -1; }
哎嘛寫着寫着咋就那麼多了,太長了你們就不愛看啦,剩下的就放在下篇吧,我們下篇見呀!
若是這篇文章對您有一丟丟幫助的話,或者是能感覺到這篇文章的用心的話,那麼感謝您的點贊,在看,轉發呀,這樣我就滿血復活啦。
我是袁廚,一個酷愛用動圖解算法的年輕人,一個酷愛作飯的程序員,一個想和你一塊兒進步的小老弟。