二分查找法做爲一種常見的查找方法,將本來是線性時間提高到了對數時間範圍,大大縮短了搜索時間,具備很大的應用場景,而在 LeetCode 中,要運用二分搜索法來解的題目也有不少,可是實際上二分查找法的查找目標有不少種,並且在細節寫法也有一些變化。以前有網友留言但願博主能針對二分查找法的具體寫法作個總結,博主因爲以前一直很忙,一直拖着沒寫,爲了樹立博主言出必行的正面形象,不能再無限制的拖下去了,那麼今天就來作個了斷吧,總結寫起來~ (如下內容均爲博主本身的總結,並不權威,權當參考,歡迎各位大神們留言討論指正)html
根據查找的目標不一樣,博主將二分查找法主要分爲如下五類:面試
第一類: 需查找和目標值徹底相等的數數組
這是最簡單的一類,也是咱們最開始學二分查找法須要解決的問題,好比咱們有數組 [2, 4, 5, 6, 9],target = 6,那麼咱們能夠寫出二分查找法的代碼以下:函數
int find(vector<int>& nums, int target) { int left = 0, right = nums.size(); while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] == target) return mid; else if (nums[mid] < target) left = mid + 1; else right = mid; } return -1; }
會返回3,也就是 target 的在數組中的位置。注意二分查找法的寫法並不惟一,主要能夠變更地方有四處:post
第一處是 right 的初始化,能夠寫成 nums.size() 或者 nums.size() - 1。url
第二處是 left 和 right 的關係,能夠寫成 left < right 或者 left <= right。spa
第三處是更新 right 的賦值,能夠寫成 right = mid 或者 right = mid - 1。指針
第四處是最後返回值,能夠返回 left,right,或 right - 1。code
可是這些不一樣的寫法並不能隨機的組合,像博主的那種寫法,若 right 初始化爲了 nums.size(),那麼就必須用 left < right,而最後的 right 的賦值必須用 right = mid。可是若是咱們 right 初始化爲 nums.size() - 1,那麼就必須用 left <= right,而且right的賦值要寫成 right = mid - 1,否則就會出錯。因此博主的建議是選擇一套本身喜歡的寫法,而且記住,實在不行就帶簡單的例子來一步一步執行,肯定正確的寫法也行。htm
第一類應用實例:
第二類: 查找第一個不小於目標值的數,可變形爲查找最後一個小於目標值的數
這是比較常見的一類,由於咱們要查找的目標值不必定會在數組中出現,也有多是跟目標值相等的數在數組中並不惟一,而是有多個,那麼這種狀況下 nums[mid] == target 這條判斷語句就沒有必要存在。好比在數組 [2, 4, 5, 6, 9] 中查找數字3,就會返回數字4的位置;在數組 [0, 1, 1, 1, 1] 中查找數字1,就會返回第一個數字1的位置。咱們可使用以下代碼:
int find(vector<int>& nums, int target) { int left = 0, right = nums.size(); while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] < target) left = mid + 1; else right = mid; } return right; }
最後咱們須要返回的位置就是 right 指針指向的地方。在 C++ 的 STL 中有專門的查找第一個不小於目標值的數的函數 lower_bound,在博主的解法中也會時不時的用到這個函數。可是若是面試的時候人家不讓使用內置函數,那麼咱們只能老老實實寫上面這段二分查找的函數。
這一類能夠輕鬆的變形爲查找最後一個小於目標值的數,怎麼變呢。咱們已經找到了第一個不小於目標值的數,那麼再往前退一位,返回 right - 1,就是最後一個小於目標值的數。
第二類應用實例:
第三類: 查找第一個大於目標值的數,可變形爲查找最後一個不大於目標值的數
這一類也比較常見,尤爲是查找第一個大於目標值的數,在 C++ 的 STL 也有專門的函數 upper_bound,這裏跟上面的那種狀況的寫法上很類似,只須要添加一個等號,將以前的 nums[mid] < target 變成 nums[mid] <= target,就這一個小小的變化,其實直接就改變了搜索的方向,使得在數組中有不少跟目標值相同的數字存在的狀況下,返回最後一個相同的數字的下一個位置。好比在數組 [2, 4, 5, 6, 9] 中查找數字3,仍是返回數字4的位置,這跟上面那查找方式返回的結果相同,由於數字4在此數組中既是第一個不小於目標值3的數,也是第一個大於目標值3的數,因此 make sense;在數組 [0, 1, 1, 1, 1] 中查找數字1,就會返回座標5,經過對比返回的座標和數組的長度,咱們就知道是否存在這樣一個大於目標值的數。參見下面的代碼:
int find(vector<int>& nums, int target) { int left = 0, right = nums.size(); while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] <= target) left = mid + 1; else right = mid; } return right; }
這一類能夠輕鬆的變形爲查找最後一個不大於目標值的數,怎麼變呢。咱們已經找到了第一個大於目標值的數,那麼再往前退一位,返回 right - 1,就是最後一個不大於目標值的數。好比在數組 [0, 1, 1, 1, 1] 中查找數字1,就會返回最後一個數字1的位置4,這在有些狀況下是須要這麼作的。
第三類應用實例:
Kth Smallest Element in a Sorted Matrix
第三類變形應用示例:
第四類: 用子函數看成判斷關係(一般由 mid 計算得出)
這是最令博主頭疼的一類,並且一般狀況下都很難。由於這裏在二分查找法重要的比較大小的地方使用到了子函數,並非以前三類中簡單的數字大小的比較,好比 Split Array Largest Sum 那道題中的解法一,就是根據是否能分割數組來肯定下一步搜索的範圍。相似的還有 Guess Number Higher or Lower 這道題,是根據給定函數 guess 的返回值狀況來肯定搜索的範圍。對於這類題目,博主也很無奈,遇到了只能自求多福了。
第四類應用實例:
Split Array Largest Sum, Guess Number Higher or Lower,Find K Closest Elements,Find K-th Smallest Pair Distance,Kth Smallest Number in Multiplication Table,Maximum Average Subarray II,Minimize Max Distance to Gas Station,Swim in Rising Water,Koko Eating Bananas,Nth Magical Number
第五類: 其餘(一般 target 值不固定)
有些題目不屬於上述的四類,可是仍是須要用到二分搜索法,好比這道 Find Peak Element,求的是數組的局部峯值。因爲是求的峯值,須要跟相鄰的數字比較,那麼 target 就不是一個固定的值,並且這道題的必定要注意的是 right 的初始化,必定要是 nums.size() - 1,這是因爲算出了 mid 後,nums[mid] 要和 nums[mid+1] 比較,若是 right 初始化爲 nums.size() 的話,mid+1 可能會越界,從而不能找到正確的值,同時 while 循環的終止條件必須是 left < right,不能有等號。
相似的還有一道 H-Index II,這道題的 target 也不是一個固定值,而是 len-mid,這就很意思了,跟上面的 nums[mid+1] 有殊途同歸之妙,target 值都隨着 mid 值的變化而變化,這裏的right的初始化,必定要是 nums.size() - 1,而 while 循環的終止條件必須是 left <= right,這裏又必需要有等號,是否是很頭大 -.-!!!
其實仔細分析的話,能夠發現其實這跟第四類仍是比較類似,類似點是都很難 -.-!!!,第四類中雖然是用子函數來判斷關係,但大部分時候 mid 也會做爲一個參數帶入子函數進行計算,這樣實際上最終算出的值仍是受 mid 的影響,可是 right 卻能夠初始化爲數組長度,循環條件也能夠不帶等號,你們能夠對比區別一下~
第五類應用實例:
綜上所述,博主大體將二分搜索法的應用場景分紅了主要這五類,其中第二類和第三類還有各自的擴展。根據目前博主的經驗來看,第二類和第三類的應用場景最多,也是最重要的兩類。第一類,第四類,和第五類較少,其中第一類最簡單,第四類和第五類最難,遇到這類,博主也沒啥好建議,多多練習吧~
若是有寫的有遺漏或者錯誤的地方,請你們踊躍留言啊,共同進步哈~