---
title: "算法面試套路|二分查找(Binary Search)"
description: "Patterns for Coding Questions: Binary Search."
image: https://i.imgur.com/dIdSDDr.png
author: Hsins
tags: LeetCode, Coding Interview
robots:
breaks:
GA:
disqus:
---
# 算法面試套路|二分查找(Binary Search)
<p style="text-align: center">
<img src="https://i.imgur.com/dIdSDDr.png" height=200/>
</p>
> 本篇內容主要為 [Grokking the Coding Interview: Patterns for Coding Questions](https://www.educative.io/courses/grokking-the-coding-interview) 的翻譯與整理,行有餘力建議可以購買該專欄課程閱讀並在上面進行練習。
## 重點整理
- 二分查找(Binary Search)
- 策略:
- 題型:
- 排序陣列
## 題目匯總
- `0704` Binary Search
- `0744` Find Smallest Letter Greater Than Target
## [例題] Order-agnostic Binary Search
### 問題
給定一個已排序的整數陣列,查找給定 `key` 值是否存在於陣列中;但是題目並未告知排序是以升序排列或是降序排列,並且陣列中可能存在重複值。
撰寫一個函數,若 `key` 值存在於陣列中,返回其索引,否則返回 `-1`。
**Example 01**
```
Input : arr = [4, 6, 10], key = 10
Output : 2
```
**Example 02**
```
Input : arr = [1, 2, 3, 4, 5, 6, 7], key = 5
Output : 4
```
**Example 03**
```
Input : arr = [10, 6, 4], key = 10
Output : 0
```
**Example 04**
```
Input : arr = [10, 6, 4], key = 4
Output : 2
```
### 題解
為了讓事情更為簡單一些,首先我們假設輸入的陣列為升序排列,使用 **二分查找(binary search)** 的具體步驟如下:
1. 假設 `start` 指向陣列中的第一個元素,而 `end` 指向陣列中的最後一個元素,即:
```
int start = 0;
int end = arr.length - 1;
```
2. 透過 `start` 與 `end` 獲取 `middle`。在 Java 和 C++ 中,在大多數的狀況下可以透過 $\text{middle} = \text{(start + end)}/2$ 獲得,但是當 `start` 或 `end` 較大時可能會發生整數溢位(integer overflow)。因此建議採用以下的方式更加安全:
```
middle = start + (end - start)/2
```
3. 接著我們檢查 `key` 是否與 `middle` 所指向的元素相等,如果相等就返回索引值 `middle`。
4. 如果 `key` 與 `middle` 所指向的元素不相等,則需要確認兩件事:
- 如果 `key < arr[middle]`,我們可以得知在升序陣列中的 `key` 小於索引值 `middle` 後的所有元素,我們將查找範圍變更為 `end = mid - 1`
- 如果 `key > arr[middle]`,我們可以得知在升序陣列中的 `key` 大於索引值 `middle` 後的所有元素,我們將查找範圍變更為 `start = mid + 1`
5. 獲取新的 `start` 與 `end` 並重複執行上述 (2)-(4) 的步驟,當 `start` 大於 `end` 時,表示我們無法於陣列中找到對應的 `key` 值,此時返回 `-1`。
以範例 2 為例,執行過程如圖所示:
<p style="text-align: center">
<small>1. 初始狀態</small><br/>
<img src="https://i.imgur.com/775j2kr.png"><br/>
<small>2. <code>key > arr[middle]</code></small>,因此 <code>start = middle + 1</code><br/>
<img src="https://i.imgur.com/RWJkqRa.png"><br/>
<small>3. <code>key < arr[middle]</code></small>,因此 <code>end = middle - 1</code><br/>
<img src="https://i.imgur.com/KdRi7yu.png"><br/>
</p>
如果陣列為降序排列,則需要調整上述步驟 (4) 如下:
- 如果 `key > arr[middle]`,我們可以得知在降序陣列中的 `key` 大於索引值 `middle` 後的所有元素,我們將查找範圍變更為 `end = mid - 1`
- 如果 `key < arr[middle]`,我們可以得知在降序陣列中的 `key` 小於索引值 `middle` 後的所有元素,我們將查找範圍變更為 `start = mid + 1`
那麼要如何得知陣列是以升序排列還是降序排列呢?我們可以透過 `arr[start] < arr[end]` 進行判斷。
### 代碼
#### C++
``` cpp
class BinarySearch {
public:
static int search(const vector<int>& arr, int key) {
int start = 0;
int end = arr.size() - 1;
bool isAscending = arr[start] < arr[end];
while (start <= end) {
int mid = start + (end - start) / 2;
if (key == arr[mid]) return mid;
if (isAscending) {
if (key < arr[mid]) end = mid - 1;
else start = mid + 1;
} else {
if (key > arr[mid]) end = mid - 1;
else start = mid + 1;
}
}
return -1;
}
}
```
#### Java
``` java
class BinarySearch {
public static int search(int[] arr, int key) {
int start = 0;
int end = arr.length - 1;
boolean isAscending = arr[start] < arr[end];
while (start <= end) {
// calculate the middle of the current range
int mid = start + (end - start) / 2;
if (key == arr[mid]) return mid;
if (isAscending) {
if (key < arr[mid]) end = mid - 1;
else start = mid + 1;
} else {
if (key > arr[mid]) end = mid - 1;
else start = mid + 1;
}
}
return -1;
}
}
```
#### JavaScript
``` javascript
const binarySearch(arr, key) => {
let start = 0;
let end = arr.length - 1;
let isAscending = arr[start] < arr[end];
while (start <= end) {
let mid = Math.floor(start + (end - start) / 2);
if (key === arr[mid]) return mid;
if (isAscending) {
if (key < arr[mid]) end = mid - 1;
else start = mid + 1;
} else {
if (key > arr[mid]) end = mid - 1;
else start = mid + 1;
}
}
return -1;
}
```
#### Python
``` python
def binary_search(arr, key):
start = 0
end = len(arr) - 1
while start <= end:
mid = start + (end - start) // 2
if key == arr[mid]:
return mid
if isAscending:
if key < arr[mid]:
end = mid - 1
else:
start = mid + 1
else:
if key > arr[mid]:
end = mid - 1
else:
start = mid + 1
return -1
```
### 分析
- 時間複雜度:$\mathcal{O}(\log{n})$
- 空間複雜度:$\mathcal{O}(1)$
## [例題] Ceiling of a Number
### 問題
給定一個按照升序排列的整數陣列,找到 `key` 的天花板數(Ceiling Number),亦即給定陣列中,大於或等於 `key` 的最小元素。
撰寫一個函數,若滿足要求的天花板數存在於陣列中,返回其索引,否則返回 `-1`。
**Example 01**
```
Input : arr = [4, 6, 10], key = 6
Output : 1
```
**Example 02**
```
Input : arr = [1, 3, 8, 10, 15], key = 12
Output : 4
```
**Example 03**
```
Input : arr = [4, 6, 10], key = 17
Output : -1
```
**Example 04**
```
Input : arr = [4, 6, 10], key = -1
Output : 0
```
### 題解
這一題遵循 **二分查找(Binary Search)** 策略。我們可以採取類似 [Order Agnostic Binary Search](#例題-Order-agnostic-Binary-Search) 一題中的處理方式,嘗試在陣列中搜索 `key` 值,如果能夠找到 `key` 值則直接返回;如果無法找到 `key` 值,下一個更大的數字將由 `start` 指出。
以範例 2 為例,執行過程如圖所示:
<p style="text-align: center">
<small>1. 初始狀態</small><br/>
<img src="https://i.imgur.com/ogF6GiS.png"><br/>
<small>2. <code>key > arr[middle]</code></small>,因此 <code>start = middle + 1</code><br/>
<img src="https://i.imgur.com/kRC7W3F.png"><br/>
<small>3. <code>key < arr[middle]</code></small>,因此 <code>start = middle + 1</code><br/>
<img src="https://i.imgur.com/wgw9f4m.png"><br/>
</p>
由於我們不斷調整範圍來查找 `key` 值,當迴圈結束時,範圍的起始位置便指向了陣列中大於 `key` 值的最小數字。除此之外,我們可以在最一開始添加一個判斷,檢查 `key` 是否大於陣列中的最大數字。
### 代碼
#### C++
``` cpp
class CeilOfANumber {
public:
static int searchCeilingOfANumber(const vector<int>& arr, int key) {
if (key > arr[arr.size() - 1]) return -1;
int start = 0;
int end = arr.size() - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (key < arr[mid]) {
end = mid - 1;
} else if (key > arr[mid]) {
start = mid + 1;
} else {
return mid;
}
}
return start;
}
}
```
#### Java
``` java
class CeilingOfANumber {
public static int searchCeilingOfANumber(int[] arr, int key) {
if (key > arr[arr.length - 1]) return -1;
int start = 0;
int end = arr.length - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (key < arr[mid]) {
end = mid - 1;
} else if (key > arr[mid]) {
start = mid + 1;
} else {
return mid;
}
}
return start;
}
}
```
#### JavaScript
``` javascript
const searchCeilingOfANumber(arr, key) => {
const n = arr.length;
if (key > arr[n - 1]) return -1;
let start = 0;
let end = n - 1;
while (start <= end) {
mid = Math.floor(start + (end - start) / 2);
if (key < arr[mid]) {
end = mid - 1;
} else if (key > arr[mid]) {
start = mid + 1;
} else {
return mid;
}
}
return start;
}
```
#### Python
``` python
def search_ceiling_of_a_number(arr, key):
n = len(arr)
if key > arr[n - 1]:
return -1
start, end = 0, n - 1
while start <= end:
mid = start + (end - start) // 2
if key < arr[mid]:
end = mid - 1
elif key > arr[mid]:
start = mid + 1
else:
return mid
return start
```
### 分析
- 時間複雜度:$\mathcal{O}(\log{n})$
- 空間複雜度:$\mathcal{O}(1)$
## [例題] Next Letter
### 問題
給定一個以升序排序的小寫字元陣列,找出其中大於給定 `key` 值的最小字母。假設給定的陣列為一個 **循環列表(circular list)**,也就是說最後一個字母將與第一個字母相連;這也代表陣列中最小的字母,若大於陣列中最後一個字母的同時,將會是陣列中的第一個字母。
撰寫一個函數,找出給定 `key` 的下一個字母。
**Example 01**
```
Input : arr = ['a', 'c', 'f', 'h'], key = 'f'
Output : 'h'
```
**Example 02**
```
Input : arr = ['a', 'c', 'f', 'h'], key = 'b'
Output : 'c'
```
**Example 03**
```
Input : arr = ['a', 'c', 'f', 'h'], key = 'm'
Output : 'a'
```
**Example 04**
```
Input : arr = ['a', 'c', 'f', 'h'], key = 'h'
Output : 'a'
```
### 題解
這一題遵循 **二分查找(Binary Search)** 策略。我們可以採取類似 [Ceiling of A Number](#例題-Ceiling-of-a-Number) 一題中的處理方式,但要考慮以下不同處:
1. 陣列視作循環列表,這代表如果 `key` 值大於陣列中的最後一個字母,或者小於陣列中的第一個字母,則 `key` 的下一個字母即為陣列中的第一個字母。
2. 我們必須找到的是 `key` 的下一個字母(不能與 `key` 相同),這代表需要忽略 `key == arr[middle]` 的狀況,為了處理這樣的情形,我們將更新範圍異動為 `start = middle + 1`。
最後,我們返回的字母將透過 `start % arr_length` 訪問而非 `start`;如同上述第 (2) 點所討論的,比如最後一個字母恰好等於 `key`,此時我們需要返回的會是陣列中的第一個字母。
### 代碼
#### C++
``` cpp
class TwoSingleNumbers {
public:
static vector<int> findSingleNumbers(vector<int> &nums) {
// get the XOR of all numbers
int n1xn2 = 0;
for (int num : nums) n1xn2 ^= num;
// get the rightmost bit that is '1'
int rightmostSetBit = 1;
while ((rightmostSetBit & n1xn2) == 0) rightmostSetBit = rightmostSetBit << 1;
int num1 = 0;
int num2 = 0;
for (int num : nums) {
if ((num & rightmostSetBit) != 0) num1 ^= num;
else num2 ^= num;
}
return vector<int> {num1, num2};
}
}
```
#### Java
``` java
class NextLetter {
public static char searchNextLetter(char[] letters, char key) {
int n = letters.length;
if (key < letters[0] || key > letters[n - 1]) return letters[0];
int start = 0;
int end = n - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (key < letters[mid]) {
end = mid - 1;
} else {
start = mid + 1;
}
}
return letters[start % n];
}
}
```
#### JavaScript
``` javascript
const searchNextLetter(letters, key) => {
const n = letters.length;
if (key < letters[0] || key > letters[n - 1]) return letters[0];
let start = 0;
let end = n - 1;
while (start <= end) {
let mid = Math.floor(start + (end - start) / 2);
if (key < letters[mid]) {
end = mid - 1;
} else {
start = mid + 1;
}
}
return letters[start % n];
}
```
#### Python
``` python
def search_next_letter(letters, key):
n = len(letters)
if key < letters[0] or key > letters[n - 1]:
return letters[0]
start, end = 0, n - 1
while start <= end:
mid = start + (end - start) // 2
if key < letters[mid]:
end = mid - 1
else:
start = mid + 1
return letters[start % n]
```
### 分析
- 時間複雜度:$\mathcal{O}(\log{n})$
- 空間複雜度:$\mathcal{O}(1)$
## 參考資料
- [Coding Patterns: Modified Binary Search | emre.me](https://emre.me/coding-patterns/modified-binary-search/)