在我看來,大部分面試的算法題從來都不是難在思維,而是缺乏系統的教學。它不像數學屬於普及的基礎教育,算法題目的大部分知識、技巧往往都局限於 competitive programming 當中 (比如各種 OI 競賽、 ACM 競賽等),這些都是大部分計算機行業從業者接觸不到的。它就像一個大群體中一個半封閉的小群體一樣,系統的知識就在那裡,只是我們很少會主動走進去。因此,我期望將這些知識給帶出來,就引申出了本系列文章和視頻。
二分法是一種隨處可見卻又非常精妙的算法,我們最熟知的用法是在一個有序數組中查找某個 target 是否存在。初學二分法的同學可能會被各種邊界情況、不同寫法、是開區間還是閉區間等細節弄糊塗,以至於捨本逐末。其實並不需要如此,我們只需要記住一種最方便的寫法並理解它即可。
二分法主題分為兩篇,本篇為第一篇。共涉及到 60 道 題目,以 LeetCode 難度劃分屬於 hard 和 medium 偏上的題目。
注意:搭配 B 站視頻更香奧!
正文通常我們將二分法分為整數域上的二分和實數域上的二分。
整數二分的細節在於終止邊界、左右區間取捨時的開閉情況,來避免漏掉答案或者造成死循環。因此,本文會分別給出 整數二分 和 實數二分 的模板,來幫助大家解決 90% 以上的廣泛二分問題。廣泛二分問題指的是如下幾種類型:
最大/最小平均值(max/min average)理解了這幾種題型,也就能夠理解接近 100 道的算法題,基本都是 leetcode hard/medium 級別的。
在給出模板之前,我們得先理解什麼是二分?
二分不等價於序列上的單調性。它的本質是:
如果我們能夠通過某種特性將一個區間劃分成兩個區間,左邊區間滿足某種特性;右邊區間不滿足某種特性。那麼我們就能夠通過二分法來尋找左區間的右端點或者右區間的左端點。換句話講,有單調性一定可以二分,但是二分不一定要有單調性。
更多細節可以在 B 站搜索 《百學原理》 up主,面試向算法系列 - 1.4 二分法的經典應用
(圖 4-1)
整數二分模板def binary_search(l , r): # 模板一:尋找右區間的左端點(上圖紅色部分)
while l < r:
mid = (l+r)//2 # 注意 l+r
if check(mid): r = mid
else: l = mid+1def binary_search(l , r): # 模板二:尋找左區間的右端點(上圖藍色部分)
while l < r:
mid = (l+r+1)//2 # 注意 l+r+1
if check(mid): l = mid
else: r = mid-1整數二分為什麼會有兩個模板?
這是為了方便構造 check() 方法和對答案的求解。
我們來看 leetcode: 34. 在排序數組中查找元素的第一個和最後一個位置(題目視頻中有詳細講解)[1]。
給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置。你的算法時間複雜度必須是 O(log n) 級別。
如果數組中不存在目標值,返回 [-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]如何尋找 target 在數組中的開始位置呢?
很明顯,由於從 開始位置往後 所有的值都是
def check(x):
return x >= target也就是我們通過上述 check() 方法將數組劃分為兩個區間,開始位置就是右區間的左端點(也就是模板一),如下圖 4-2。
(圖 4-2)
那麼,我們是否可以用 模板二 求解開始位置呢?很顯然也是可以的,只是會麻煩一些,如圖 4-2 中的 4。我們可以通過 x < target 求得左區間的右端點,也就是最後一個 < target 的位置,假設為 i,那麼 i+1 顯然就是 target 的起始位置(這裡不考慮不存在情況)。
因此,我們可以得出一個結論:模板一 和 模板二 都可以得出具體解,只是在面對不同問題時哪個更加方便而已。
那麼該如何求解結束位置呢?
這個時候使用 模板二 就更加方便了,如圖 4-2 中的 3 所示,我們只要構造 check() 為 x <= target 即可。
下面我們來看模板的細節,一個很重要的點是:
模板一:mid = (l+r)//2; r = mid; l = mid+1模板二:mid = (l+r+1)//2; l = mid; r = mid+1為什麼要這樣寫呢?
這兩種寫法都是在解決同一個問題 - 避免範圍縮小到 2 時,產生死循環為什麼改變寫法就有可能產生死循環呢? 其實舉個反例即可。
本身 模板一就是為了獲得右區間的左端點,所以 r = mid 和 l = mid+1是肯定的,大家記住即可。那麼如果模板一變成 mid = (l+r+1)//2,也就是在 [i, i+1] 時, mid = i+1. 可能會產生如下圖所示的情況:(圖 4-3)
模板二的證明和模板一類似。
下面我們來看下模板一和模板二是如何運用在基礎題目裡面的(以下所有題目在 B站 1.4.1 和 1.4.2 中都有詳細講解)
(圖 4-4)
題目問的是 - 是否存在目標值,所以套用模板一和模板二都可以。稍微有點變形的是題目給出的序列是二維的,因此我們需要通過簡單的變形將二維轉為一維。
l = 0
r = m * n - 1
while l < r:
mid = (l+r)//2
row = mid // n
col = mid % n
if matrix[row][col] >= target: r = mid
else: l = mid+1
if matrix[l//n][l%n] == target: return True
else: return False
leetcode: 153. 尋找旋轉排序數組中的最小值[3]假設按照升序排序的數組在預先未知的某個點上進行了旋轉。( 例如,數組 [0,1,2,4,5,6,7] 可能變為 [4,5,6,7,0,1,2] )。請找出其中最小的元素。你可以假設數組中不存在重複元素。
(圖 4-5)
如上圖所示,可以直接根據模板一構造出 check() 方法(代碼就不給了)。
如果允許重複值呢?比如 leetcode: 154. 尋找旋轉排序數組中的最小值 II[4]
其實重複值只是迷惑了我們的眼睛,儘管在判斷到重複時(出現 check(mid) == nums[r]),我們無法劃分區間。但是很顯然,去掉其中一個不會對我們最終的結果產生影響,所以我們只需要 r-- 即可。
l = 0
r = n-1
while l < r:
mid = (l+r)//2
if nums[mid] == nums[r]:
r -= 1
elif nums[mid] > nums[r]:
l = mid + 1
else:
r = mid
return nums[l]按照這種做法,leetcode: 81. 搜索旋轉排序數組 II[5] 的解法也就顯而易見了。
除了完成上述題目,還有一些其他題目大家可以嘗試:
leetcode: 275. H 指數 II[7]實數二分模板binary_search(l , r): // 尋找結果
eps = 1e-6
while r - l >= eps:
mid = (l+r) / 2
if check(mid): r = mid
else: l = mid實數二分不會存在整數二分的細節問題,只需要注意精度問題即可。至於具體原因,大家簡單畫個圖即可,或者類比於微積分,當兩個點無限接近時,我們就認為其在無限小時是一個點。
實數二分最常見的例子就是:
二分法求三次方根 acwing: 790. 數的三次方根[9]具體代碼我就不給了,想要更詳細的過程可以看 B 站視頻 1.4.1 和 1.4.2
課後習題
acwing: 696. 哈默隊長(Google Kickstart2013 Practice Round Problem B)[10]總結剩下的 4 種經典應用請看下篇。
參考資料[1]leetcode: 34. 在排序數組中查找元素的第一個和最後一個位置(題目視頻中有詳細講解): https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/
[2]leetcode: 74. 搜索二維矩陣: https://leetcode-cn.com/problems/search-a-2d-matrix/
[3]leetcode: 153. 尋找旋轉排序數組中的最小值: https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array
[4]leetcode: 154. 尋找旋轉排序數組中的最小值 II: https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/
[5]leetcode: 81. 搜索旋轉排序數組 II: https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii/
[6]leetcode: 162. 尋找峰值: https://leetcode-cn.com/problems/find-peak-element/
[7]leetcode: 275. H 指數 II: https://leetcode-cn.com/problems/h-index-ii/
[8]codeforces edu 4 道題目: https://codeforces.com/edu/course/2/lesson/6/1/practice
[9]acwing: 790. 數的三次方根: https://www.acwing.com/problem/content/792/
[10]acwing: 696. 哈默隊長(Google Kickstart2013 Practice Round Problem B): https://www.acwing.com/problem/content/description/698/