二分查找的那些事兒

二分查找算法,是一種在有序數組中查找某一特定元素的搜索算法。算法

注意兩點:數組

(1)有序:查找以前元素必須是有序的,能夠是數字值有序,也能夠是字典序。爲何必須有序呢? 若是部分有序或循環有序能夠嗎?函數

(2)數組:全部邏輯相鄰的元素在物理存儲上也是相鄰的,確保能夠隨機存取。學習

 

算法思想:spa

搜素過程從數組的中間元素開始,若是中間元素正好是要查找的元素,則搜素過程結束;若是某一特定元素大於或者小於中間元素,則在數組大於或小於中間元素的那一半中查找,並且跟開始同樣從中間元素開始比較。若是在某一步驟數組爲空,則表明找不到。這種搜索算法每一次比較都使搜索範圍縮小一半。指針

 

這裏咱們能夠看到:code

(1) 若是查找值和中間值不相等的時候,咱們能夠確保能夠下次的搜索範圍能夠縮小一半,正是因爲全部元素都是有序的這一先決條件對象

(2) 咱們每次查找的範圍都是 理應包含 查找值的區間,當搜索中止時,若是仍未查找到,那麼此時的搜索位置就應該是 查找值 應該處於的位置,只是該值不在數組中而已blog

 

算法實現及各類變形:內存

1. 非降序數組A, 查找 任一個  值==val的元素,若找到則返回下標位置,若未找到則返回-1

2. 非降序數組A, 查找 第一個  值==val的元素,若找到則返回下標位置,若未找到則返回-1 (相似:查找數組中元素最後一個 小於 val 值 的位置)

3. 非降序數組A, 查找 最後一個值==val的元素,若找到則返回下標位置,若未找到則返回-1 (相似:查找數組中元素 第一個 大於 val 值 的位置)

4. 非降序數組A, 查找任一 值爲val的元素,保證插入該元素後 數組仍然有序,返回能夠插入的任一位置

5. 非降序數組A, 查找任一 值爲val的元素,保證插入該元素後 數組仍然有序,返回能夠插入的第一個位置

6. 非降序數組A, 查找任一 值爲val的元素,保證插入該元素後 數組仍然有序,返回能夠插入的最後一個位置

7. 非降序數組A, 查找 任一個  值==val的元素,若找到則 返回一組下標區間(該區間全部值 ==val),若未找到則返回-1

8. 非降序字符串數組A, 查找 任一個  值==val的元素,若找到則返回下標位置,若未找到則返回-1(相似:未找到時返回應該插入點)

9. 循環有序數組中查找 == val 的元素,若找到則返回下標位置,若未找到則返回-1

10.非降序數組A,查找絕對值最小的元素,返回其下標位置

 

1. 非降序數組A, 查找 任一個  值==val的元素,若找到則返回下標位置,若未找到則返回-1

 1 int binary_search(int* a, int len, int val)
 2 {
 3     assert(a != NULL && len > 0);
 4     int low = 0;
 5     int high = len - 1;
 6     while (low <= high) {
 7         int mid = low + (high - low) / 2;
 8         if (val < a[mid]) {
 9             high = mid - 1;
10         } else if (val > a[mid]) {
11             low = mid + 1;
12         } else {
13             return mid;
14         }
15     }
16     return -1; 17 }

注意:

(1) 使用assert對函數輸入進行合法性檢查

(2) while 循環的條件是 low<=high,這裏若是查找值未找到,則此時必定 low = high + 1 

(3) 對 val 和 a[mid] 作比較時,首先考慮不等狀況,最後考慮相等狀況,若是隨機分佈的話 不等的機率確定 大於 相等的機率

 

2. 非降序數組A, 查找 第一個  值==val的元素,若找到則返回下標位置,若未找到則返回-1 (相似:查找數組中元素最後一個 小於 val 值 的位置)

由於數組中可能有重複元素,因此數組中是有可能存在多個值與 val 相等的,咱們對普通二分進行變形:

當 val < a[mid] 時, 接下來的搜索範圍減半  high = mid - 1

當 val > a[mid] 時, 接下來的搜索範圍減半  low  = mid + 1

當 val == a[mid] 時,這個時候就不能簡單的返回了,咱們要求的是第一個 == val 的值,什麼條件下是第一個呢?

                             當 mid == 0 那固然是第一個

                             當 mid > 1 && a[mid - 1] != val 這個時候也是第一個

                             其餘狀況下,這個時候查找到的值不是第一個,此時咱們應該繼續搜索,而不是返回,搜索範圍是什麼呢? 由於是查找第一個,那麼接下來確定應該在

                             此時位置的左邊繼續搜索,即 high = mid - 1

 

 1 int search_first(int* a, int len, int val)
 2 {
 3     assert(a != NULL && len > 0);
 4     int low = 0;
 5     int high = len - 1;
 6     while (low <= high) {
 7         int mid = low + (high - low) / 2;
 8         if (val < a[mid]) {
 9             high = mid - 1;
10         } else if (val > a[mid]) {
11             low = mid + 1;
12         } else {
13             if (mid == 0) return mid;
14             if (mid > 0 && a[mid-1] != val) return mid;
15             high = mid - 1;
16         }
17     }
18     return -1; 19 }

 

