【二分专题】-「“两段性”」-LeetCode-33. 搜索旋转排序数组

33. 搜索旋转排序数组


引言

by 三叶题解

  • 有序数组中搜索,可以考虑使用 【二分】求解。
  • 「二分」的本质是两段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。

本题由于发生了“旋转”,整体上并不是纯粹的“有序数组”,所以首先需要找到“旋转点”的位置,而由于“旋转点”分割的左、右子数组 则均为“有序” --> 可以二分

查找“旋转点”的位置又可以有两种方法,即“线性遍历”、或“利用两段性”使用二分来找“旋转点”,具体的两种解法如下所示。

二分(线性遍历旋转点)

思路

  1. 线性扫描 n u m s nums nums 找到“旋转点”位置 index
  2. 第一次二分:在 [ 0 , i n d e x + 1 ) [0, index + 1) [0,index+1) 二分查找 t a r g e t target target 是否存在;
  3. 第二次二分:在 [ i n d e x + 1 , n ) [index + 1, n) [index+1,n) 二分查找 t a r g e t target target 是否存在;
class Solution {
    public int search(int[] nums, int target) {
        int n = nums.length;
        // 找旋转点
        int index = 0;
        for ( ; index + 1 < n && nums[index] < nums[index + 1]; index++) {}
        System.out.println("index = " + index);
        // 1、在[0, index + 1)二分
        int res = binarySearch(nums, 0, index + 1, target);
        if (res != -1) {
            return res;
        }
        // 2、在[index + 1, n)二分
        res = binarySearch(nums, index + 1, n, target);
        return res;
    }

    // 在nums下标为[left, right)中搜索target
    public int binarySearch(int[] nums, int left, int right, int target) {
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid; // 左闭右开
            } else {
                return mid;
            }
        }
        return -1; // 不存在
    }

    public static void main(String[] args) {
        int[] nums = {1};
        int target = 1;
        System.out.println(new Solution().search(nums, target));
    }
}
  • 时间复杂度:最坏 O ( n ) O(n) O(n)(“旋转点”在 n u m s nums nums末尾,需要线性扫描整个 n u m s nums nums),最好 O ( l o g n ) O(logn) O(logn)(“旋转点”在 n u m s nums nums开头)
  • 空间复杂度: O ( 1 ) O(1) O(1)

二分(二分找旋转点

此种解法和 剑指Offer Day4 中记录的 剑指 Offer 53 - I. 在排序数组中查找数字 I 很类似,但又有所不同。

by 三叶题解

  • 「二分」的本质是两段性,并非单调性。只要一段满足某个性质,另外一段不满足某个性质,就可以用「二分」。

  • 经过旋转的数组,显然前半段满足 > = n u m s [ 0 ] >= nums[0] >=nums[0],而后半段不满足 > = n u m s [ 0 ] >= nums[0] >=nums[0]。我们可以以此作为依据,通过「二分」找到旋转点。

class Solution {
    public int search(int[] nums, int target) {
        int n = nums.length;
        // 第一次二分:找旋转点
        int left = 0;
        int right = n - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= nums[0]) { // 大于等于nums[0],说明旋转点还在mid右边
                left = mid + 1;
            } else {
                right = mid - 1; // 左闭右闭
            }
        }
        int index = right; // 左边数组的最右边界,即[0, index]为左数组
        System.out.println("index = " + index);

        // 第二次二分:
        if (target >= nums[0]) { // target“可能”在左数组中(必不可能在右数组)
            left = 0;
            right = index; // 左闭右闭
        } else {
            left = index + 1;
            right = n - 1; // 左闭右闭
        }
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1; // 左闭右闭
            } else {
                return mid;
            }
        }
        return -1; // 不存在
    }
}
  • 时间复杂度: O ( l o g n ) O(logn) O(logn)
  • 空间复杂度: O ( 1 ) O(1) O(1)

81. 搜索旋转排序数组 II

在这里插入图片描述
在这里插入图片描述

引言

直接套用 33. 搜索旋转排序数组 二分找“旋转点”时,则 gg。

原因在于:本题中 n u m s nums nums可能存在重复元素,若“旋转”后将重复元素分在了左、右两个子数组中时,会造成 n u m s nums nums 丢失“两段性”。

如下将 n u m s = [ 0 , 1 , 1 , 1 , 1 ] nums=[0, 1, 1, 1, 1] nums=[0,1,1,1,1] “旋转后”,得到 [ 1 , 0 , 1 , 1 , 1 ] [1, 0, 1, 1,1] [1,0,1,1,1]。其中,“旋转点”为 i n d e x = 0 index = 0 index=0,此时 左子数组(即, [ 1 ] [1] [1]) 中元素均 > = n u m s [ 0 ] >=nums[0] >=nums[0]成立,但是 右子数组(即, [ 0 , 1 , 1 , 1 ] [0, 1, 1, 1] [0,1,1,1])中“所有””元素均 > = n u m s [ 0 ] >=nums[0] >=nums[0] 并非恒成立。即,此时“丢失两段性”,需要经过维护“两段性”后,才能使用 二分。

在这里插入图片描述

二分(维护“两段性”

如上分析,需要首先维护“两段性”,使得 左子数组中所有元素 > = n u m s [ 0 ] >= nums[0] >=nums[0]、右子数组中所有元素 < n u m s [ 0 ] < nums[0] <nums[0]时,才可以使用二分。

class Solution {
    public boolean search(int[] nums, int target) {
        // 找“旋转点”
        int n = nums.length;
        int left = 0;
        int right = n - 1;
        while (left < right && nums[0] == nums[right]) { // 恢复“二段性”
            right--;
        }
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= nums[0]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        int index = right; // “旋转点”,即[0, index]为左子数组
        System.out.println("index = " + index);
        // 第二次二分
        if (target >= nums[0]) { // target只“可能”在左子数组中
            left = 0;
            right = index; // 左闭右闭
        } else { // target只“可能”在右子数组中
            left = index + 1; 
            right = n - 1; // 左闭右闭
        }
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1; // 左闭右闭
            } else {
                return true;
            }
        }
        return false;
    }
}
  • 时间复杂度:最坏 O ( n ) O(n) O(n)(维护“两段性”需要线性扫描整个 n u m s nums nums),最好 O ( l o g n ) O(logn) O(logn)(不用维护“两段性”)
  • 空间复杂度: O ( 1 ) O(1) O(1)
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值