力扣Leetcode热题100-二分查找 解题思路分享

1.搜索插入位置

题目如下:

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

思路分析

与最基本的二分查找算法类似,但是基础的二分查找在找不到值的时候一般情况下返回 -1 ,找到的值返回索引,下面先展示最基本的二分查找的Java代码:

 public static int binarySearch(int[] arr,int target){
        int i = 0; //设置位于索引 0 位置的指针
        int j = arr.length - 1; //设置位于最后一位索引位置的指针
        while(i <= j){   //继续循环的条件
            int m = (i + j)>>1; //设置位于数组中间位置的指针
            if(arr[m] > target){  //情况一:目标值在中间值的左边
                j = m - 1;  //移动末位置指针,缩小检索范围
            } else if (arr[m] < target) { //情况二:目标值在中间值的右边
                i = m + 1;  //移动 0 位置指针,缩小检索范围
            }else {   //arr[m] == target的情况
                return m;  //找到目标索引
            }
        }
        return -1; //没找到target
    }

这是最基础的二分查找代码,有助于我们解答接下来的其它题目。

我们接着看这道题,想要处理寻找插入值位置的操作,首先我们会先把 中间值等于目标值的情况合并到 目标值在中间值的左边的情况里面,然后删除没找到目标值就返回 -1 的语句,因为我们在一个升序的数组当中,寻找一个不存在的目标值,最基础的二分查找m指针不可能指向 目标值 而是最终的m指针的右边一位(可自己列一串数组验证)。

代码演示

 public static int binarySort(int[] arr, int target) {
        int i = 0;
        int j = arr.length - 1;
        while (i <= j) {
            int m = (i + j) >> 1; //右移运算,可提高计算效率
            if (arr[m] >= target) {
                j = m - 1;
            } else {
                i = m + 1;
            }
        }
        return i;
    }

2.搜索二维矩阵

给你一个满足下述两条属性的 m x n 整数矩阵:

  • 每行中的整数从左到右按非递减顺序排列。

  • 每行的第一个整数大于前一行的最后一个整数。

给你一个整数 target ,如果 target 在矩阵中,返回 true ;否则,返回 false

思路分析

看到这道题目,最直接的思路就是把二维数组直接存入一个一维数组当中再进行二分查找,这个方法的可行性主要是基于这两个条件:

  • 每行中的整数从左到右按非递减顺序排列。

  • 每行的第一个整数大于前一行的最后一个整数。

这使得我们最后合并的数组一定是一个升序的数组。那么接下来的问题就转化为如何把二维数组存入一维数组的问题,这就很简单了,遍历二维数组元素存入即可。

代码演示

首先我们先完成将二维数组存入一维数组的代码部分:

 private static int[] changeArr(int[][] matrix){
        int[] newArr = new int[matrix.length * matrix[0].length];//创建新数组
        int index = 0; //作为新数组的索引,接下来每次遍历 +1 即可
        for (int i = 0; i < matrix.length; i++) {     //外循环
            for (int j = 0; j < matrix[i].length; j++) {  //内循环
                newArr[index] = matrix[i][j];
                index++;
            }
        }
        return newArr;
    }

然后就是正常的二分查找部分,二分查找代码部分就不演示了

最后演示完整的代码:

 public static boolean searchMatrix(int[][] matrix, int target) {
        int[] ints = changeArr(matrix);
        int i = binarySearch(ints, target);
        if (i == -1) {       //没找到target
            return false;
        } else {            //找到target
            return true;
        }
    }

    private static int[] changeArr(int[][] matrix) {
        int[] newArr = new int[matrix.length * matrix[0].length];//创建新数组
        int index = 0; //作为新数组的索引,接下来每次遍历 +1 即可
        for (int i = 0; i < matrix.length; i++) {     //外循环
            for (int j = 0; j < matrix[i].length; j++) {  //内循环
                newArr[index] = matrix[i][j];
                index++;
            }
        }
        return newArr;
    }

    private static int binarySearch(int[] newArr, int target) {
        int i = 0;
        int j = newArr.length - 1;
        while (i <= j) {
            int m = (i + j) >> 1;
            if (newArr[m] < target) {
                i = m + 1;
            } else if (newArr[m] > target) {
                j = m - 1;
            } else {
                return m;
            }
        }
        return -1;
    }

3.查找元素第一个和最后一个位置

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

思路分析

首先我们看到这道题一时间会想到两种方法,一种是左右两边扩展寻找,还有一种即是二分查找的方法,由于时间复杂度是 O(log n)所以我们果断选择二分查找的方法

那么这道题就涉及到二分查找的LeftRightMost算法,即查找有重复数字的升序数组中,目标值最左边元素,和最右边元素的索引的问题

下面以LeftMost为例:

