本文首發於 www.shaotianyu.com/article/5ec…算法
目標是把本身知道的東西講清楚數組
二分法的使用場景,其實比較受限,最明顯的特色是:函數
咱們從一個最簡單的單調遞增數組開始提及,問題以下:ui
在 [1, 2, 3, 4, 5, 6, 7, 8, 9] 中找到 4,若存在則返回下標,不存在返回-1,要求算法複雜度O(logn)spa
看到上面這題目,O(logn)複雜度的要求,第一反應就是使用二分查找法,怎麼作呢?3d
先在圖上模擬如下二分法的大概流程:指針
根據圖解,代碼以下:code
function searchNum (target, nums) {
if (!nums.length) return -1
let left = 0
let right = nums.length - 1
let mid
while (left <= right) {
// >> 1 位運算代替 除2 取整 操做
// 爲何不寫成 mid = (left+right)/2 ,由於考慮到left+right的溢出邊界狀況
mid = left + ((right - left) >> 1)
if (nums[mid] === target) {
return mid
}
if (nums[mid] < target) {
left = mid + 1
}
if (nums[mid] > target) {
right = mid - 1
}
}
return -1
}
複製代碼
咱們能夠從上面的問題中,看出點二分法的套路出來,二分法是有律可循的,而且能夠推導出基礎的模板:cdn
let left = start
let right = end
let mid
while (left <= right) {
mid = (left + right) / 2
if (array[mid] === target) {
return result 或者 break down
}
if (array[mid] < target) {
left = mid + 1
}
if (array[mid] > target) {
right = mid - 1
}
}
複製代碼
咱們獲得二分法的基礎模板後,就能夠順勢解決 x的平方根 這種類型的題目了~blog
附上 x的平方根 解題代碼:
const mySqrt = function(x) {
if (x < 2) return x
let left = 1, mid, right = Math.floor(x / 2);
while (left <= right) {
mid = Math.floor(left + (right - left) / 2)
if (mid * mid === x) return mid
if (mid * mid < x) {
left = mid + 1
}else {
right = mid - 1
}
}
return right
}
複製代碼
上面說過,二分法的特性之一是,存在明顯單調性。這樣的話,咱們的二分法模板纔有用武之地,但是事實上總會存在特殊的狀況。
題目連接:尋找旋轉排序數組中的最小值
題目描述:
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。請找出其中最小的元素。你能夠假設數組中不存在重複元素。
解法分析:
咱們這裏怎麼使用二分法呢?如何去恰當地利用二分法的特性?如何改進一向的二分法?
首先分解一下題目,題目的前提條件是 升序排序、在未知的某個點旋轉,這樣的話咱們就要首先考慮到,如何判斷這個數組最後是升序排列
仍是已經被旋轉打亂順序
? 咱們先畫個圖看一下:
根據上面的圖,咱們能夠直觀看出一個規律,如何判斷是否是被旋轉打亂了?正常升序的前提條件是 first element < last element
,而亂序的條件則是:first element > last element
而後再繼續挖掘一下亂序數組的規律,那就是亂中有序,如何理解呢 ?
發現了沒有,在以黑色虛線爲分界點的左右兩側,都是分別升序的,而黑色虛線所在的那個分界點,也就是紅色箭頭指向的那個 Point
,咱們能夠理解它爲 分界點
,用來分界兩個升序數組。
因此咱們能夠總結如下規律:
回到問題自己,咱們的出發點是要找數組中的最小值,如今看來,咱們能夠找什麼?咱們能夠找分界點
,分界點
找到了,最小值就在分界點的旁邊,最小值就順便找到了。
問題的關鍵找到了,怎麼找分界點呢 ?
思路:
第一步:基於二分法的思路,先找mid
第二步:若mid > first element ,說明什麼?說明mid的左側是升序,最小值確定不在mid左邊,此時,咱們須要在mid的右邊找,因此 left = mid + 1
第三步:若mid < first element ,說明什麼?說明最小值確定在mid左邊,此時,咱們須要在mid的左邊找,因此 right = mid - 1
第四步:終止條件是什麼?分兩種狀況討論:
總體思路清楚了,代碼就簡單了:
const findMin = function (nums) {
if(!nums.length) return null
if(nums.length === 1) return nums[0]
let left = 0, right = nums.length - 1, mid
// 此時數組單調遞增,first element就是最小值
if (nums[right] > nums[left]) return nums[0]
while (left <= right) {
mid = left + ((right - left) >> 1)
if (nums[mid] > nums[mid + 1]) {
return nums[mid + 1]
}
if (nums[mid] < nums[mid - 1]) {
return nums[mid]
}
if (nums[mid] > nums[0]) {
left = mid + 1
} else {
right = mid - 1
}
}
return null
}
複製代碼
題目連接:搜索旋轉排序數組
題目描述:
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。搜索一個給定的目標值,若是數組中存在這個目標值,則返回它的索引,不然返回 -1 。你能夠假設數組中不存在重複的元素。 你的算法時間複雜度必須是 O(log n) 級別。
算法時間複雜度必須是 O(log n) 級別,二分查找。
這個題目,和上面的很相似,是一類題目,這個題目的解法也有不少種,下面是比較常見的2種:
分界點
的左側仍是右側,而後在對應的一側升序數組進行二分查找分界點
的邊界關係若是使用方法2,怎麼在二分查找的時候肯定邊界呢?
由上面2.1中,能夠得知,
若mid > first element,說明mid的左側是升序的;若mid < first element,說明mid的右側是升序的,而咱們經過這規律,就能夠區分兩段升序的數組,而後在對應的升序區間內,進行二分查找,而後不斷調整left和right的位置
咱們能夠先寫一下本道題的思路,思路清楚了,代碼就簡單了:
思路:
第一步:基於二分法的思路,先找mid
第二步:判斷mid 和 first element的大小關係,確立mid所在的區間
第三步:分兩部分討論:
const search = function(nums, target) {
if (!nums.length) return -1
let left = 0, right = nums.length - 1, mid
while (left <= right) {
mid = left + ((right - left) >> 1)
if (nums[mid] === target) {
return mid
}
if (nums[mid] >= nums[left]) {
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1
} else {
left = mid + 1
}
} else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1
} else {
right = mid - 1
}
}
}
return -1
}
複製代碼
題目連接:搜索旋轉排序數組 II
題目描述:
假設按照升序排序的數組在預先未知的某個點上進行了旋轉。( 例如,數組 [0,0,1,2,2,5,6] 可能變爲 [2,5,6,0,0,1,2] )。編寫一個函數來判斷給定的目標值是否存在於數組中。若存在返回 true,不然返回 false。
這個題目是2.2的變形,思路相似。
不一樣的點在於這裏的數組是含有重複元素的,咱們怎麼排除重複元素的干擾呢?好比[1,3,1,1,1]
這種的數組,很難經過mid
和left element
的比較界定兩個升序區間,不過咱們能夠經過不斷比較mid
和left
是否相同,來排除重複的干擾項。
思路
若 mid element === left element:
此時說明具備重複項目,應該調整left指針,使left向右移動,用以去除重複干擾
複製代碼
將上面的思路轉化爲代碼,加入到2.2裏面,就能夠獲得這道題的解:
const search = function(nums, target) {
if (!nums.length) return false
let left = 0, right = nums.length - 1, mid
while (left <= right) {
mid = left + ((right - left) >> 1)
if (nums[mid] === target) {
return true
}
if (nums[left] === nums[mid]) {
++left
continue
}
if (nums[mid] >= nums[left]) {
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1
} else {
left = mid + 1
}
} else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1
} else {
right = mid - 1
}
}
}
return false
}
複製代碼
成功AC~
執行用時 :56 ms, 在全部 JavaScript 提交中擊敗了98.31%的用戶
上面是對二分法的基礎使用案例,和旋轉數組系列的基本套路作了一次小彙總,二分法還有不少經典的案例,後續會不斷補充。
我的能力有限,如有不足,還望指出。3Q。