二分查找(Binary Search)算法,也叫折半查找算法。

二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为 0

假设有 1000 条订单数据,已经按照订单金额从小到大排序,每个订单金额都不同,并且最小单位是元。现在想知道是否存在金额等于 19 元的订单。如果存在,则返回订单数据,如果不存在则返回 null。

利用二分思想,每次都与区间的中间数据比对大小,缩小查找区间的范围。下图中,low 和 high 表示待查找区间的下标,mid 表示待查找区间的中间元素下标。

 

二分查找的时间复杂度:

假设数据大小是 n,每次查找后数据都会缩小为原来的一半,也就是会除以 2。最坏情况下,直到查找区间被缩小为空,才停止。

被查找区间的大小变化:

  n, n/2, n/4, n/8, ...,n/2k...

这是一个等比数列。其中 n/2= 1 时,k 的值就是总共缩小的次数。而每一次缩小操作只涉及两个数据的大小比较,所以,经过了 k 次区间缩小操作,时间复杂度就是 O(k)。通过 n/2= 1,

可以求得 k=log2n,所以时间复杂度就是 O(logn)。这是一种极其高效的时间复杂度,有的时候甚至比时间复杂度是常量级 O(1) 的算法还要高效。

因为 logn 是一个非常“恐怖”的数量级,即便 n 非常非常大,对应的 logn 也很小。比如 n 等于 2 的 32 次方,大约是 42 亿。也就是说,如果我们在 42 亿个数据中用二分查找一个数据,最多需要比较 32 次。

用大 O 标记法表示时间复杂度的时候,会省略掉常数、系数和低阶。对于常量级时间复杂度的算法来说,O(1) 有可能表示的是一个非常大的常量值,比如 O(1000)、O(10000)。所以,常量级时间复杂度的算法有时候可能还没有 O(logn) 的算法执行效率高。

反过来,对数对应的就是指数。一个非常著名的“阿基米德与国王下棋的故事”。 指数时间复杂度的算法在大规模数据面前是无效的。

2. 二分查找的递归与非递归实现

    /**
     * 1.循环退出条件是 low <= high,而不是 low < high。
     * 2.mid = (low + high) / 2这种写法,在 low 和 high 比较大时,两者之和可能会溢出。
     *    改进的方法是将 mid 的计算方式写成 low + (high - low) / 2, 转化成位运算 low+((high-low)>>1) 更佳。
     * 3.low=mid+1,high=mid-1。注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环。
     *    比如,当 high=3,low=3 时,如果 a[3] 不等于 value,就会导致一直循环不退出。
     * 时间复杂度为 O(logn)
     * @param arr
     * @param n
     * @param value
     * @return
     */
    public int bsearch(int[] arr, int n, int value) {
        int low = 0;
        int high = n - 1;
        while (low <= high) {
            int mid = low + (high - low) / 2;   // (low + high) / 2, 可能会发生溢出
            if (arr[mid] > value ) {            // 中间值 > 目标值, 往左边找, mid - 1
                high = mid - 1;
            } else if (arr[mid] < value) {      // 中间值 < 目标值, 往右边找, mid + 1
                low = mid + 1;
            } else {                            // 中间值 == 目标值
                return mid;
            }
        }
        return -1;
    }

    /**
     * 2. 二分查找的递归实现
     * @param arr
     * @param n
     * @param value
     * @return
     */
    public int bsearch_recurse(int[] arr, int n, int value) {
        return recursive(arr, 0, n - 1, value);
    }
    private int recursive(int[] arr, int low, int high, int value) {
        if (low > high) return -1;
        int mid = low + ((high - low) >> 1);
        if (arr[mid] > value) {
            return recursive(arr, low, mid - 1, value);
        } else if (arr[mid] < value) {
            return recursive(arr, mid + 1, high, value);
        } else {
            return mid;
        }
    }

3. 二分查找应用场景的局限性

首先,二分查找依赖的是顺序表结构,简单点说就是数组。

    那二分查找能否依赖其他数据结构呢?比如链表。答案是不可以的,主要原因是二分查找算法需要按照下标随机访问元素。数组按照下标随机访问数据的时间复杂度是 O(1),而链表随机访问的时间复杂度是 O(n)。所以,如果数据使用链表存储,二分查找的时间复杂就会变得很高。

二分查找只能用在数据是通过顺序表来存储的数据结构上。如果你的数据是通过其他数据结构存储的,则无法应用二分查找。

如果数据使用链表存储,二分查找的时间复杂就会变得很高, 选择用数组而不是链表来实现二分查找了。
假设链表长度为n,二分查找每次都要找到中间点(计算中忽略奇偶数差异): 
第一次查找中间点,需要移动指针n/2次;
第二次,需要移动指针n/4次;
第三次需要移动指针n/8次;
......
以此类推,一直到1次为值
总共指针移动次数(查找次数) = n/2 + n/4 + n/8 + ...+ 1,这显然是个等比数列,根据等比数列求和公式:Sum = n - 1. 
最后算法时间复杂度是:O(n-1),忽略常数,记为O(n),时间复杂度和顺序查找时间复杂度相同
但是稍微思考下,在二分查找的时候,由于要进行多余的运算,严格来说,会比顺序查找时间慢
View Code

相关文章:

  • 2021-11-08
  • 2022-03-03
猜你喜欢
  • 2021-12-10
  • 2021-12-11
  • 2022-12-23
  • 2022-12-23
  • 2021-06-13
  • 2022-12-23
  • 2021-05-19
相关资源
相似解决方案