去年九月份投了人生第一份簡歷,百度地圖公共出行部門的實習生,就抱着感覺面試+碰運氣的態度去面試了(結果可想而知,哈哈哈...Emm..面試官都挺好的,只不過三個部門的老大輪流面,每位大佬都面了一個小時可把我面死了,<ps:我猜想由於每一個部門的老大都不想要我,因此都給別的部門來看一看> 並且我仍是人生第一次面試, T.T) 其中第二位面試官問個人算法題讓我手撕 二分查找,當時還想,誒?這個面試官這麼好,出的題好簡單。如今刷了100道leetcode之後,再來審視這個問題發現,並不簡單...
其實你們也可能有和我同樣的想法,不就是二分查找嗎,有什麼可難的,
好,如今在沒有複習的狀況下我問你們幾個問題,你們自查一下: 循環的條件是left<right
仍是left<=right
?二分時是left=mid+1
仍是left=mid
一樣的是right = mid-1
仍是right=mid
?對於上面全部的問題:爲何這樣作?面試
若是你可以對每一步的作法都道的清楚,那麼恭喜你,這篇給像我這樣的小菜菜看和複習的文章您能夠跳過啦!算法
可是若是你和我同樣,爆了粗口F&%^!k,好的,先握個手,咱們先一塊兒去學習一下liwei大佬如何理解這個問題(篇幅較長,耐心消化):
https://www.liwei.party/2019/... 數組
好了啦,其實我這篇文章也只是對他的一個自我消化,方便我往後複習,以及複習我失敗的面試經歷(臥薪嚐膽啊懂不懂!)函數
liwei大佬的方法我以爲有這麼三點比較重要:學習
多說無益,直接上題:測試
Leetcode704: 二分查找code
給定一個 n 個元素有序的(升序)整型數組 nums 和一個目標值 target, 寫一個函數搜索 nums 中的 target,若是目標值存在返回下標,不然返回 -1。 示例 1: 輸入: nums = [-1,0,3,5,9,12], target = 9 輸出: 4 解釋: 9 出如今 nums 中而且下標爲 4
學會了以後先練個手,這很簡單吧,咱們這裏把思考過程寫清楚一點
這裏目標值target也給出了(PS:有的題目須要本身思考目標值),
1.首先肯定左邊界和右邊界 left = 0;
right = nums.length-1;
2.寫出while(left<right){}
3.肯定分支邏輯.對於這一題咱們能夠想到若是nums[mid]<target
,那麼mid及mid左側的元素裏必定不存在目標值,咱們縮減區間爲left = mid+1;
;那麼相應的另外一個分支爲num[mid]>=target
,這個分支的條件下 mid及mid左側的元素均可能是target(或者說mid右側的元素都不多是target),咱們縮減區間right = mid
;(對於這一題,其實先從nums[mid]>target的角度思考是同樣的啦)
4.咱們的分支邏輯中包括了left=mid+1
,爲了不死循環,咱們須要定義mid = left + (right-left)/2
(也就是當剩下偶數個元素時,選擇左邊的那個mid,同理當你的二分邏輯爲right=mid-1
時,須要定義mid = left + (right-left+1)/2
)
5.完成循環後,left==right,此時選left和right都是同樣的.但因爲target可能不存在數組中(能夠簡單思考一下這時候left指向了哪裏),所以須要特判一下 return nums[left]==target?left:-1;
完整代碼以下:排序
class Solution { public int search(int[] nums, int target) { int len = nums.length; int left =0; int right=len-1; if(len==0) return -1; while(left<right){ int mid = left + (right-left)/2; if(nums[mid]<target){ left = mid+1; } else{ right = mid; } } return nums[left]==target?left:-1; } }
來再作個應用題
Leetcode69: x的平方根索引
實現 int sqrt(int x) 函數。 計算並返回 x 的平方根,其中 x 是非負整數。 因爲返回類型是整數,結果只保留整數的部分,小數部分將被捨去。 示例 1: 輸入: 4 輸出: 2 示例 2: 輸入: 8 輸出: 2 說明: 8 的平方根是 2.82842..., 因爲返回類型是整數,小數部分將被捨去。
這一題求x的平方根取整,你固然能夠用庫函數,或者遍歷O(N)的複雜度,哈哈哈,可是咱不是在練習嘛,來來來,咱們用O(logN)時間複雜度,二分的方法作一下。
都是套路,繼續上模板
1.首先肯定左邊界和右邊界 left = 1;
right = x;
這裏right爲啥能夠等於能夠取到x了呢,由於一個數的平方根多是它本身啊(不用看了,1,說的就是你)
2.寫出while(left<right){}
3.肯定分支邏輯.注意,這裏由於咱們要找平方根,因此咱們的目標是求一個mid使得( mid * mid ) == x
.而且根據題意,小數部分被捨棄,即向下取整,那麼使得i * i <=x
的 i 值都有多是所求,但能夠肯定的是,使得i * i>x
的 i 值必定不是所求。據此,咱們能夠寫出分支邏輯:當mid * mid > x
時,mid及mid右側的元素必定不包括目標值,縮減區間爲right = mid - 1;
;那麼相似於上一個題目,稍(套)加(用)思(模)考(板)另外一else分支爲left = mid
4.因爲本題目的目標值必定存在,所以最後不須要特判返回便可.
完整代碼以下:leetcode
class Solution { public int mySqrt(int x) { // 0 特判一下 if(x==0) return 0; // 測試用例中存在大整數,所以用long long left = 1; long right = x; long mid; while (left<right){ mid = left + (right-left+1)/2; if(mid*mid>x){ right = mid-1; } else { left = mid; } } return (int)left; } }
"老闆!不過癮,再來一碗酒(題)!"
"呦,客官,您看咱們的招牌上寫了‘三碗不過岡’啊.."
行吧,既然你這麼要求了
Leetcode35: 搜索插入位置
給定一個排序數組和一個目標值,在數組中找到目標值, 並返回其索引。若是目標值不存在於數組中,返回它將會被按順序插入的位置。 你能夠假設數組中無重複元素。 示例 1: 輸入: [1,3,5,6], 5 輸出: 2 示例 2: 輸入: [1,3,5,6], 2 輸出: 1
其實這道題目和第一題大差不差,只不過這裏數組裏不存在目標值時還要求返回應該插入的位置.
由於邏輯判斷的思路基本一致,因此再也不贅述思考過程了,這裏只討論兩個不同的點
1.初始搜索區間爲left=0; right=nums.lenght
,由於須要找到插入的位置,所以須要把右邊界擴展
你可能會想:誒?當right=nums.length難道這樣不會越界嗎?
答:可能會。區間收縮的過程當中,可能收縮到這:[nums.length-1,nums.length], 而此時若是你選擇了nums.length爲mid 那麼就會出現越界.但若是你選擇了左邊的爲mid,緊接着left=mid+1, 此時left==right==nums.length,跳出循環,因此你選擇左邊的mid就不會出現越界
2.若是數組中不存在目標值,跳出循環時,邊界指向哪裏?在第一題中我也讓你們思考了這個問題。如今咱們來討論一下,能夠確定的是,此時兩個邊界值相同,且必定指向第一個小於目標值的元素或者第一個大於目標值的元素,那具體指向哪個呢?答案是包含了mid的那個分支條件,更具體來講即left=mid
或right=mid
中的其一,對應的即第一個小於目標值的元素或第一個大於目標值的元素.由於mid一直包含在這個分支中。
完整代碼以下:
public class Solution{ public static int searchInsert(int[] nums, int target) { if(nums.length==0) return 0; int left = 0; int right = nums.length; while (left<right){ int mid = left + (right-left)/2; // 若是mid 小於target ,mid及mid左側必定不是 if(nums[mid]<target){ left = mid+1; } // 若是 mid 大於等於target, mid或mid左側多是,但右側必定不是 else { right = mid; } } // 由於在變化的是左邊界,而右邊界包含了mid, // 最後邊界會收縮到右邊界的分支,即target的索引(找到了)或第一個>target的索引(沒找到) // 而沒找到時,這個索引恰好是應該插入的位置 return left; } }
這段代碼等價於:
public int searchInsert(int[] nums, int target) { if(nums.length==0) return 0; if(target<nums[0]) return 0; if(target>nums[nums.length-1]) return nums.length; int left = 0; int right = nums.length-1; while (left<right){ int mid = left + (right-left+1)/2; // 若是mid 大於target ,mid及mid右側必定不是 if(nums[mid]>target){ right = mid-1; } // 若是 mid 小於等於target, mid或mid右側多是,但左側必定不是 else { left = mid; } } // 由於在變化的是右邊界 因此最後區間會收縮到左邊界的條件 // 所以咱們須要判斷一下 return nums[left]==target?left:left+1; }
第二段代碼換了一個起始的思考過程,卻同時帶來了各類麻煩,咱們須要各類特判來解決麻煩。形成麻煩的緣由就在於跳出循環時,若該元素不存在,邊界的指向位置在應插入位置的左側。
你們能夠經過nums=[1,2] target=0; nums=[1,2] target=3
這個兩個例子本身手動模擬一遍兩種不一樣的方式,深入理解邊界的停留位置。
未完待續....