算法:二分查找

1.什么是二分査找?

    二分查找是一种查询效率非常高的查找算法。又称折半查找。

    生活中二分查找的思想无处不在。一个最常见的就是猜数游戏,我随机写一个 0 到 99 的数,然后你来猜我写的是什么。猜的过程中,我会告诉你每次是猜大了还是猜小了,直到猜中为止。假如我写的数是 23,猜数过程如下所示。

    最多只需要 7 次就猜出来了,这个过程是很快的。同理,要查找某个数据是否在给定的数组中,我们同样也可以利用这个思想。

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

 

 

2. 二分查找的时间复杂度?

    我们假设数据大小为 n,每次查找数据大小都会缩小为原来的一半,最坏情况下,直到查找区间缩小为空时停止查找。

    若经过 k 次区间缩小最后变为空,则 n2k=1,k=log2nn2k=1,k=log2n所以二分查找的时间复杂度为 O(logn)

    这种对数时间复杂度的算法是一种非常高效的算法,有时候甚至比时间复杂度为常量级的算法还要高效。因为常量级的时间复杂度对应的常数可能非常大,比如 O(100), O(1000),因此这些算法有时候可能还没有 O(logn)O(logn) 的算法执行效率高。

3.如何实观二分查找?

    可以使用循环法和递归法分别实现:

public class BinarySearch {
    /**
     * 使用循环实现二分查找算法(不使用递归实现二分查找算法)
     * 注意事项:
     • 循环退出条件是:starte<=end,而不是 start<mid
     • min的取值,使用mid=start+(end-start)/2,而不用 mid=(start+end)/2,因为如果 start和end比较大的话,求和可能会发生int类型的值超出最大范围.,为了把性能优化到极致,可以将除以2转换成位运算,即 start+(end-start)>>1),因为相比除法运算来说,计算机处理位运算要快得。
     • start和end 的更新:start=mid-1,end=mid+1,若直接写成start=mid,end=md,就可能会发生死循环.
     * @param arr
     * @param x
     * @return
     */
    public static int binarySearch(int[] arr, int x) {
        int low = 0;
        int high = arr.length-1;
        while(low <= high) {
            int middle = (low + high)/2;
            if(x == arr[middle]) {
                return middle;
            }else if(x <arr[middle]) {
                high = middle - 1;
            }else {
                low = middle + 1;
            }
        }
        return -1;
    }
    
    /**
     * 使用递归实现二分查找
     * 二分查找递归实现的局限性:
     • 二分查找依赖的是顺序表结构,即数组.
     • 二分查找针对的是有序数据,因此只能用在插入,删除操作不频繁,一次排序多次查找的场景
     • 数据量太小不适合二分查找,与直接遍历相比效卒提升不明显,但有一个例外,就是数据之间的比较操作非常费时,比如数组中存储的部是长度超过300的字符串,那这是还是尽量减少比较操作使用二分查找吧.
     • 数据量太大也不是适合用二分查找,因为数组需要连续的空间,若数据量太大,往往找不到存储如此大规模数据的连续内存空间
     * @param srcArray
     * @param start
     * @param end
     * @param key
     * @return
     */
    public static int binSearch(int srcArray[], int start, int end, int key) {
        int mid = (end - start) / 2 + start;
        if (srcArray[mid] == key) {
            return mid;
        }
        if (start >= end) {
            return -1;
        } else if (key > srcArray[mid]) {
            return binSearch(srcArray, mid + 1, end, key);
        } else if (key < srcArray[mid]) {
            return binSearch(srcArray, start, mid - 1, key);
        }
        return -1;
    }
    public static void main(String[] args) {
        int[] arr = { 6, 12, 33, 87, 90, 97, 108, 561 };
        System.out.println("循环查找:" + (binarySearch(arr, 87) + 1));
        int srcArray[] = {3,5,11,17,21,23,28,30,32,50,64,78,81,95,101};
        System.out.println("递归查找:" + binSearch(srcArray, 0, srcArray.length - 1, 81));
    }
}

注意事项

    • 循环退出条件 left <= right

    • mid = left + ((right-left) >> 1),用移位运算优化计算性能

    • left 和 right 的更新分别是 mid+1 和 mid-1

 

4. 二分查找的应用场景?

    • 二分查 找依赖的是顺序表结构,也就是数组,需要能够按照下标随机访问元素。

    • 二分查找针对的是有序数据,如果数据无序,需要先进行排序。而如果有频繁的插入、删除操作,则每次查找前都需要再次排序,这时候,二分查找将不再适用。

    • 数据量太小可以直接遍历查找,没有必要用二分查找。但如果数据之间的比较操作非常耗时,比如数据为长度超过 300 的字符串,则不管数据量大小,都推荐使用二分查找。

    • 而如果数据量过大,则意味着我们需要用非常大的连续内存空间来存储这些数据,内存开销可能无法满足。

 

5.思考

    最简单的二分查找情况下,我们假设数组中没有重复元素,因此很容易实现。如果数组中存在重复元素,二分查找就没有想象中那么容易了。

