【详解】LeetCode#34,在排序数组中查找元素的第一个和最后一个位置,学习二分查找,告别恼人的边界问题

二分查找

二分查找法的原理非常简单,而且能把查找的时间复杂度降为 O(logn),但我之前在做二分法的时候经常因为边界问题而摸不着头脑,比如右边界我该不该取的问题、while 循环的判断条件带不带 = 的问题。当然你完全可以根据自己的定义的边界,根据自己的想法决定 while 循环里带不带等号,但需要注意最后跳出循环时左右边界情况,再做讨论。所以,二分查找题主要考察的是细节,要把细节理解清楚就需要规范且完整的思考过程,定义好 l 和 r 的含义,到底是 [l, r] 还是 [l, r)。

刷题的意义就再于形成一套自己的解题思路,有利于在面试现场很短时间内 bug free 掉一道算法题。因此,以下介绍我使用的这套二分模板应对求解大部分二分查找题,透彻地理解了模板才能应对复杂多变的情况。

二分查找模板:寻找 target

例1. 查找数组 arr 中元素等于 target 的索引,若找不到则返回 -1

以下是在 [l, r] 区间里找 target 值,我习惯于用这种形式作为我解二分法题型的模板,后面会解释这样写的好处,特点有:

  1. [l, r]
  2. while( l <= r)
  3. if 判断语句里面不包括等号的情况
  4. 在 2 的条件下有 l = r + 1

Java 代码:

// 当 arr 数组中没有 target 值时,方法返回 -1。
private static int binarySearch(int[] arr, int target) {
    if (arr == null || arr.length == 0) {
        return -1;
    }
    int l = 0, r = arr.length - 1;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (target < arr[mid]) {
            r = mid - 1;
        } else if (target > arr[mid]) {
            l = mid + 1;
        } else {
            return mid;
        }
    }
    return -1;
}

得出第四点的结论分析如下:

最后一个 while 循环时,l = r,mid = l,

若 target < arr[mid], 则 r = mid - 1 = l - 1,即 l = r + 1;

若 target > arr[mid], 则 l = mid + 1 = r + 1;

若 target = arr[mid], 则 target = mid = l = r;

LeetCode34 (寻找左侧边界、右侧边界)

LeetCode34. 在排序数组中查找元素的第一个和最后一个位置。给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值,返回 [-1, -1]。(你的算法时间复杂度必须是 O(log n) 级别。)

示例1:

输入: nums = [5,7,7,8,8,10], target = 8

输出: [3,4]

分析:

第一种方式是通过二分法先找到第一个 8 的位置,再用循环在 8 前后查找,即可确定开始索引和结束索引。但这种方法由于循环查找的加入,使得时间复杂度不是 O(logn) 的。如何只用二分法查找?

我的思路是,先用二分法查找 < 8 的最大索引即可确定 8 的开始索引,用 left 记录,同理查找 > 8 的最小索引即可确定 8 的结束索引,用 right 记录,最后要返回的就是 [left + 1, right - 1],如果 [left + 1, right - 1] 无效(即left + 1 > right - 1),则返回 {-1, -1}。

寻找左侧边界

  1. < 8 的最大索引。

还是按照上面的模板写,由于是 < 8,没有取到 8 ,所以 while 循环里不会出现 return target 的情况,因此 if 判断只有两种情况。按照模板的要求写的代码如下

private int searchLeft(int[] nums, int target) {
    int l = 0, r = nums.length-1;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] < target) {
            
        } else {
            
        }
    }
    return  ;
}

还需要确定 1.if 判断里的替换 l;2.替换 r 的语句;3. return 返回的是什么。

先确定 else 里的语句,如果 nums[mid] >= target,而要找的索引位置是**< 8的最大所用**,此时要找的索引在[l, mid-1] 这一侧,因此 r = mid - 1;

private int searchLeft(int[] nums, int target) {
    int l = 0, r = nums.length-1;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] < target) {
            
        } else {
            r = mid - 1; 
        }
    }
    return  ;
}

