二分查找算法,是一种在有序数组中查找某一特定元素的搜索算法。算法
注意两点:数组
(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 }
欢迎你们批评指正,共同窗习。