zoukankan      html  css  js  c++  java
  • 二分查找的那些事儿

    二分查找算法,是一种在有序数组中查找某一特定元素的搜索算法。

    注意两点:

    (1)有序:查找之前元素必须是有序的,可以是数字值有序,也可以是字典序。为什么必须有序呢? 如果部分有序或循环有序可以吗?

    (2)数组:所有逻辑相邻的元素在物理存储上也是相邻的,确保可以随机存取。

    算法思想:

    搜素过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜素过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

    这里我们可以看到:

    (1) 如果查找值和中间值不相等的时候,我们可以确保可以下次的搜索范围可以缩小一半,正是由于所有元素都是有序的这一先决条件

    (2) 我们每次查找的范围都是 理应包含 查找值的区间,当搜索停止时,如果仍未查找到,那么此时的搜索位置就应该是 查找值 应该处于的位置,只是该值不在数组中而已

    算法实现及各种变形:

    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 }

     欢迎大家批评指正,共同学习。

  • 相关阅读:
    SQL SERVER 2005中同义词实例
    内聚性是模块之所以成为模块的原因--一个中心、单一职责
    软件开发的方法论
    系统集成与软件开发
    编程的本质是构建---建构你想要表达的世界
    编程思想与以人为本-编程的本质
    软件开发之道-软件开发背后的哲学
    swift 协议(结合扩展)的特点
    swift的特性:扩展、协议、泛型
    从数据流角度管窥 Moya 的实现(一):构建请求
  • 原文地址:https://www.cnblogs.com/wwwjieo0/p/3575278.html
Copyright © 2011-2022 走看看