5.1  查找第一个值等于给定值的元素?

    如果数据中存在相同的元素,但我们要查找第一个值等于给定值的元素,如果直接用最简单的二分查找,显然是不满足的。看下面的例子,有 3 个等于 8 的元素,简单二分查找会返回 7,但第一个值等于 8 的元素应该是 a[5]。

    其实,只要我们在简单二分查找的基础上再多加上一点限制即可。

    当我们发现 a[mid] = val 时,我们需要继续确认 a[mid] 左边还有没有等于 val 的元素。若 mid 左边没有和 a[mid] 相等的元素,则此时 a[mid] 就是我们要找的第一个值等于给定值的元素。另外,若 mid 到了第一个元素的位置,说明左边已经没有元素,此时 a[mid] 也即是我们要找的元素。否则,我们就需要继续向左边查找。

float Binary_Search(float data[], int left, int right, float value)
{
    int begin = left;
    while (left <= right)
    {
        int mid = left + (right - left) / 2;
        if (value == data[mid])
        {
            if (mid == begin || a[mid - 1] != a[mid]) return mid;
            else right = mid - 1;
        }
        else if (value < data[mid])
        {
            right = mid - 1;
        }
        else
        {
            left = mid + 1;
        }
    }

    return -1;
}

 

5.2  查找最后一个值等于给定值的元素?

    这个问题和上面要查找第一个值等于给定值的问题思路一样,只不过是查找的方向改变了。

    当我们发现 a[mid] = val 时,我们需要继续确认 a[mid] 右边还有没有等于 val 的元素。若 mid 右边没有和 a[mid] 相等的元素,则此时 a[mid] 就是我们要找的最后一个值等于给定值的元素。另外,若 mid 到达了最后一个元素的位置,说明右边已经没有元素,此时 a[mid] 也即是我们要找的元素。否则,我们就需要继续向右边查找。

float Binary_Search(float data[], int left, int right, float value)
{
    int end = right;
    while (left <= right)
    {
        int mid = left + (right - left) / 2;
        if (value == data[mid])
        {
            if (mid == end || a[mid + 1] != a[mid]) return mid;
            else left = mid + 1;
        }
        else if (value < data[mid])
        {
            right = mid - 1;
        }
        else
        {
            left = mid + 1;
        }
    }

    return -1;
}

 

5.3  查找第一个大于等于给定值的元素?

    当我们发现 a[mid] >= val 时,我们需要继续确认 a[mid] 左边还有没有大于等于 val 的元素。若 mid 左边的值小于 a[mid],则此时 a[mid] 就是我们要找的第一个大于等于给定值的元素。另外,若 mid 到达了第一个元素的位置,说明左边已经没有元素,此时 a[mid] 也即是我们要找的元素。否则,我们就需要继续向左边查找。

float Binary_Search(float data[], int left, int right, float value)
{
    int begin = left;
    while (left <= right)
    {
        int mid = left + (right - left) / 2;
        if (data[mid] >= value)
        {
            if (mid == begin || a[mid - 1] < a[mid]) return mid;
            else right = mid - 1;
        }
        else
        {
            left = mid + 1;
        }      
    }

    return -1;
}

 

5.4 查找最后一个小于等于给定值的元素?

    当我们发现 a[mid] <= val 时,我们需要继续确认 a[mid] 右边还有没有小于等于 val 的元素。若 mid 右边的值大于 a[mid],则此时 a[mid] 就是我们要找的最后一个小于等于给定值的元素。另外,若 mid 到达了最后一个元素的位置,说明右边已经没有元素,此时 a[mid] 也即是我们要找的元素。否则,我们就需要继续向右边查找。

float Binary_Search(float data[], int left, int right, float value)
{
    int end = right;
    while (left <= right)
    {
        int mid = left + (right - left) / 2;
        if (data[mid] <= value)
        {
            if (mid == end || a[mid + 1] > a[mid]) return mid;
            else left = mid + 1;
        }
        else
        {
            right = mid - 1;
        }      
    }

    return -1;
}

 

5.5 如何快速定位 IP 对应的省份?

    当我们要查找 202.102.133.13 这个 IP 地址的归属地时,我们就在地址库中搜索,发现这个 IP 位于 [202.102.133.0, 202.102.133.255] 这个范围内,我们就可以找到对应的归属地——山东东营。

[202.102.133.0, 202.102.133.255]  山东东营市

[202.102.135.0, 202.102.136.255]  山东烟台

[202.102.156.34, 202.102.157.255] 山东青岛

[202.102.48.0, 202.102.48.255] 江苏宿迁

[202.102.49.15, 202.102.51.251] 江苏泰州

[202.102.56.0, 202.102.56.255] 江苏连云港

    因此,我们可以按照 IP 地址库的起始地址对所有区间进行排序,然后问题就转化为了找到最后一个起始地址小于等于给定 IP 地址的区间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员学习圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值