二分查找基础

今天劳动节假期,闲来无事,在leetcode看到了有关于二分查找的题目,想起二分查找虽然简单,但是边界条件容易出现问题,因此特别写下这篇文章记录一下。

二分查找


二分查找又称为折半查找,是针对有序数组实现O(logN)的算法。总结起来二分查找有四种情况,从区间闭合情况和中间值取值方式上分,一共可以有四种形式。

[l,r]


在左闭,右闭区间的情况下,退出条件应该包含等于的情况,并且中间每一个步骤都是考虑左闭、右闭的处理方式。对于mid的取值,既可以取上限,也可以取下限。

/**
*mid取下限
*/
public class BinarySearch {

    public int search(int[] nums, int target) {
        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
            int m = l + ((r - l) >> 1);
            if (nums[m] == target) {
                return m;
            } else if (nums[m] > target) {
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        return -1;
    }
}

因为如果是左闭、右闭区间,那么l/r两个指针是可能在最后指向同一个元素的,因此如果取上限的方式,加1的操作导致此时的mid不指向这最后一个元素,因此,取上限的方式需要做特殊的处理,不推荐使用。
还有需要注意的是,这种方式在最后剩下两个元素的时候,其实还需要有两个元素需要验证。还有,以mid取下限为例:

  • 如果寻找的target小于nums中的所有元素,那么最终r为-1
  • 如果寻找的target大于nums中的所有元素,那么最终l为nums.length
  • 如果选择的是l < r的判断方式,在跳出循环以后,仍需要比较,而且必须考虑越界的情况

[l,r)


public class BinarySearch {

    public int search(int[] nums, int target) {
        int l = 0;
        int r = nums.length;
        while (l < r) {
            int m = l + ((r - l) >> 1);  //为了可以取到左边,必须取下限
            if (nums[m] == target) {
                return m;
            } else if (nums[m] > target) {
                r = m;  //依然保持右开的特性
            } else {
                l = m + 1;
            }
        }
        return -1;
    }
}

因为l/r两个指针指向连续两个元素时,因为有一个区间的开的,因此,其实只剩下最后一个元素需要验证了,因此,判断完成以后,直接结束。

(l,r]


public class BinarySearch {

    public int search(int[] nums, int target) {
        int l = -1;
        int r = nums.length - 1;
        while (l < r) {
            int m = l + ((r - l) >> 1) + 1;//为了可以取到右边,必须取上限
            if (nums[m] == target) {
                return m;
            } else if (nums[m] > target) {
                r = m - 1;  //维持右闭
            } else {
                l = m;   //维持左开
            }
        }
        return -1;
    }
}

最后,附上两个leetcode经典例题

leetcode34. 在排序数组中查找元素的第一个和最后一个位置


class Solution {
    public int[] searchRange(int[] nums, int target) {
        int l = findLeft(nums, target);
        if (l == -1) {
            return new int[]{-1, -1};
        }
        int r = findRight(nums, target, l);
        return new int[]{l, r};
    }

    //寻找最左边的target
    private int findLeft(int[] nums, int target) {
        int res = -1;
        //使用左闭,右闭的形式
        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
            int m = l + ((r - l) >> 1);
            if (nums[m] == target) {
                res = m;
                r = m - 1;
            } else if (nums[m] > target) {
                r = m - 1;
            } else {
                l = m + 1;
            }
        }
        return res;
    }

    private int findRight(int[] nums, int target, int left) {
        //此时已经有了l边界,因此直接考虑(l,r]方式 并且必然存在最右
        int l = left;
        int r = nums.length - 1;
        int res = left;
        while (l < r) {
            int m = l + ((r - l) >> 1) + 1;
            if (nums[m] == target) {
                res = m;
                l = m;  // 向右继续逼近
            } else if (nums[m] > target) {
                r = m - 1;
            } else {
                l = m;
            }
        }
        return res;
    }
}

leetcode 875. 爱吃香蕉的珂珂


class Solution {

    public int minEatingSpeed(int[] piles, int h) {
        //1.由题目piles.length <= h <= 109条件可得,以所有香蕉堆中最多的速度吃,一定吃的完
        //因此可以考虑使用二分法,在[1,max]之间进行判断,找到最小的值
        //考虑到极致情况下 必须max速度才可以吃完 使用左闭、右闭区间
        int l = 1;
        int r = getMax(piles);
        int res = 0;
        while (l <= r) {
            int speed = l + ((r - l) >> 1);
            long times = getTimes(piles, speed);
            if (times <= h) {
                //说明吃的太快,可以放慢速度
                res = speed;
                r = speed - 1;
            } else {
                //说明吃的太慢,需要加快速度
                l = speed + 1;
            }
        }
        return res;
    }

    private int getMax(int[] piles) {
        int res = 0;
        for (int i = 0; i < piles.length; i++) {
            res = Math.max(res, piles[i]);
        }
        return res;
    }

    //根据speed计算出,吃完所有香蕉所需要的时间
    //trick:利用整数的特点完成向上取整 因为如果超出,那么最起码超出1.加上speed - 1 以后就可以实现向上
    private long getTimes(int[] piles, int speed) {
        //防止数据溢出,使用long
        long res = 0;
        for (int i = 0; i < piles.length; i++) {
            res += ((piles[i] + speed - 1) / speed);
        }
        return res;
    }
}

leetcode 69. x 的平方根

class Solution {

    public int mySqrt(int x) {
        //判断特殊情况
        if (x == 0 || x == 1) {
            return x;
        }
        //排除上述两个情况以后,不可能结果是x本身了,且最小为1,因此使用[l,r)来书写
        //这样写
        int l = 1;
        int r = x;
        int res = l;
        while (l < r) {
            int m = l + ((r - l) >> 1);
            //为了避免出现乘法溢出的情况  使用除法
            if (m == x / m) {
                return m;
            } else if (m > x / m) {
                r = m;  //维持右开
            } else {
                l = m + 1;
                res = m;  //使得m * m越来越接近 x
            }
        }
        return res;
    }
}

结语


个人认为:二分查找的关键点在于,区间选择、循环条件的结束以及结果更新的时机,做好上述三点,逻辑就会更加清楚。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值