标签归档:二分查找

算法学习十一、二分查找

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

二分查找的实现

function binary($arr, $val)
{
    if (empty($arr)) {
        return -1;
    }
    $low = 0;
    $high = count($arr);
    while ($low <= $high) {
        $middle = (int)(($low + $high) / 2);
        if ($arr[$middle] == $val) {
            return $middle;
        } else if ($arr[$middle] > $val) {
            $high = $middle - 1;
        } else {
            $low = $middle + 1;
        }
    }
    return -1;
}

容易出错的三个地方

  1. 循环退出条件

注意是 low<=high,而不是 low<high。

  1. mid 的取值

mid = ( low + high ) / 2 这种写法是有问题的。因为如果 low 和 high 比较大的话,两者之和就有可能会溢出。改进的方法是将 mid 的计算方式写成 low + ( high – low ) / 2。更进一步,如果要将性能优化到极致的话,我们可以将这里的除以 2 操作转化成位运算 low + ( ( high – low ) >> 1 )。因为相比除法运算来说,计算机处理位运算要快得多。

  1. low 和 high 的更新

low=mid+1,high=mid-1。注意这里的 +1 和 -1,如果直接写成 low=mid 或者 high=mid,就可能会发生死循环。

二分查找应用的局限性

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

二分查找算法需要按照下标随机访问元素。我们在数组和链表那两节讲过,数组按照下标随机访问数据的时间复杂度是 O(1),而链表随机访问的时间复杂度是 O(n)。所以,如果数据使用链表存储,二分查找的时间复杂就会变得很高。

  1. 二分查找针对的是有序数据。
  2. 数据量太小不适合二分查找。
  3. 数据量太大也不适合二分查找。

比如,我们有 1GB 大小的数据,如果希望用数组来存储,那就需要 1GB 的连续内存空间。

求一个数的平方根,保留 6 位小数

public function sqrt($x, $point)
{
    $left = 0;
    $right = $x;
    $re = -1;
    while ($left <= $right) {
        $mid = (int)($left + (($right - $left) >> 1));
        // 当 mid*mid 大于 x 的时候,前一个 mid 正好是平方根 
        if ($mid * $mid <= $x) {
            $re = $mid;
            $left = $mid + 1;
        } else {
            $right = $mid - 1;
        }
    }
    // 对小数的处理
    for ($i = 1; $i <= $point; $i++) {
        $p = 1;
        $k = 1;
        while ($p <= $i) {
            $k *= 10;
            $p++;
        }


        for ($j = 9; $j >= 0; $j--) {
            if (($re + $j / $k) * ($re + $j / $k) < $x) {
                $re += $j / $k;
                break;
            }
        }
    }
    return $re;
}

二分查找变体

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

public function bsearch($arr, $value)
{
    $low = 0;
    $high = count($arr) - 1;
    while ($low <= $high) {
        $mid = $low + (($high - $low) >> 1);
        if ($arr[$mid] > $value) {
            $high = $mid - 1;
        } else if ($arr[$mid] < $value) {
            $low = $mid + 1;
        } else {
            if ($mid == 0 || $arr[$mid - 1] != $value) {
                return $mid;
            } else {
                $high = $mid - 1;
            }
        }
    }
    return -1;
}

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

public function bsearch($arr, $value)
{
    $low = 0;
    $high = count($arr) - 1;
    while ($low <= $high) {
        $mid = $low + (($high - $low) >> 1);
        if ($arr[$mid] > $value) {
            $high = $mid - 1;
        } else if ($arr[$mid] < $value) {
            $low = $mid + 1;
        } else {
            if ($mid == count($arr) - 1 || $arr[$mid + 1] != $value) {
                return $mid;
            } else {
                $low = $mid + 1;
            }
        }
    }
    return -1;
}

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

public function bsearch($arr, $value)
{
    $low = 0;
    $high = count($arr) - 1;
    while ($low <= $high) {
        $mid = $low + (($high - $low) >> 1);
        if ($arr[$mid] >= $value) {
            if ($mid == 0 || $arr[$mid - 1] < $value) {
                return $mid;
            } else {
                $high = $mid - 1;
            }
        } else {
            $low = $mid + 1;
        }
    }
    return -1;
}

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

public function bsearch($arr, $value)
{
    $low = 0;
    $high = count($arr) - 1;
    while ($low <= $high) {
        $mid = $low + (($high - $low) >> 1);
        if ($arr[$mid] <= $value) {
            if ($mid == count($arr) - 1 || $arr[$mid + 1] > $value) {
                return $mid;
            } else {
                $low = $mid + 1;
            }
        } else {
            $high = $mid - 1;
        }
    }
    return -1;
}

如何快速定位一个 IP 地址的归属地

[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 区间与归属地的对应关系不经常更新,我们可以先预处理这 12 万条数据,让其按照起始 IP 从小到大排序。如何来排序呢?我们知道,IP 地址可以转化为 32 位的整型数。所以,我们可以将起始地址,按照对应的整型值的大小关系,从小到大进行排序。

然后,这个问题就可以转化为我刚讲的第四种变形问题“在有序数组中,查找最后一个小于等于某个给定值的元素”了。

当我们要查询某个 IP 归属地时,我们可以先通过二分查找,找到最后一个起始 IP 小于等于这个 IP 的 IP 区间,然后,检查这个 IP 是否在这个 IP 区间内,如果在,我们就取出对应的归属地显示;如果不在,就返回未查找到。

小结

凡是用二分查找能解决的,绝大部分我们更倾向于用散列表或者二叉查找树。即便是二分查找在内存使用上更节省,但是毕竟内存如此紧缺的情况并不多。那二分查找真的没什么用处了吗?

实际上,对于“值等于给定值”的二分查找确实不怎么会被用到,二分查找更适合用在“近似”查找问题,在这类问题上,二分查找的优势更加明显。比如上面的这几种变体问题,用其他数据结构,比如散列表、二叉树,就比较难实现了。