二分查找算法详解二分查找算法,是一种在有序数组中查找某一特定元素的搜索算法。
注意两点:(1)有序:查找之前元素必须是有序的,可以是数字值有序,也可以是字典序。
为什么必须有序呢?如果部分有序或循环有序可以吗?(2)数组:所有逻辑相邻的元素在物理存储上也是相邻的,确保可以随机存取。
算法思想:搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。
如果在某一步骤数组为空,则代表找不到。
这种搜索算法每一次比较都使搜索范围缩小一半。
这里我们可以看到:(1) 如果查找值和中间值不相等的时候,我们可以确保可以下次的搜索范围可以缩小一半,正是由于所有元素都是有序的这一先决条件(2) 我们每次查找的范围都是理应包含查找值的区间,当搜索停止时,如果仍未查找到,那么此时的搜索位置就应该是查找值应该处于的位置,只是该值不在数组中而已算法实现及各种变形:1. 非降序数组A, 查找任一个值==val的元素,若找到则返回下标位置,若未找到则返回-12. 非降序数组A, 查找第一个值==val的元素,若找到则返回下标位置,若未找到则返回-1 (类似:查找数组中元素最后一个小于val 值的位置)3. 非降序数组A, 查找最后一个值==val的元素,若找到则返回下标位置,若未找到则返回-1 (类似:查找数组中元素第一个大于val 值的位置)4. 非降序数组A, 查找任一值为val的元素,保证插入该元素后数组仍然有序,返回可以插入的任一位置5. 非降序数组A, 查找任一值为val的元素,保证插入该元素后数组仍然有序,返回可以插入的第一个位置6. 非降序数组A, 查找任一值为val的元素,保证插入该元素后数组仍然有序,返回可以插入的最后一个位置7. 非降序数组A, 查找任一个值==val的元素,若找到则返回一组下标区间(该区间所有值==val),若未找到则返回-18. 非降序字符串数组A, 查找任一个值==val的元素,若找到则返回下标位置,若未找到则返回-1(类似:未找到时返回应该插入点)9. 循环有序数组中查找== val 的元素,若找到则返回下标位置,若未找到则返回-11. 非降序数组A, 查找任一个值==val的元素,若找到则返回下标位置,若未找到则返回-11 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 - 11 int search_first(int* a, int len, int val)2 {3 assert(a != NULL && len > 1);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 > 1);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]) {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 > 1);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 > 1);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]) {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 low17 }其实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 else13 low = mid + 1;14 } else {15 if (a[mid] < val && val <= a[high])16 high = mid + 1;17 else18 low = mid - 1;19 }20 }21 return -1;22 }。