LeftMost查找与普通的二分查找的最明显的差别在于,LeftMost在查找到目标元素的时候并不急着返回,而是将右边界 right 更新为 mid-1,继续左侧二分查找左半边区间,以找到最左边的目标元素。

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

​

为什么将 candidate设为-1呢?是为了处理特殊情况,当目标值不存在于数组中时,直接返回-1

那么这题最复杂的部分就被解决了

代码演示

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 static int Leftmost(int[] arr, int target) {
        int i = 0, j = arr.length - 1;
        int candidate1 = -1;
        while (i <= j) {
            int m = (i + j) >> 1;
            if (arr[m] < target) {
                i = m + 1;
            } else if (arr[m] > target) {
                j = m - 1;
            } else {
                candidate1 = m;
                j = m - 1;
            }
        }
        return candidate1;
    }
    public static int Rightmost(int[] arr, int target) {
        int i = 0, j = arr.length - 1;
        int candidate2 = -1;
        while (i <= j) {
            int m = (i + j) >> 1;
            if (arr[m] < target) {
                i = m + 1;
            } else if (arr[m] > target) {
                j = m - 1;
            } else {
                candidate2 = m;
                i = m + 1;
            }
        }
        return candidate2;
    }
}

4.搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同

在传递给函数之前,nums 在预先未知的某个下标 k0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2]

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

思路分析

这道题最重要的就是判断旋转过后哪一个部分是有序的,哪一个部分是无序的,这里分成三种情况

第一种:中间值大于0索引处的值

就像题目中的例子[ 4, 5 ,6 ,7,0,1,2 ] 中间索引是 3 ,对应的值是 7,要大于此时 0 索引上的值 4,此时,数组中 [4,5,6,7] 这个部分我们认为有序,[7,0,1,2]这个部分我们认为无序

第二种:中间值小于0索引处的值

即是上面情况的相反 

那么我们只用分这两类情况进行讨论

代码演示

public int search(int[] nums, int target) {
        int i = 0;
        int j = nums.length - 1;
        while (i <= j){
            int m = (i + j)>>1;
            if(nums[m] == target){   //当中间值恰好等于目标值
                return m;
            }
            if(nums[m] >= nums[i]){
                if(target >= nums[i] && target < nums[m]){ //nums[i] <= target < nums[m]
                    j = m - 1;
                }else {
                    i = m + 1;
                }
            }else {
                if (target <= nums[j] && target > nums[m]){ //nums[m] < target <= nums[j]
                    i = m + 1;
                }else {
                    j = m - 1;
                }
            }
        }
        return -1;
    }

5.寻找旋转排列数组的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

思路分析

与上题思路类似,我们分成两种情况分析,

如果中间值小于最高索引的值,例如:

若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]

这种情况,我们就将 i 指针移到 m+1 的位置上,因为这种情况我们可以很明显的观察到,最小值一定是在中间位置的左边。那我们就在左边进行寻找

如果中间值大于最高索引的值,例如:

若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

这种情况,我们就让 j 指针移到 m 的位置上,为什么不是 m-1的位置上呢,因为,m本身也可能是最小值,比如[3,1,2]这种情况

代码实现

class Solution {
  public int findMin(int[] nums) {
       int low = 0;
        int high = nums.length - 1;
        while (low < high) {
            int pivot = low + (high - low) / 2;
            if (nums[pivot] < nums[high]) {
                high = pivot;
            } else {
                low = pivot + 1;
            }
        }
        return nums[low];
    }
}

*6.寻找两个正序数组的中位数

给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。

算法的时间复杂度应该为 O(log (m+n)) 。

思路分析 + 代码演示

当我第一次看到这题的时候,我第一个思路就是把两个数组结合到一个数组当中,然后排序,那么中位数就好算了,但是显然这样并不能满足题目所给的时间复杂度log(m + n)的要求。后面查找并学习了很多别人的思路和想法,这里给大家总结一下:

我们首先要知道什么是中位数,在一个数组当中那就很简单了,分两种情况

第一种 偶数个的情况

1  2   3   4   5  6

那么中位数就是 \left ( 3+4 \right )/2 = 3.5

第二种  奇数个的情况

1  2  3  4  5

那么中位数就是 中间索引的数值 3

那在两个数组中要怎么寻找中位数呢?先不讨论特殊情况

比如下面这两个数组,要如何划分呢

 遵循原则:1.short划分完的左边最大值小于long划分完的右边的最小值

                   2.long划分完的左边最大值小于short划分完的右边的最小值

                   3.划分完整体的左侧元素个数要等于右边元素个数或等于右边元素个数 + 1