3. 非降序數組A, 查找 最後一個值==val的元素,若找到則返回下標位置,若未找到則返回-1 (相似:查找數組中元素 第一個 大於 val 值 的位置)

算法思想與 第2題 相同

 1 int search_last(int* a, int len, int val)
 2 {
 3     assert(a != NULL && len > 0);
 4     int low = 0;
 5     int high = len - 1;
 6     while (low <= high) {
 7         int mid = low + (high - low) / 2;
 8         if (val < a[mid]) {
 9             high = mid - 1;
10         } else if (val > a[mid]) {
11             low = mid + 1;
12         } else {
13             if (mid == (len - 1)) return mid;
14             if (mid < (len - 1) && a[mid+1] != val) return mid;
15             low = mid + 1;
16         }
17     }
18     return -1; 19 }

 

4. 非降序數組A, 查找任一 值爲val的元素,保證插入該元素後 數組仍然有序,返回能夠插入的任一位置

當 a[mid] == val 則返回 mid,由於在該位置插入 val 數組必定保證有序

當 循環結束後 仍未查找到 val值,咱們以前說過,此時 必定有 high = low + 1,其實查找值永遠都 應該在 low和high組成的區間內,如今區間內沒空位了,因此能夠宣告該值沒有查找到,

若是仍然有空位,則val必定在該區間內。也就是說此時的 low 和 high 這兩個值就是 val 應該處於的位置,由於一般都是在位置以前插入,因此此時直接返回 low 便可

 1 int insert(int* a, int len, int val)
 2 {
 3     assert(a != NULL && len > 0);
 4     int low = 0;
 5     int high = len - 1;
 6     while (low <= high) {
 7         int mid = low + (high - low) / 2;
 8         if (val < a[mid]) {
 9             high = mid - 1;
10         } else if (val > a[mid]) {
11             low = mid + 1;
12         } else {
13             return mid;
14         }
15     }
16     return low; 17 }

 

5. 非降序數組A, 查找任一 值爲val的元素,保證插入該元素後 數組仍然有序,返回能夠插入的第一個位置

由於是要求第一個能夠插入的位置,當查找值不在數組中時,插入的位置是惟一的,即 return low

當查找值出如今數組中時,此時就演變成了 查找第一個 == val 的值,詳見 第2題

 1 int insert_first(int* a, int len, int val)
 2 {
 3     assert(a != NULL && len > 0);
 4     int low = 0;
 5     int high = len - 1;
 6     while (low <= high) {
 7         int mid = low + (high - low) / 2;
 8         if (val < a[mid]) {
 9             high = mid - 1;
10         } else if (val > a[mid]) {
11             low = mid + 1;
12         } else {
13             if (mid == 0) return mid;
14             if (mid > 0 && a[mid-1] != val) return mid;
15             high = mid - 1;
16         }
17     }
18     return low; 19 }

 

6. 非降序數組A, 查找任一 值爲val的元素,保證插入該元素後 數組仍然有序,返回能夠插入的最後一個位置

算法思想與第 5 題相同

 1 int insert_last(int* a, int len, int val)
 2 {
 3     assert(a != NULL && len > 0);
 4     int low = 0;
 5     int high = len - 1;
 6     while (low <= high) {
 7         int mid = low + (high - low) / 2;
 8         if (val < a[mid]) {
 9             high = mid - 1;
10         } else if (val > a[mid]) {
11             low = mid + 1;
12         } else {
13             if (mid == (len - 1)) return mid;
14             if (mid < (len - 1) && a[mid+1] != val) return mid;
15             low = mid + 1;
16         }
17     }
18     return low; 19 }

 

7. 非降序數組A, 查找 任一個  值==val的元素,若找到則 返回一組下標區間(該區間全部值 ==val),若未找到則返回-1

咱們首先想到的是根據 第 1 題 進行稍微修改,當 a[mid] == val 時,並不當即 return mid,而是 以 mid 爲中心 向左右兩邊搜索 獲得全部值 == val 的區間

注意此算法時間複雜度可能O(n) 當數組中全部值都等於val時,此算法的複雜度爲 O(n)

聯想到第 2 題 和 第 3 題,咱們能夠首先找到第一個 == val 的下標,而後找到最後一個 == val 的下標,兩下標即爲所求,此時,算法複雜度爲 2*log(n) 爲最優方法

具體算法實現 此處略去

 

 8.  非降序字符串數組A, 查找 任一個  值==val的元素,若找到則返回下標位置,若未找到則返回-1(相似:未找到時返回應該插入點)

注意咱們這是字符串數組,其實 這和 第 1 題基本相同,只是 元素作比較時 對象時字符串而已

 1 int binary_search(char* a[], int len, char* val)
 2 {
 3     assert(a != NULL && len > 0 && val != NULL);
 4     int low = 0;
 5     int high = len - 1;
 6     while (low <= high) {
 7         int mid = low + (high - low) / 2;
 8         if (strcmp(val, a[mid]) < 0) {
 9             high = mid - 1;
10         } else if (strcmp(val, a[mid]) > 0) {
11             low = mid + 1;
12         } else {
13             return mid;
14         }
15     }
16     return -1;   // or return low
17 }

