二分查找刷题

Posted 乐学乐知

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分查找刷题相关的知识,希望对你有一定的参考价值。


二分查找刷题


现在数组已经排序完成 一个重要的使用场景就是二分查找 所以这篇文章引入二分查找

我们依然从leetcode的一个题目引入

  1. 二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例 1:

 输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

 输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

思路: 很明显数组已经排序好了 对于一个有序的数组的查找 我们最优的方法肯定是二分 也就是对半查找

这个题目比较简单 就直接实现了

public int search(int[] nums, int target) {
   int len = nums.length;
   if (len == 0) return -1;
   int start = 0, end = len - 1;
   //这里需要取等于的原因是 当end = start + 1的时候 中间数一直是start
   while (start <= end) {
       //防止溢出
       int mid = start + (end - start) / 2;
       if (nums[mid] == target) return mid;
       //中间值偏大 我们从左侧找
       if (nums[mid] > target) end = mid - 1;
       else start = mid + 1;
   }
   //额外说一句 目前的start就是target 应在的索引
   return -1;
}

额外说一句: 最后的start就是target没有找到下的索引值 这个十分有用 你去观察javaapi实现会注意到 对最后的插入点  做了一个偏移 主要是考虑到 0 的负数还是 0 那么就无法区分是找到了还是没有找到 就像上一篇推文的桶排序一样 需要做偏移 -start-1 而且不会溢出

二分查找

二分查找是十分简单的 但是应用确实十分广泛 有时候刷leetcode的时候都意识不到 它是一个二分查找的题目

拓展练习:

  1. 第 N 个神奇数字

  2. 寻找两个有序数组的中位数

  3. 搜索插入位置

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

  5. 寻找旋转排序数组中的最小值 II

  6. 搜索旋转排序数组

  7. 搜索旋转排序数组 II

我们来找几个题目实现下 练练手

  1. 寻找旋转排序数组中的最小值 II

    假设按照升序排序的数组在预先未知的某个点上进行了旋转。

    ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

    请找出其中最小的元素。

    注意数组中可能存在重复的元素。

    示例 1:

    输入: [1,3,5]
    输出: 1

    示例 2:

    输入: [2,2,2,0,1]
    输出: 0

    思路: 主要是定位区间 mid在哪个区间 然后根据区间进行不同操作

    public int findMin(int[] nums) {
       int len = nums.length;
       int start = 0, end = len - 1;
       //这里不能取相等的原因是 一旦相等说明已经找到了 在进入循环的话 start会自加
       while (start < end) {
           int mid = start + (end - start) / 2;
           //如果两者相等 我们无法判断区间 所以将start++
           if (nums[start] == nums[end]) start++;
           //如果mid>end了 说明在左区间 我们之间可以将start=mid+1
           else if (nums[mid] > nums[end]) {
               start = mid + 1;
           }
           //这种情况下 说明在右区间 注意可能正好是最小值 我们不能mid-1
           else if (nums[mid] < nums[start]) {
               end = mid;
           }
           //正常提增序列 我们之间break
           else break;
       }
       return nums[start];
    }

再加一个练习:

  1. 搜索旋转排序数组 II

    假设按照升序排序的数组在预先未知的某个点上进行了旋转。

    ( 例如,数组 [0,0,1,2,2,5,6] 可能变为 [2,5,6,0,0,1,2] )。

    编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false

    示例 1:

    输入: nums = [2,5,6,0,0,1,2], target = 0
    输出: true

    示例 2:

    输入: nums = [2,5,6,0,0,1,2], target = 3
    输出: false

    思路: 我发现对于这种旋转的二分搜索思路都是类似的 就是需要定位好mid的区间

    public boolean search(int[] nums, int target) {
       int len = nums.length;
       if (len == 0) return false;
       int start = 0, end = len - 1;
       while (start <= end) {
           int mid = start + (end - start) / 2;
           if (nums[mid] == target) return true;
           if (nums[start] == nums[end]) start++;
           else if (nums[start] <= nums[mid]) {
               //这个if的条件是在左递增区间上 这个时候 end可以之间等于mid-1
               if (nums[start] <= target && nums[mid] > target) end = mid - 1;
                   //否则的话 我们知道目标一定在mid+1...end之间 我们之间将start = mid+1
               else start = mid + 1;
           } else if (nums[end] >= nums[mid]) {
               if (nums[end] >= target && nums[mid] < target) start = mid + 1;
               else end = mid - 1;
           }
       }
       return false;
    }

