153. 寻找旋转排序数组中的最小值 & 154. 寻找旋转排序数组中的最小值 II ●●

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

描述

已知一个长度为 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) 的算法解决此问题。

示例

输入:nums = [3,4,5,1,2]
输出:1
解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

输入:nums = [4,5,6,7,0,1,2]
输出:0
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

题解

二分法

定义区间边界 left、right,考虑区间的右边界元素 nums[right],在最小值右侧的元素(不包括最后一个元素本身),它们的值一定都严格小于 nums[right];而在最小值左侧的元素,它们的值一定都严格大于 nums[right]。因此,我们可以根据这一条性质,通过二分查找的方法找出最小值。

我们将中轴元素 nums[mid] 与右边界元素 nums[right] 进行比较,可能会有以下的三种情况:

(1)第一种情况是 nums[mid]<nums[right]。如下图所示,这说明 nums[mid] 是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分,right = mid。
在这里插入图片描述
(2)第二种情况是 nums[mid]>nums[right]。如下图所示,这说明 nums[mid] 是最小值左侧的元素,因此我们可以忽略二分查找区间的左半部分,left = mid + 1。
在这里插入图片描述

(3)由于数组不包含重复元素,并且只要当前的区间长度不为 1,mid 就不会与 right 重合;而如果当前的区间长度为 1,这说明我们已经可以结束二分查找了。因此不会存在 nums[mid]=nums[right] 的情况。

当二分查找结束时,我们就得到了最小值所在的位置。

  • 时间复杂度: O ( log ⁡ n ) O(\log n) O(logn),其中 n 是数组 nums 的长度。在二分查找的过程中,每一步会忽略一半的区间,因此时间复杂度为 O(logn)。
  • 空间复杂度:O(1)。

二分法中每次循环中 left 和 right 共同约束了本次查找的范围(左闭右闭或左闭右开等), 要让本次循环与上一次循环查找的范围既不重复(重复了会引起死循环), 也不遗漏, 并且要让 left 和 right 共同约束的查找的范围变得无意义时不再进行查找(即跳出 while)(否则会导致访问越界), 这其实就是所谓的循环不变量。因此要清楚对查找区间的定义,在循环中根据查找区间的定义来做边界处理。

class Solution {
    
    
public:
    int findMin(vector<int>& nums) {
    
    
        int n = nums.size();
        if(n == 1) return nums[0];
        if(nums[0] < nums[n-1]) return nums[0];
        int left = 0, right = n-1;			// 左闭右闭
        while(left < right){
    
    				/* 循环不变式,如果left == right,则循环结束 */
            int mid = (left + right) >> 1;	// 向下整除
            if(nums[mid] < nums[right]){
    
    
                right = mid;
            }else{
    
              				// if(nums[mid] > nums[right])
                left = mid + 1;				// mid偏向与左边, 使用left=mid+1,right=mid
            }
        }
        return nums[left];					/* 循环结束,left == right,最小值输出nums[left]或nums[right]均可 */  
    }
};

为什么比较 mid 与 right 而不比较 mid 与 left?
简单讲就是因为我们找最小值,要偏向左找,目标值右边的情况会比较简单,容易区分,所以比较 mid 与 right 而不比较 mid 与 left。

那么能不能通过比较 mid 与 left 来解决问题?
能,转换思路,不直接找最小值,而是先找最大值,最大值偏右,可以通过比较 向上整除 的 mid 与 left 来找到最大值,最大值向右移动一位就是最小值了(需要考虑最大值在最右边的情况,右移一位后对数组长度取余)。

class Solution {
    
    
public:
    int findMin(vector<int>& nums) {
    
    
        int n = nums.size();
        if(n == 1) return nums[0];
        if(nums[0] < nums[n-1]) return nums[0];
        int left = 0, right = n-1;			    // 左闭右闭
        while(left < right){
    
    				
            int mid = (left + right + 1) >> 1;	// 向上整除
            if(nums[mid] < nums[left]){
    
    
                right = mid - 1;                // 缩移右边界
            }else{
    
              				    
                left = mid;                     // 缩移左边界
            }
        }
        return nums[(right + 1) % n];			// 右移一位取余(考虑最大值在最右边)
    }
};