再写 if 结果为真的执行语句,如果 l = mid; ,则在最后一次 while 循环时,l = r = mid,num[mid] < target,如果 l = mid,永远也跳不出循环,所以 l = mid + 1;

private int searchLeft(int[] nums, int target) {
    int l = 0, r = nums.length-1;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] < target) {
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    return  ;
}

最后,退出循环的隐含条件是 l = r + 1, 即 [r+1, r]。最后一次 while 循环时,l = r = mid,

如果 num[mid] < target,则 l = mid + 1, 则需要返回的值是 mid®,因为不能保证 r + 1 还满足 num[r + 1] < target;

如果 num[mid] >= target, 则 r = mid - 1,则返回的值是 mid - 1®,因为这个时候 mid - 1一定是 < target 的最大索引。

因此,return r;。整个过程的代码如下

private int searchLeft(int[] nums, int target) {
    int l = 0, r = nums.length-1;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] < target) {
            l = mid + 1; 
        } else {
            r = mid - 1; 
        }
    }
    return r;  // 一定会跳出while循环,此时l = r + 1;
}

寻找右侧边界

  1. />8 的最小索引

和上面的分析过程一样,按照三步确定最后的代码为:

private int searchRight(int[] nums, int target) {
    int l = 0, r = nums.length-1;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] > target) {
            r = mid - 1;  //如果是r = mid,则会陷入死循环
        } else {
            l = mid + 1;  //如果nums[mid] < target的话,说明要找的索引在右边,因此把l往mid往右挪一个
        }
    }
    return l;  // 一定会跳出while循环,此时l = r + 1;
}

返回结果

  1. 得到了 < 8 的最大索引和 > 8 的最小索引,用 left 和 right 记录索引位置。如果 left + 1 > right - 1,即[left +1, right - 1] 这个范围无效,说明找不到目标值,因此返回 new int[]{-1,-1}
public int[] searchRange(int[] nums, int target) {
    
    int left = searchLeft(nums, target);  // left: < target 的最大索引
    int right = searchRight(nums, target);  // right: > target 的最小索引

    if (left + 1 > right - 1) {
        return new int[]{-1, -1};
    }
    
    return new int[]{left+1, right-1};
}

private int searchLeft(int[] nums, int target) {
    int l = 0, r = nums.length-1;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] < target) {
            l = mid + 1; 
        } else {
            r = mid - 1; 
        }
    }
    return r;  // 一定会跳出while循环,此时l = r + 1;
}

private int searchRight(int[] nums, int target) {
    int l = 0, r = nums.length-1;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] > target) {
            r = mid - 1;  //如果是r = mid,则会陷入死循环
        } else {
            l = mid + 1;  //如果nums[mid] < target的话,说明要找的索引在右边,因此把l往mid往右挪一个
        }
    }
    return l;  // 一定会跳出while循环,此时l = r + 1;
}

剑指Offer 面试题53—II,0~n-1 中缺失的数字

一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。
在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
示例:
输入: [0,1,2,3,4,5,6,7,9]
输出: 8

思路:找值大于索引的最小索引

public int missingNumber(int[] nums) {
    // 找值大于索引的最小索引
    int l = 0, r = nums.length - 1;
    while(l <= r) {
        int mid = l + (r - l) / 2;
        if(nums[mid] != mid) {
            if(mid == 0 || nums[mid - 1] == mid - 1) {
                return mid;
            }
            r = mid - 1;
        } else {
            l = mid + 1;
        }
    }
    return l;
}

LeetCode287 寻找重复数

LeetCode 287. 寻找重复数。给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

示例 1:

输入: [1,3,4,2,2]
输入: [1,3,4,2,2]

二分查找法:

时间O(nlogn), 空间O(1)

public int findDuplicate2(int[] nums) {
    int l = 1, r = nums.length;
    while (l <= r) {
        int mid = l + (r - l) / 2;
        int count = 0;
        for (int num : nums) {
            if (num <= mid) {
                count++;
            }
        }
        if (count <= mid) {
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    return l;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值