您现在的位置是:首页 >技术教程 >【刷题之路Ⅱ】LeetCode 33&81.搜索旋转排序数组Ⅰ&Ⅱ网站首页技术教程

【刷题之路Ⅱ】LeetCode 33&81.搜索旋转排序数组Ⅰ&Ⅱ

林先生-1 2023-05-25 04:00:03
简介【刷题之路Ⅱ】LeetCode 33&81.搜索旋转排序数组Ⅰ&Ⅱ

一、题目描述

原题连接: 33. 搜索旋转排序数组 81. 搜索旋转排序数组 II
题目描述:
33题的描述如下:

整数数组 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,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

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

示例 2:

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

示例 3:

输入: nums = [1], target = 0
输出: -1

提示:
1 <= nums.length <= 5000
-104 <= nums[i] <= 104
nums 中的每个值都 独一无二
题目数据保证 nums 在预先未知的某个下标上进行了旋转
-104 <= target <= 104

而81题只是在33题的基础上增加了有重复元素的条件。

二、解题

1、方法1——暴力法

1.1、思路分析

顺序遍历数组中所有的元素,遇到nums[i] == target返回i即可。

1.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

int search1(int* nums, int numsSize, int target) {
    assert(nums);
    int i = 0;
    for (i = 0; i < numsSize; i++) {
        if (nums[i] == target) {
            return i;
        }
    }
    return -1;
}

时间复杂度:O(n),n为数组长度。
空间复杂度:O(1),我们只需要用到常数级的额外空间。

当然啦,暴力法是万能的,所以对于81题这个方法根本不用做任何修改也能直接通过。

2、方法2——二分法

2.1、思路分析

看到题目中给的“有序”我们就应该想到要用二分查找法,但这里的有序并不是完全有序,而是部分有序。
而我们知道,对于完全有序的序列,我们是可以百分百的确定一个数是否在这个序列中的,那我们就可以用二分法的变种——二分搜索,该算法可以每次淘汰一半的数据,具体思路如下:
先判断nums[left]和nums[mid]和nums[right]中有没有等于target的的,如果有直接返回下标即可;
若nums[mid] != target,则应判断mid两边的区间[left, mid] 和 [mid, right]主要看有序的那边,这里优先判断左端是否有序。如果target在有序的那边,那就转而搜索有序的那边:
在这里插入图片描述
就将搜索区间改成[left + 1, mid - 1] (因为两端和中点都被判断过):
在这里插入图片描述
否则,转而判断另一端:
在这里插入图片描述
这里给出该方法的递归版本。

2.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

// 先写一个递归的二分搜索算法
int binary_search(int* nums, int left, int right, int target) {
    assert(nums);
    if (left > right) {
        return -1;
    }
    int mid = left + (right - left) / 2;
    if (nums[left] == target) {
        return left;
    }
    else if (nums[mid] == target) {
        return mid;
    }
    else if (nums[right] == target) {
        return right;
    }
    else {
        if (nums[left] < nums[mid]) {
            if (target > nums[left] && target < nums[mid]) {
                return binary_search(nums, left + 1, mid - 1, target);
            }
            else {
                return binary_search(nums, mid + 1, right - 1, target);
            }
        }
        else {
            if (target > nums[mid] && target < nums[right]) {
                return binary_search(nums, mid + 1, right - 1, target);
            }
        }
    }
    return binary_search(nums, left + 1, mid - 1, target);
}

int search2(int* nums, int numsSize, int target) {
    assert(nums);
    return binary_search(nums, 0, numsSize - 1, target);
}

时间复杂度:O(logn),n为数组长度。
空间复杂度:O(1)。

2.3、升级到81题

2.3.1、改进思路分析

我们看到81题的描述中其实就只是增加了一个条件,就是可能有重复元素:
在这里插入图片描述
但就是因为增加了这个条件才导致了一个不怎么好解决的问题,那就是可能会出现nums[left]和nums[mid]和nums[right]都相同的情况,例如:
在这里插入图片描述
这时候如果再用像33题那样的判断方法,就无法判断出哪一端是有序或无序的。
这个时候我们其实可以继续递归判断区间[left + 1, right - 1]的,因为在nums[left]和nums[right]相同的时候,在区间[left, right]和在区间[left + 1, rihgt - 1]中查找是等价的:
在这里插入图片描述
然后其他地方只需要改变一处,就是在判断左端是否有序时将nums[left] < nums[mid]改成nums[left] <= nums[mid]即可。

2.3.1、改进代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

// 先写一个递归版的二分查找法
bool binary_search(int* nums, int left, int right, int target) {
    assert(nums);
    if (left > right) {
        return false;
    }
    int mid = left + (right - left) / 2;
    if (nums[left] == target) {
        return true;
    }
    else if (nums[mid] == target) {
        return true;
    }
    else if (nums[right] == target) {
        return true;
    }
    else if (nums[left] == nums[mid] && nums[mid] == nums[right]) {
        return binary_search(nums, left + 1, right - 1, target);
    }
    else if (nums[left] <= nums[mid]) {
        if (target > nums[left] && target < nums[mid]) {
            return binary_search(nums, left + 1, mid - 1, target);
        }
        else {
            return binary_search(nums, mid + 1, right - 1, target);
        }
    } else {
        if (target > nums[mid] && target < nums[right]) {
            return binary_search(nums, mid + 1, right - 1, target);
        }
    }
    return binary_search(nums, left + 1, mid - 1, target);
}
bool search(int* nums, int numsSize, int target){
    assert(nums);
    return binary_search(nums, 0, numsSize - 1, target);
}

时间复杂度:O(logn),n为数组长度。
空间复杂度:O(1)。

3、改进二分法

3.1、思路分析

对于33题,其实我们可以这样来改进算法:
其实我们可以通过nums[0]和nums[mid]的大小关系来判断mid所在的区间范围,当nums[mid]>nums[0]时:
在这里插入图片描述
说明mid所在的区间为较大的那部分升序,而当nums[mid] < nums[0]时,则说明mid所在的区间为较小的那一部分升序:
在这里插入图片描述
所以,我们就可以这样来改进我们的算法:
当nums[mid] >= nums[0]且nums[0] < target < nums[mid]时,说明target在范围为[left, mid ]“大部分"区间里,所以执行right = mid - 1:
在这里插入图片描述
当nums[mid] < nums[0]但target < nums[mid]时,则说明mid所在的区间为"小部分”,而target比nums[mid]更小,所以执行right = mid - 1:
在这里插入图片描述
当nums[mid] < nums[0]且target >= nums[0]时,说明mid所在的区间为"小部分",而target所在的区间为"大部分"所以我们还是要执行right = mid - 1:
在这里插入图片描述

其他情况都执行left = mid + 1;

3.2、代码实现

有了以上思路,那我们写起代码来也就水到渠成了:

int search3(int* nums, int numsSize, int target) {
    assert(nums);
    int left = 0;
    int right = numsSize - 1;
    int mid = 0;
    while (left < right) {
        mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return mid;
        }
        else if (nums[mid] >= nums[0] && nums[0] <= target && target  <= nums[mid]) {
            right = mid;
        }
        else if (nums[mid] < nums[0] && target < nums[mid]) {
            right = mid;
        }
        else if (nums[mid] < nums[0] && target >= nums[0]) {
            right = mid;
        }
        else {
            left = mid + 1;
        }
    }
    return left == right && nums[left] == target ? left : -1;
}

时间复杂度:O(logn),n为数组的长度。
空间复杂度:O(1),我们只需要用到常数级的额外空间。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。