Loading

Leetcode刷题记录之二分查找

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

题目描述

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

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

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

输入输出

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

题解

设计两个函数,lower_boundupper_bound,这俩函数的功能和 STL 中同名函数的功能一样。

lower_bound

lower_bound 用于找出 第一个 小于等于 target 的值,如果数组中所有值都大于 target,则返回 -1。

使用二分查找,如果数组中有多个 值 和 target 相等,比如 nums = [5,7,7,8,8,10]target = 8,因为要找出最左边的那个 8,所以当遇到当前值和 target 相等时,要继续往左搜索,即令 right = mid - 1。二分查找结束时,left 要么指向第一个 小于等于 target 的值,要么指向 nums.size()

upper_bound

upper_bound 用于找出 第一个 大于等于 target 的值,如果数组中所有值都小于 target,则返回-1。

当遇到当前值和 target 相等时,要继续往 搜索,即令 left = mid + 1。二分查找结束时,right 要么指向第一个 大于等于 target 的值,要么指向 nums.size()

代码

class Solution {
public:
  vector<int> searchRange(vector<int>& nums, int target) {
    int low_idx = lower_bound(nums, target);
    if (low_idx != -1 && nums[low_idx] == target) {
      int up_idx = upper_bound(nums, target);
      return {low_idx, up_idx};
    }
    return {-1, -1};
  }
  int lower_bound(vector<int> &nums, int target) {
    int left = 0, right = static_cast<int>(nums.size()) - 1;
    while (left <= right) {
      int mid = (left + right) / 2;
      if (target <= nums[mid]) right = mid - 1;
      else left = mid + 1;
    }
    return left >= nums.size() ? -1 : left;
  }
  int upper_bound(vector<int> &nums, int target) {
    int left = 0, right = static_cast<int>(nums.size()) - 1;
    while (left <= right) {
      int mid = (left + right) / 2;
      if (target >= nums[mid]) left = mid + 1;
      else right = mid - 1;
    }
    return right < 0 ? -1 : right;
  }
};

刷题记录

一刷 2022年8月7日

81. 搜索旋转排序数组 II

题目描述

已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。

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

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

你必须尽可能减少整个操作步骤。

输入输出

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

题解

使用二分查找,每次二分时,当前子序列都可以分为左右两半部分,左右两半部分一定有一个是严格非递减的。分三种情况讨论:

  1. nums[right] == nums[mid] 时,比如 1011111101 这种。此种情况下分不清到底是前面有序还是后面有序,此时 left++ 即可。相当于去掉一个重复的干扰项。
  2. nums[right] < nums[mid] 时,比如 2 3 4 5 6 7 12 < 5 这种。这种情况下,前半部分是有序的。
    • 因此如果 nums[left] <= target < nums[mid],则在前半部分找
    • 否则去后半部分找。
  3. nums[right] > nums[mid] 时, 6 7 1 2 3 4 56 > 2 这种。这种情况下,后半部分有序。
    • 因此如果 nums[mid] <target <= nums[right]。则在后半部分找。
    • 否则去前半部分找。

代码

class Solution {
public:
  bool search(vector<int>& nums, int target) {
    int sz = static_cast<int>(nums.size());
    int left = 0, right = sz - 1;
    while (left <= right) {
      int mid = (left + right) / 2;
      if (nums[mid] == target || nums[left] == target || nums[right] == target) return true;
      if (nums[mid] == nums[right]) { // 无法判断左半边有序还是右半边有序
        ++left;
      } else if (nums[mid] < nums[right]) { // 右半边有序
        if (nums[mid] < target && target < nums[right]) {
          left = mid + 1;
        } else {
          right = mid - 1;
        }
      } else { // 左半边有序
        if (nums[left] < target && target < nums[mid]) {
          right = mid - 1;
        } else {
          left = mid + 1;
        }
      }
    }
    return false;
  }
};

刷题记录

一刷 2022年8月8日

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

题目描述

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

你必须尽可能减少整个过程的操作步骤。

输入输出

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

题解

同样将数组分为左右两部分,最小值一定在左右两部分的分界点,即右半部分的起始位置处。假设我们要找的这个起始点为 i

  1. nums[mid] > nums[right] 时,mid 一定在第 1 个排序数组中,i 一定满足 mid < i <= right,因此执行 left = mid + 1

  2. nums[mid] < nums[right] 时,mid 一定在第 2 个排序数组中,i 一定满足 left < i <= mid,因此执行 right = mid

  3. nums[mid] == nums[right] 时,此题中数组的元素可重复,难以判断分界点 i 指针区间,我们令 right = right - 1,缩小搜索区间;

最后,left 指针一定指向起始点 i

代码

class Solution {
public:
  int findMin(vector<int>& nums) {
    int left = 0, right = static_cast<int>(nums.size()) - 1;
    while (left < right) {
      int mid = (left + right) / 2;
      if (nums[mid] == nums[right]) { // 无法判断 mid 在哪个区间
        --right;
      } else if (nums[mid] < nums[right]) { // mid 在右区间
        right = mid;
      } else { // mid 在左区间
        left = mid + 1;
      }
    }
    return nums[left];
  }
};

刷题记录

一刷 2022年8月8日

540. 有序数组中的单一元素

题目描述

给你一个仅由整数组成的有序数组,其中每个元素都会出现两次,唯有一个数只会出现一次。

请你找出并返回只出现一次的那个数。

你设计的解决方案必须满足 O(log n) 时间复杂度和 O(1) 空间复杂度。

输入输出

输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2

题解