使用 left < right 作while循环条件可以很方便推广到数组中有重复元素的情况,即154. 寻找旋转排序数组中的最小值 II

初始条件是左闭右闭区间 right = nums.size() - 1,
那么能否将 while 循环的条件也选为左闭右闭区间 left <= right 呢?

class Solution {
    
    
public:
    int findMin(vector<int>& nums) {
    
    
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right) {
    
                             // 循环的条件选为左闭右闭区间left <= right
            int mid = left + (right - left) / 2;
            if (nums[mid] >= nums[right]) {
    
                 // 注意是当中值大于等于右值时,
                left = mid + 1;                         // 将左边界移动到中值的右边
            } else {
    
                                        // 当中值小于右值时
                right = mid;                            // 将右边界移动到中值处
            }
        }
        return nums[right];                             // 最小值返回nums[right]
    }
};

作者:armeria-program
链接:https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/solution/er-fen-cha-zhao-wei-shi-yao-zuo-you-bu-dui-cheng-z/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

描述

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

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

示例

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

题解

二分法

将中轴元素 nums[mid] 与右边界元素 nums[right] 进行比较,可能会有以下的三种情况:

(1)第一种情况是 nums[mid]<nums[right]。如下图所示,这说明 nums[mid] 是最小值右侧的元素,因此我们可以忽略二分查找区间的右半部分,right = mid。
在这里插入图片描述
(2)第二种情况是 nums[mid]>nums[right]。如下图所示,这说明 nums[mid] 是最小值左侧的元素,因此我们可以忽略二分查找区间的左半部分,left = mid + 1。
在这里插入图片描述

(3)第三种情况是 nums[mid]==nums[right]。如下图所示,由于重复元素的存在,我们并不能确定 nums[mid] 究竟在最小值的左侧还是右侧,因此我们不能莽撞地忽略某一部分的元素。我们唯一可以知道的是,由于它们的值相同,所以无论 nums[right] 都不是最小值,都有一个它的「替代品」nums[mid],因此我们可以忽略二分查找区间的右端点

在这里插入图片描述

  • 时间复杂度:平均时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),其中 n 是数组 nums 的长度。
    如果数组是随机生成的,那么数组中包含相同元素的概率很低,在二分查找的过程中,大部分情况都会忽略一半的区间。
    而在最坏情况下,如果数组中的元素完全相同,那么 while 循环就需要执行 n 次,每次忽略区间的右端点,时间复杂度为 O(n)。
  • 空间复杂度:O(1)。
class Solution {
    
    
public:
    int findMin(vector<int>& nums) {
    
    
        int left = 0, right = nums.size() - 1;
        while(left < right){
    
    
            int mid = (left + right) >> 1;  // 向下取整,与右边界比较,查找最小值
            if(nums[mid] < nums[right]){
    
    
                right = mid;
            }else if(nums[mid] > nums[right]){
    
    
                left = mid + 1;
            }else{
    
    
                --right;                    // 去重,可能结果在 mid 和 right 之间
            }
        }
        return nums[right];                 // 退出循环时 left = right
    }
};

有重复元素时,无法通过确定最大值通过直接加 1 位来找到最小值。

拓展:找到旋转数组的最大值:

class Solution {
    
    
public:
    int findMin(vector<int>& nums) {
    
    
        int left = 0, right = nums.size() - 1;
        while(left < right){
    
    
            int mid = (left + right + 1) >> 1;  // 向上取整,与左边界比较,查找最大值
            if(nums[mid] < nums[left]){
    
    
                right = mid - 1;                // 缩移右边界
            }else if(nums[mid] > nums[left]){
    
    
                left = mid;                     // 缩移左边界
            }else{
    
    
                ++left;                         // 去重,可能结果在 mid 和 left 之间
            }
        }
        return nums[left];      // 退出循环使 left = right
    }
};

猜你喜欢

转载自blog.csdn.net/qq_19887221/article/details/126268218