二分查找算法,是一種在有序數組中查找某一特定元素的搜索算法。算法
注意兩點:數組
(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 }
歡迎你們批評指正,共同窗習。