我们来个难一点的应用题

  1. 最长上升子序列

    给定一个无序的整数数组,找到其中最长上升子序列的长度。

    示例:

    输入: [10,9,2,5,3,7,101,18]
    输出: 4
    解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

    说明:

    进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

    思路: 基础版本的题目显然可以用动态规划搞定我们用dp[8]int数组来储存比当前索引小的值个数 比如示例我们生成的dp{1,1,1,2,2,4,7,7} 那么转移方程是什么样子呢? dp[i] = max(dp[0...i-1])+1其中0...i-1索引能参与计算的 需要满足 arr[idx]<arr[i] 显然是n^2的时间复杂度

    我们试着做优化:

    idx=0, val=10 这时候明显len=1 我们取第一个元素 {10}

    idx=1, val=9  9<10 长度依然是 len=1 这个时候我们需要在 910之间做一个取舍 需要使得最后的序列最长 那么我们优先保留 较小的数字 这个时候我们取值数组变成了{9}

    ....

    以此类推 那么当我们维护的数组长度达到了 n的时候 怎么定位一个新进来的数字呢  可以使用二分查找的方式

    public int lengthOfLIS(int[] nums) {
       int len = nums.length;
       if (len == 0) return 0;
       int res = 1;//第一个idx=0是我们维护的数组的长度
       for (int i = 1; i < len; i++) {
           //新进来的数字大于了 我们维护的最大数字 长度加一
           if (nums[i] > nums[res - 1]) nums[res++] = nums[i];
               //小于了 我们去做替换 保持维护的数组最优
           else if (nums[i] < nums[res - 1]) {
               //二分查找 很容易实现 见上面的实现
               int idx = Arrays.binarySearch(nums, 0, res, nums[i]);
               //api 做了偏移
               //return -(low + 1);  // key not found.
               if (idx < 0) idx = -idx - 1;
               nums[idx] = nums[i];
           }
       }
       return res;
    }

    这是一个最长递增子序列的题目 很常见

    拓展练习

    • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。

    • 你算法的时间复杂度应该为 O(n2) 。

    1. 俄罗斯套娃信封问题 (思路一样 不过要先排序 比如长度排序后需要注意宽度倒序 然后转化成LIS问题)

再来一个难一点的题目

  1. 寻找两个有序数组的中位数

    给定两个大小为 m 和 n 的有序数组 nums1nums2

    请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。

    你可以假设 nums1nums2 不会同时为空。

    示例 1:

    nums1 = [1, 3]
    nums2 = [2]

    则中位数是 2.0

    示例 2:

    nums1 = [1, 2]
    nums2 = [3, 4]

    则中位数是 (2 + 3)/2 = 2.5

    思路: 有序数组逃不开二分查找...

    长度是(m+n) 比如4 我们找到 在 m数组和n数组分别切一刀 使得切点前的元素个数是2个 那么中位数就是(max(l1,l2) + min(r1,r2))/2 切点需要满足 (L1<=R2 && L2<=R1)

    m: 1(L1) <(cut)>(R1) 4

    n: 2  (L2)<(cut)>(R2) 5

    我们来实现下:

    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
           int len1 = nums1.length, len2 = nums2.length;
           //我们二分查找小的 减少检索
           if (len1 > len2) return findMedianSortedArrays(nums2, nums1);
           int len = (len1 + len2 + 1) / 2;
           int start = 0, end = len1 - 1;
           do {
               int cut1 = end == -1 ? -1 : start + (end - start) / 2;
               int cut2 = len - (cut1 + 1) - 1;
               //判断越界
               int L1 = cut1 >= 0 && cut1 < len1 ? nums1[cut1] : Integer.MIN_VALUE;
               int R1 = cut1 + 1 < len1 ? nums1[cut1 + 1] : Integer.MAX_VALUE;
               int L2 = cut2 >= 0 && cut2 < len2 ? nums2[cut2] : Integer.MIN_VALUE;
               int R2 = cut2 + 1 < len2 ? nums2[cut2 + 1] : Integer.MAX_VALUE;
               if (L1 <= R2 && L2 <= R1) {
                   if ((len1 + len2) % 2 == 0) return (Math.max(L1, L2) + Math.min(R1, R2)) / 2.0;
                   return Math.max(L1, L2);
               }
               //切点位置太大 我们往左切一点
               else if (L1 > R2) end = cut1 - 1;
                   //切点位置太小 我们往右切一点
               else if (R1 < L2) start = cut1 + 1;
           } while (true);
       }


以上是关于二分查找刷题的主要内容,如果未能解决你的问题,请参考以下文章

蓝桥杯刷题笔记:二分查找

LeetCodeLCP 12. 小张刷题计划(二分查找)

二分查找刷题

LeetCode刷题704-简单-二分查找

LeetCode刷题704-简单-二分查找

leetcode刷题之二分查找(Java)