首先我们要满足第三条原则,总共数字的个数是8个,那么我们是不是可以分成划分完左边4个,右边4个的形式。那么我们可以有好几种划分方案比如说:

 比如说这种划分方案,满足第一条和第三条原则,但不满足第二条原则,故这不是正确的划分方法,那么我们把short的线往右移一格,long的往左移动一格试试

 这种划分方案就是正确的划分方案,接下来就是如何计算中位数的问题

在总共元素有偶数个的情况下,

以这题为例,左边最大值是4,右边最小值是5,那么中位数就是 (4+5)/ 2.0 

 在总共元素有奇数个的情况下,

总结一下思路:首先找到如何划分,然后按照公式计算中位数

 众所周知,思路字越少,代码越难写,接下来我们来看下代码要怎么写(最后会有完整代码,下面只展示步骤)


1.将短数组(short)排在前面,将长数组(long)放在后面

2.讨论特殊情况,即short数组长度为 0 的情况

3.接下来就是最复杂的步骤,我们在写代码时,把这两串数组当作一串去写,如果有读不懂的地方,大家请自己举例子去一步一步,具体地看。我的表达能力有限,敬请原谅 ಠ_ಠ

        int start = 0; //表示划分完的左边元素的个数
        int end = m;   //代表short数组能够划分的极限个数
        double result = 0.0;
        while (start <= end){     //从short数组能拿出的个数的循环条件
            int mid = (m + n) / 2;  //划分出来的分隔线,也是short上的线
            int other = (m + n + 1) / 2 - mid;  //代表右边的元素的个数,也同时可以作为long上的线

            //short被分到左边的最大值:第一种,分割线划在索引0元素的左边;第二种,就是正常情况
            int shortLeftMax = mid == 0 ? Integer.MIN_VALUE : nums1[mid - 1];
            //long被分到左边的最大值:第一种,long左边没取值;第二种,正常情况
            int longLeftMax = other == 0 ? Integer.MIN_VALUE : nums2[other - 1];
            //short被分到右边的最小值:第一种,分割线划在short最大索引元素的右边;第二种,正常情况
            int shortRightMin = mid == m ? Integer.MAX_VALUE : nums1[mid];
            //long被分到右边的最小值: 第一种,分割线划在long最大索引处的右边;第二种,正常情况
            int longRightMin = other == n ? Integer.MAX_VALUE : nums2[other];

            if(shortLeftMax <= longLeftMax && longLeftMax <= shortRightMin){  //划分已经完成
                if((m + n) % 2 == 0 ){     //长度是偶数的情况:中位数等于(左边最大+右边最小)/2.0
                    int leftMax = Math.max(shortLeftMax,longLeftMax);
                    int rightMin = Math.min(shortRightMin,longRightMin);
                    result = (leftMax + rightMin) / 2.0;
                    break;
                }else {            //长度是奇数个的情况:中位数等于左边的最大值
                    int leftMax = Math.max(shortLeftMax,longLeftMax);
                    result = (leftMax) * 1.0;
                    break;
                }
            } else if (longLeftMax > shortRightMin) {  //long左边最大值 大于 short右边最小值
                start = mid + 1; //将short上面的线往后移动一格
            }else{
                end = mid - 1;
            }
        }
        return result;

 完整代码:

class Solution {
    public static double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int m = nums1.length;
        int n = nums2.length;

        if(m > n) return findMedianSortedArrays(nums2,nums1);

        if(m == 0){
            if(n % 2 == 0){
                return (nums2[(n-1)/2] + nums2[n/2]) / 2.0;
            }else {
                return nums2[n/2] * 1.0;
            }
        }

        int start = 0;
        int end = nums1.length;
        double result = 0.0;
        while (start <= end){
            int mid = (start + end) / 2;
            int other = (m + n + 1) / 2 - mid;

            int shortLeftMax = mid == 0  ? Integer.MIN_VALUE : nums1[mid - 1];
            int longLeftMax = other == 0 ? Integer.MIN_VALUE : nums2[other - 1];

            int shortRightMin = mid == m ? Integer.MAX_VALUE : nums1[mid];
            int longRightMin = other == n ?Integer.MAX_VALUE : nums2[other];

            if(shortLeftMax <= longRightMin && longLeftMax <= shortRightMin){
                if((m + n) % 2 == 0){
                    int leftMax = Math.max(shortLeftMax,longLeftMax);
                    int rightMin = Math.min(longRightMin,shortRightMin);
                    result = (leftMax + rightMin) / 2.0;
                    break;
                }else {
                    result =  Math.max(shortLeftMax,longLeftMax) * 1.0;
                    break;
                }
            } else if (longLeftMax > shortRightMin) {
                start = mid + 1;
            }else {
                end = mid - 1;
            }
        }
        return result;
    }
}

最后一题难度较大,我也是看了其它大神的解析才会,如果大家有更好思路,请和我分享

 第一次写解析,如有不足,欢迎指正!(●'◡'●)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值