【数据结构与算法】Leetcode二分查找

说明

本文记录了笔者做leetcode二分查找题(冲一个二分查找勋章)的过程,实现语言为JAVA。
leetcode35题和704题是最基础的二分查找,用了三个方法/模版:二分查找基础版,二分查找改动版和二分查找平衡版。首先解决这两道题,从而熟悉二分查找的模版以及模版之间的区别。

35. 搜索插入位置/704. 二分查找

(1) 二分查找基础版

基础版查找区间包括j(右指针),查找区间内没有值的时候跳出循环。

class Solution {
    public int searchInsert(int[] nums, int target) {
        int i = 0, j = nums.length - 1; //设置指针的初始值
        while( i <= j){//如果区间内有值
            int m = (i + j) >>> 1;//计算中间值
            if(target < nums[m]){ //目标在左边
                j = m - 1;
            }else if( target > nums[m]){//目标在右边
                i = m + 1;
            }else{
                return m;//找到了
            }
        }return i ;//找不到返回将会被按顺序插入的位置,如果是704,就返回-1
    }
}

要点:

  1. 为什么是while (i <= j) ,而不是(i < j) ?
    因为最后一次比较时,i = j = m,如果此时a[m] = target就返回m,如果不等于就说明找不到该元素;如果只是(i < j) ,则漏掉了最后一次比较,可能造成本来数组中有这个元素却找不到。
  2. 为什么在计算m的时候用m = (i + j) >>>1, 而不是 m = (i + j) / 2?
    因为二进制数有两种表现形式,一种是把最高位看成符号位,另一种是看成普通数字,但JAVA中默认是符号位,所以两个正数相加有可能是负数,进一步(i + j) / 2的结果也是负数。如果使用 > > > >>> >>>(无符号右移运算符),右移的效果就是整除2(向下取整)。
(2) 二分查找改动版

改动版要点(三个改动):

  1. j = a.length,j不参与循环内的比较,扮演区间边界的角色;
  2. 循环条件(i < j)而不是(i <= j),是为了避免进入j = m,m = j的死循环;
  3. j = m 同1的解释。
class Solution {
    public int searchInsert(int[] nums, int target) {
        int i = 0, j = nums.length;// 第一处改动,j不指向任何数
        while( i < j){//第二处改动,i<j以避免陷入死循环
            int m = (i + j) >>> 1;
            if(target < nums[m]){
                j = m ;//第三处改动,j占到m的位置
            }else if( target > nums[m]){
                i = m + 1;
            }else{
                return m;
            }
        }return i ;//找不到返回将会被按顺序插入的位置,如果是704,就返回-1
    }
}
(3)二分查找平衡版

二分查找基础版和改动版都存在一个问题:向左查找和向右查找的比较次数不平衡,所以平衡版通过减少循环内的平均比较次数解决了这个问题,数据量很大的时候可以体现这个优势。

class Solution {
    public int searchInsert(int[] nums, int target) {
        int i = 0, j = nums.length; //j不在查找区间
        while( j - i > 1){ // 当区间内有多于1个值时,循环只是为了缩小区间
            int m = (i + j) >>> 1;
            if(target < nums[m]){
                j = m;
            }else{//去掉else if,只需要比较一次,比较次数就平衡了
                i = m;//target >= nums[m],所以i要包括中间值
            }
        }if(target <= nums[i]){//当查找区间只有一个元素(i指向的元素)时退出循环
            return i;
        }else{
            return i+1;//找不到返回将会被按顺序插入的位置,如果是704,就返回-1
        }
    }
}

在这里插入图片描述

744. 寻找比目标字母大的最小字母

这里用的是二分查找的第三个模版,在while循环中,如果target == a[m]时令i = m,就是一直网友找,如果是令j = m,就是一直往左找。最后找到target的左右邻居,然后根据条件判断返回值。

class Solution {
    public char nextGreatestLetter(char[] letters, char target) {
        int i = 0, j = letters.length - 1;//i,j是查找范围的两端
        while(j - i > 1){ //当查找范围内至少有三个值的时候
            int m = (i+j) >>> 1;
            if(target >= letters[m]){
                i = m;//一直往右找
            }else{
                j = m;
            }
        }if(letters[i] > target){//进行一系列条件判断
            return letters[i];
        }else if(letters[i] <= target && target < letters[j]){
            return letters[j];
        }else{
            return letters[0];
        }
    }
}

在这里插入图片描述

69. x的平方根