其實c語言標準庫已經提供了二分查找算法,調用標準庫以前咱們必須首先定義一個 cmp 比較函數,做爲 函數指針 傳給 bsearch 函數

對於字符串的比較函數:

1 int cmp(const void* a, const void* b)
2 {
3     assert(a != NULL && b != NULL);
4     const char** lhs = (const char**)a;
5     const char** rhs = (const char**)b;
6     return strcmp(*lhs, *rhs);
7 }

字符串的比較函數爲何不是 直接 return strcmp((char*)a, (char*)b) ? 而是首先轉爲 指針的指針,而後再 引用元素?

首先咱們必需要知道 比較函數 cmp(void* a, void* b) 指針a和指針b是直接指向須要作比較的元素的,

而在字符串比較函數中,由於 char* a[] 是一個指針數組,即數組中每一個元素都是一個指針,指向須要作比較的元素

若是咱們直接 寫成  return strcmp((char*)a, (char*)b)  則咱們是在對數組中的元素作比較,而數組中的元素是一個內存地址(此時將一個內存地址解釋爲1個字節的char來作比較)

實際上它所指向的元素纔是咱們須要比較的,因此這裏有個二級指針

 

9. 循環有序數組中查找 == val 的元素,若找到則返回下標位置,若未找到則返回-1

這裏咱們對 循環有序數組作一下限制,本來數組應該是所有有序,如 a = {0, 1, 4, 5, 6, 10, 25, 28}

這裏咱們從某一位置將數組切成兩半,將後一半總體挪到數組前面去,例如 a = {5, 6, 10, 25, 28, 0, 1, 4}

這樣每次定位到一個mid時,會出現兩種類型的子數組:

(1) {5, 6, 10, 25} 所有有序的子數組:當子數組的第一個元素 <= 最後一個元素時,咱們能夠確定該子數組是有序的

爲何呢? 會不會出現 {5, 6, 10, 0, 25} 或者 {5, 6, 10, 25, 15}這樣的呢? 答案是不會,你們想一想這兩段數組是怎麼來的就知道了 

(2) {28, 0, 1, 4} 不是所有有序的子數組

 

當 a[mid] == val 時 直接 return mid

當 a[low] <= a[mid] 且 a[low] <= val < a[mid] 時,  此時搜索區間確定轉到 mid 左邊,反之就是右邊

當 a[low] > a[mid] 且  a[mid] < val <= a[high]時, 此時搜索區間確定轉到 mid 右邊,反之就是左邊

 

這裏咱們還必須認識到一點:

任意查找時刻,只能處於如下3種狀況:

a. mid左邊是所有有序 mid右邊也是所有有序

b. mid左邊非所有有序,mid 右邊是所有有序

c. mid左邊所有有序, mid右邊是非所有有序

任什麼時候候,都至少有一個區間是所有有序的,咱們就是對這個區間進行準確的判斷 查找值是否在該區間

 1 int binary_search(int* a, int len, int val)
 2 {
 3     assert(a != NULL && len > 0);
 4     int low = 0;
 5     int high = len - 1;
 6     while (low <= high) {
 7         int mid = low + (high - low) / 2;
 8         if (a[mid] == val) return mid;
 9         if (a[low] <= a[mid]) {
10             if (a[low] <= val && val < a[mid])
11                 high = mid - 1;
12             else
13                 low = mid + 1;
14         } else {
15             if (a[mid] < val && val <= a[high])
16                 high = mid + 1;
17             else
18                 low = mid - 1;
19         }
20     }
21     return -1;
22 }

 

10.非降序數組A,查找絕對值最小的元素,返回其下標位置

毫無疑問絕對值最小的是0,咱們能夠首先查找數組老是否有0,若是有,那麼直接返回下標,不然比較0應該插入的位置的左右兩邊的絕對值誰大誰小

咱們能夠直接利用第4題的方法

 1 int binary_search(int* a, int len, int val)
 2 {
 3     assert(a != NULL && len > 0);
 4     int low = 0;
 5     int high = len - 1;
 6     while (low <= high) {
 7         int mid = low + (high - low) / 2;
 8         if (val < a[mid]) {
 9             high = mid - 1;
10         } else if (val > a[mid]) {
11             low = mid + 1;
12         } else {
13             return mid;
14         }
15     }
16     return low;
17 }
18 
19 int find_abs_min(int *a, int len)
20 {
21     assert(a != NULL && len > 0);
22     int zero_pos = binary_search(a, len, 0);
23     if (a[zero_pos] == 0) {
24         return zero_pos;
25     } else {
26         if (zero_pos == 0) {
27             return zero_pos;
28         } else if (zero_pos == len) {
29             return zero_pos - 1;
30         } else {
31             if (abs(a[zero_pos]) < abs(a[zero_pos-1]))
32                 return zero_pos;
33             else
34                 return zero_pos - 1;
35         }
36     }
37 }

 

 歡迎你們批評指正,共同窗習。

相關文章
相關標籤/搜索