出现一次的那个元素是个分界点,

  1. nums[mid] = nums[mid + 1]
    • 对于分界点左边的元素,其下标为偶数
    • 对于分界点右边的元素,其下标为奇数
  2. nums[mid] = nums[mid - 1]
    • 对于分界点左边的元素,其下标为奇数
    • 对于分界点右边的元素,其下标为偶数

代码

class Solution {
public:
  int singleNonDuplicate(vector<int>& nums) {
    int left = 0, right = static_cast<int>(nums.size()) - 1;
    while (left <= right) {
      int mid = (left + right) / 2;
      int left_num = mid == 0 ? nums[mid] - 1 : nums[mid - 1];
      int right_num = mid == nums.size() - 1 ? nums[mid] + 1 : nums[mid + 1];
      if (nums[mid] != left_num && nums[mid] != right_num) {
        return nums[mid];
      } else if ((nums[mid] == left_num && mid % 2 == 0) || (nums[mid] == right_num && mid % 2 == 1)) {
        right = mid - 1;
      } else {
        left = mid + 1;
      }
    }
    return 0;
  }
};

刷题记录

一刷 2022年8月9日

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

题目描述

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

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

输入输出

输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

题解

image-20220810110142044

如上图所示,用一条线分割把两个数组分别分割成两个部分,这个分割线满足两个条件:

  1. 分割线左边和右边的元素个数相等,或者左边比右边元素多 1 个。

    • 当两数组总元素个数为偶数时,左边元素个数 = 右边元素个数, 中位数就是左边的元素最大值(比较一下分割线左边上下两个数组的最大值就能找出来)。比如下图,比较 6 和 7 两个元素就可以。
    image-20220810203635584
    • 当两数组总元素个数为奇数时,左边元素个数 = 右边元素个数 + 1。左边元素最大值和右边元素最小值加一起取平均值就是中位数。比如下图。
    image-20220810203832106
  2. 分割线左边所有元素的数值 <= 分割线右边所有元素的数值。

只要能找到这么一条分割线,那么就能确定中位数。寻找这条分割线使用二分查找。

如何保持条件1

假设数组 1 的长度为 m,数组 2 的长度为 n。

当 m + n 为偶数时,$size_{left} = \frac{m + n}2 = \frac{m + n + 1}2$;因为 m + n 为偶数,所以$\frac{m + n}2 = \frac{m + n + 1}2$;

当 m + n 为奇数时,$size_{left} = \frac{m + n + 1}2$;

这样,我们不用分奇偶讨论,只需要确定一个数组的分割线位置,另一个数组的分割线位置就能通过公式计算出来。即,假设第一个数组$size_{left1} = x$,那么第二个数组$size_{left2} = \frac{m + n + 1}2 - x$。

为了防止数组访问越界的情况发生,应该始终通过二分查找先确定长度较小的那个数组的分割线位置,再通过公式计算长度较大的那个数组的分割线位置。

比如,假设数组 1 的长度为 10,数组 2 的长度为 4。如果先确定数组 1 $size_{left1} = 2$,通过计算数组 2 $size_{left2} = \frac{10 + 4 + 1}2 - 2 = 5$,很显然,5 已经超出了数组2 的长度,会出现越界的情况。

如何保持条件2

由于两个数组都是有序数组,在同一个数组内,分割线一定满足左边的所有元素小于等于右边的所有元素。

在不同的数组之间,应该保证交叉小于等于关系成立,即:

  1. 分割线左边第 1 个数组的最大值 <= 分割线右边第 2 个数组的最小值

  2. 分割线左边第 2 个数组的最大值 <= 分割线右边第 1 个数组的最小值

如下图:

image-20220810205123341

只要不符合交叉小于等于关系,就要适当调整分割线位置

举例1

比如下图,左边的 8 大于右边的 6,不符合交叉小于等于关系的第 2 种情况。

调整方案:将中位数分割线在数组 1 的位置右移。

image-20220810205422333
举例2

比如下图,左边的 8 大于右边的 7,右边的数太小,不符合交叉小于等于关系的第 1 种情况。

调整方案:将中位数分割线在数组 1 的位置左移。

image-20220810210228608

代码

class Solution {
public:
  double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
    // 始终让nums1是长度较小的那个数组
    if (nums1.size() > nums2.size()) {
      return findMedianSortedArrays(nums2, nums1);
    }

    int m = static_cast<int >(nums1.size());
    int n = static_cast<int >(nums2.size());
    int left = 0, right = m;
    // left_max:前一部分的最大值
    // right_min:后一部分的最小值
    int left_max = INT32_MIN, right_min = INT32_MAX;

    while (left <= right) {
      // 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
      // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
      int i = (left + right) / 2;
      int j = (m + n + 1) / 2 - i;

      // nums_im1, nums_i, nums_jm1, nums_j 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
      int nums_im1 = (i == 0 ? INT32_MIN : nums1[i - 1]);
      int nums_i = (i == m ? INT32_MAX : nums1[i]);
      int nums_jm1 = (j == 0 ? INT32_MIN : nums2[j - 1]);
      int nums_j = (j == n ? INT32_MAX : nums2[j]);
      left_max = std::max(nums_im1, nums_jm1);
      right_min = std::min(nums_i, nums_j);
      if (nums_im1 > nums_j) { // 不满足交叉小于条件1
        right = i - 1;
      } else if (nums_jm1 > nums_i) { // 不满足交叉小于条件2
        left = i + 1;
      } else { // 成功找到分割点
        break;
      }
    }

    return (m + n) % 2 == 0 ? (left_max + right_min) / 2.0 : left_max;
  }
};

刷题记录

一刷 2022年8月10日
posted @ 2022-08-09 12:02  cclemontree  阅读(221)  评论(0)    收藏  举报