利用二分查找基础版
首先考虑,0和1的平方根都是它本身,所以当x<2时,返回x。从2开始,使用二分查找基础版,一直缩小查找范围,当i=j时,i也等于m,相当于判断i/j是不是x的平方根,如果i比x的实际平方根小,即i<x/i,则i右移一位。直到i>j,此时退出循环,返回的j的值就是要找的平方根,如果j比实际平方根大,则左移一位,i>j退出循环,还是返回j。
要点:x与m*m的比较需要占用更大的内存,所以要写成x/m与m的比较。此外,因为x是>=2的整数,所以m不会等于0。

class Solution {   
    public int mySqrt(int x){
        if(x < 2){
            return x;
        }
        int i = 0, j = x;
        while(i <= j){
            int m = (i + j) >>> 1;
            if(x/m <  m){
                j = m - 1;
            }else if(x/m > m){
                i = m + 1;
            }else{
                return m;
            }
        }return j;
    }
}

在这里插入图片描述

374. 猜数字大小

这道题用的是二分查找基础版,注意循环结束还要返回一个值。

/** 
 * Forward declaration of guess API.
 * @param  num   your guess
 * @return 	     -1 if num is higher than the picked number
 *			      1 if num is lower than the picked number
 *               otherwise return 0
 * int guess(int num);
 */

public class Solution extends GuessGame {
    public int guessNumber(int n){
        int i = 1, j = n;
        while(i <= j){
            int m = (i + j) >>> 1;
            if(guess(m) == -1){//判断pick与m的大小,如果pick小于m返回-1
                j = m - 1;
            }else if(guess(m) == 1){//判断pick与m的大小,如果pick大于m返回1
                i = m + 1;
            }else{
                return m;}
        }return 0;//因为是1~n的数字,如果它输入有误的话就返回0
    }
}

在这里插入图片描述

278. 第一个错误的版本

使用二分查找改动版进行解答。

/* The isBadVersion API is defined in the parent class VersionControl.
      boolean isBadVersion(int version); */

public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        int i = 0, j = n; //i是第一个版本的索引,j指向查找范围的右侧。
        while(i < j){
            int m = (i + j) >>> 1;
            if(isBadVersion(m + 1) == true){//注意m+1是索引为m对应的版本号
                j = m;
            }else{
                i = m + 1;
            }
        }return i + 1;//返回版本号
    }
}

在这里插入图片描述

162. 寻找峰值

class Solution {
    public int findPeakElement(int[] nums) {
        int i = 0;
        int j = nums.length - 1;  //j在查找区间内
        while(i < j){ //查找区间至少有两个值
            int m = (i + j) >>> 1;
            if(nums[m] > nums[m + 1]){//如果是下坡
                j = m;//让j位于m这个顶
            }
            else{
                i = m + 1;//如果是上坡,让i位于顶
            }
        }
        return i;//当i = j时,上坡坡顶和下坡坡顶汇于一点,就是峰值
    }
}

在这里插入图片描述

852.山脉数组的峰顶索引

class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int i = 0 , j = arr.length - 1; //i,j都在查找范围内
        while(j - i > 1){ //查找范围内两个以上的数值
            int m = (i + j) >>> 1;
            if(arr[m] > arr[m + 1]){//如果中间值大于右值,向左继续查找
                j = m;  
            }else{//如果中间值小于右值,向右继续查找
                i = m;
            }
        }if(arr[i] > arr[j]){//最后剩下两个相邻的值进行判断
            return i;
        }else{
            return j;
        }  
    }
}

在这里插入图片描述

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

说明leftmost和rightmost函数的功能:
找到与目标相等的重复元素最左侧值的索引(Leftmost)应用场景:求排名(Leftmost+1),求前任(Leftmost -1),求小于target的所有值(索引0到索引Leftmost -1),求大于等于target的所有值(leftmost到最后一个元素)。

找到与目标相等的重复元素最右侧值的索引(Rightmost)应用场景:求后任(rightmost +1),求大于target的所有值(索引rightmost +1到最后一个元素),求小于等于target的所有值(索引0到索引rightmost)。

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int x = leftMost(nums,target);
        if(x == -1){
            return new int[]{-1,-1};
        }else{
            return new int[]{x, rightMost(nums,target)};
        }
    }

    public int leftMost(int[] nums, int target){
        int i = 0, j = nums.length - 1;
        int candidate = -1;
        while(i <= j){
            int m = (i+j) >>> 1;
            if(target < nums[m]){
                j = m - 1;
            }else if(target > nums[m]){
                i = m + 1;
            }
            else{
                j = m - 1;
                candidate = m;
            }
        }return candidate;
    }

    public int rightMost(int[] nums, int target){
        int i = 0, j = nums.length - 1;
        int candidate = -1;
        while(i <= j){
            int m = (i+j) >>> 1;
            if(target < nums[m]){
                j = m - 1;
            }else if(target > nums[m]){
                i = m + 1;
            }else{
                i = m + 1;
                candidate = m;
            }
        }return candidate;
    }
}

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值