关于二分查找的扩展题

前言

在前一篇文章中经典二分查找的分析,我们理解了关于经典二分查找的方式和问题,相对来说经典的二分查找还是比较简单的,现在我们在经典的基础上,再看看二分查找的一些扩展题,就要稍微复杂一点了。

1、查找第一个等于指定值的元素下标

假设有这么一串数值:1, 2, 3, 3, 5, 6, 7, 8,要查找第一个3的位置,那么应该返回下标2

    public int binarySearch(int[] nums, int target) {
    
    
        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
    
    
            int mid = l + ((r - l) >> 1);
            if (nums[mid] > target) {
    
    
                r = mid - 1;
            } else if (nums[mid] < target) {
    
    
                l = mid + 1;
            } else {
    
    
                // 如果已经查找到第一位,或者当前查找的位置前一位不等于目标值,则认为已经查找到了第一个等于目标值的下标
                if (mid == 0 || nums[mid - 1] != target) {
    
    
                    return mid;
                } else {
    
    
                    // 不是第一个等于目标值的下标,则往前挪一位
                    r = mid - 1;
                }
            }
        }
        return -1;
    }

2、查找最后一个等于指定值的元素下标

同理,查找最后一个等于指定值的元素下标,就非常简单

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

3、查找第一个大于等于指定值的元素下标

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

4、查找最后一个小于等于指定值的元素下标

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

除了上面4种扩展题之外,还有几种来自LeetCode中的,在部分有序的情况下使用二分查找完成的

1、搜索旋转排序数组

来自leetcode第33题

在这里插入图片描述

解题分析

虽然数组不是完全有序的,但是我们不难发现,通过一次二分后,必然有一半有序的,一半无序的(但是无序的情况依旧能通过二分变为一半有序一半无序),只要符合这样的规律,那么我们就可以依然可以使用二分进行查找。

图解说明(x轴表示数组下标,y轴表示值)

假设中间点在旋转点的左边,那么中间点左半部分一定是有序的。
在这里插入图片描述
假设中间点在旋转点的右边,那么中间点右半部分一定是有序的。

在这里插入图片描述

代码

    public int search(int[] nums, int target) {
    
    
        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
    
    
            int mid = l + ((r - l) >> 1);
            if (nums[mid] == target) {
    
    
                return mid;
            }
            /**
             * 如果nums[mid] < nums[r]成立,则表示mid 到 r区间一定是有序的,那么接下来按照有序的方式进行二分查找即可
             * 否则表示l 到 mid的区间一定是有序的,那么同样在这个区间也可以按照二分的方式查找即可
             */
            if (nums[mid] < nums[r]) {
    
    
                //如果nums[mid] < target && target <= nums[r]成立,则在mid到r的范围内找,否则在l到mid的范围找
                if (nums[mid] < target && target <= nums[r]) {
    
    
                    l = mid + 1;
                } else {
    
    
                    r = mid - 1;
                }
            } else {
    
    
                //如果nums[l] <= target && target < nums[mid]成立,则在l到mid的范围内找,否则在mid到r的范围找
                if (nums[l] <= target && target < nums[mid]) {
    
    
                    r = mid - 1;
                } else {
    
    
                    l = mid + 1;
                }
            }
        }
        return -1;
    }

2、搜索旋转排序数组 II

来自leetcode第81题

在这里插入图片描述
这题与上一题不同之处在于,允许数组中有重复元素,那么也就意味有可能出现lmidr的情况,在这种情况下,我们就无法区分出哪半边是有序的。

举例说明

下面两张图,都是nums[l] = nums[mid] = nums[r] 的情况,假设目标值在升序的范围中,那么很明显两种升序的范围分别在mid左边和mid右边。
在这里插入图片描述

在这里插入图片描述

如果遇到这样的情况,我们就让l和r分别向右和向左移动一位,然后再找中间点,直到不满足 lrmid,此时我们就能判断升序到底是在mid的左边还是右边了。

在这里插入图片描述

代码

    public boolean search(int[] nums, int target) {
    
    
        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
    
    
            int mid = l + ((r - l) >> 1);
            if (nums[mid] == target) {
    
    
                return true;
            }
            //如果nums[l] == nums[mid] == nums[r],就让l、r都移动一位。其他逻辑和前一题没有重复的值情况一样
            if (nums[l] == nums[mid] && nums[r] == nums[mid]) {
    
    
                l++;
                r--;
            } else if (nums[l] <= nums[mid]) {
    
    
                if (nums[l] <= target && target < nums[mid]) {
    
    
                    r = mid - 1;
                } else {
    
    
                    l = mid + 1;
                }
            } else {
    
    
                if (nums[mid] < target && target <= nums[r]) {
    
    
                    l = mid + 1;
                } else {
    
    
                    r = mid - 1;
                }

            }
        }
        return false;
    }

3、寻找旋转排序数组中的最小值

在这里插入图片描述

这题与第一题类似,实际上也可以很容易分析出规律

在这里插入图片描述
从图中可以看出,当mid指向的下标值小于r指向的下标值时,则最小值一定在mid的右边,反之则一定在mid的左边。

代码实现

    public int findMin(int[] nums) {
    
    
        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
    
    
            int mid = l + ((r - l) >> 1);
            /**
             * 通过不断的比较nums[r] <= nums[mid],最终一定会走到一段排序的范围中, 那么再通过r=mid,使得r无限的靠近最小值,直到二分到最后。
             */
            if (nums[r] <= nums[mid]) {
    
    
                l = mid + 1;
            } else {
    
    
                r = mid;
            }
        }
        return nums[r];
    }

4、寻找旋转排序数组中的最小值

在这里插入图片描述

结合前面几题的套路,这题就比较容易找出规律了,直接上代码,和前一题比无非就是多了一种nums[r] == nums[mid]的情况,按照套路移动一次r的下标即可。

    public int findMin(int[] nums) {
    
    
        int l = 0;
        int r = nums.length - 1;
        while (l < r) {
    
    
            int mid = l + ((r - l) >> 1);
            if (nums[r] < nums[mid]) {
    
    
                l = mid + 1;
            } else if (nums[r] > nums[mid]) {
    
    
                r = mid;
            } else {
    
    
                r -= 1;

            }
        }
        return nums[r];
    }

Guess you like

Origin blog.csdn.net/CSDN_WYL2016/article/details/120331058