前端面試官:請使用二分法搜索旋轉數組

標題黨一次~ 😼

本文首發於 www.shaotianyu.com/article/5ec…算法

目標是把本身知道的東西講清楚數組

1 基本的二分法是什麼樣子的

二分法的使用場景,其實比較受限,最明顯的特色是:函數

  • 絕大狀況,查找目標具備單調性質(單調遞增、單調遞減)
  • 有上下邊界,而且最好可以經過index下標訪問元素

1.1 從頭開始,基本的二分法使用

咱們從一個最簡單的單調遞增數組開始提及,問題以下: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
}
複製代碼

1.2 小結二分法的套路

咱們能夠從上面的問題中,看出點二分法的套路出來,二分法是有律可循的,而且能夠推導出基礎的模板: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
}
複製代碼

2 二分法的擴展

上面說過,二分法的特性之一是,存在明顯單調性。這樣的話,咱們的二分法模板纔有用武之地,但是事實上總會存在特殊的狀況。

2.1 旋轉數組系列 I

題目連接:尋找旋轉排序數組中的最小值

題目描述:

假設按照升序排序的數組在預先未知的某個點上進行了旋轉。( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。請找出其中最小的元素。你能夠假設數組中不存在重複元素。

解法分析:

  • 一、暴力求解,遍歷數組,記錄最小值,時間複雜度是 O(N),N是給定數組的大小
  • 二、二分法,時間複雜度是 O(logN),

咱們這裏怎麼使用二分法呢?如何去恰當地利用二分法的特性?如何改進一向的二分法?

2.1.1 撕開旋轉數組的外衣,解析它的規律

首先分解一下題目,題目的前提條件是 升序排序在未知的某個點旋轉,這樣的話咱們就要首先考慮到,如何判斷這個數組最後是升序排列仍是已經被旋轉打亂順序? 咱們先畫個圖看一下:

https://user-gold-cdn.xitu.io/2020/5/27/17251ebfa87eed61?w=1120&h=946&f=jpeg&s=40509

根據上面的圖,咱們能夠直觀看出一個規律,如何判斷是否是被旋轉打亂了?正常升序的前提條件是 first element < last element,而亂序的條件則是:first element > last element

而後再繼續挖掘一下亂序數組的規律,那就是亂中有序,如何理解呢 ?

https://user-gold-cdn.xitu.io/2020/5/29/1725c2ad476ba04f?w=1110&h=684&f=jpeg&s=23317

發現了沒有,在以黑色虛線爲分界點的左右兩側,都是分別升序的,而黑色虛線所在的那個分界點,也就是紅色箭頭指向的那個 Point,咱們能夠理解它爲 分界點,用來分界兩個升序數組。

因此咱們能夠總結如下規律:

  • 一、分界點的左側元素 >= 第一個元素
  • 二、分界點的右側元素 < 第一個元素

回到問題自己,咱們的出發點是要找數組中的最小值,如今看來,咱們能夠找什麼?咱們能夠找分界點分界點找到了,最小值就在分界點的旁邊,最小值就順便找到了。

問題的關鍵找到了,怎麼找分界點呢 ?

思路:

第一步:基於二分法的思路,先找mid

第二步:若mid > first element ,說明什麼?說明mid的左側是升序,最小值確定不在mid左邊,此時,咱們須要在mid的右邊找,因此 left = mid + 1

第三步:若mid < first element ,說明什麼?說明最小值確定在mid左邊,此時,咱們須要在mid的左邊找,因此 right = mid - 1

第四步:終止條件是什麼?分兩種狀況討論:

  • 一、若mid > mid + 1,此時mid + 1就是最小值,返回結果
  • 二、若mid < mid - 1,此時mid就是最小值,返回結果

總體思路清楚了,代碼就簡單了:

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
}
複製代碼

2.2 旋轉數組系列 II

題目連接:搜索旋轉排序數組

題目描述:

假設按照升序排序的數組在預先未知的某個點上進行了旋轉。( 例如,數組 [0,1,2,4,5,6,7] 可能變爲 [4,5,6,7,0,1,2] )。搜索一個給定的目標值,若是數組中存在這個目標值,則返回它的索引,不然返回 -1 。你能夠假設數組中不存在重複的元素。 你的算法時間複雜度必須是 O(log n) 級別。

算法時間複雜度必須是 O(log n) 級別,二分查找。

這個題目,和上面的很相似,是一類題目,這個題目的解法也有不少種,下面是比較常見的2種:

  • 一、根據2.1的最小值的索引,將數組分爲兩部分有序的數組,再根據num[0]和target的比較關係肯定target在分界點的左側仍是右側,而後在對應的一側升序數組進行二分查找
  • 二、直接對數組進行二分查找,而後在查找中肯定分界點的邊界關係

若是使用方法2,怎麼在二分查找的時候肯定邊界呢?

由上面2.1中,能夠得知,

若mid > first element,說明mid的左側是升序的;若mid < first element,說明mid的右側是升序的,而咱們經過這規律,就能夠區分兩段升序的數組,而後在對應的升序區間內,進行二分查找,而後不斷調整left和right的位置

咱們能夠先寫一下本道題的思路,思路清楚了,代碼就簡單了:

思路:

第一步:基於二分法的思路,先找mid

第二步:判斷mid 和 first element的大小關係,確立mid所在的區間

第三步:分兩部分討論:

  • 在左側升序區間中,若target >= left 同時 target < mid, 說明target在mid的左側,應該在[left, mid]之間找,此時執行right = mid - 1;不然target在mid的右側,在[mid, right]之間找, 此時left = mid + 1;
  • 在右側升序區間中,若target > mid 同時 target <= right , 說明target在mid的右側,應該在[mid, right]之間找,此時執行left = mid + 1;不然target在mid的左側,應該在[left, mid]之間找,此時right = mid -1
  • 終止條件是,mid element === target,結束
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
}
複製代碼

2.3 旋轉數組系列 III

題目連接:搜索旋轉排序數組 II

題目描述:

假設按照升序排序的數組在預先未知的某個點上進行了旋轉。( 例如,數組 [0,0,1,2,2,5,6] 可能變爲 [2,5,6,0,0,1,2] )。編寫一個函數來判斷給定的目標值是否存在於數組中。若存在返回 true,不然返回 false。

這個題目是2.2的變形,思路相似。

不一樣的點在於這裏的數組是含有重複元素的,咱們怎麼排除重複元素的干擾呢?好比[1,3,1,1,1]這種的數組,很難經過midleft element的比較界定兩個升序區間,不過咱們能夠經過不斷比較midleft是否相同,來排除重複的干擾項。

思路

若 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%的用戶

3 小結

上面是對二分法的基礎使用案例,和旋轉數組系列的基本套路作了一次小彙總,二分法還有不少經典的案例,後續會不斷補充。

我的能力有限,如有不足,還望指出。3Q。

相關文章
相關標籤/搜索