LeetCode | 旋转排序数组问题总结

前言

​ 所谓旋转排序数组,就是指按照升序排序的数组在预先未知的某个点上进行了旋转(例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2])。而LeetCode上与此相关的的题共有三道,下面就具体来分析一下这三题。

搜索旋转排序数组(LeetCode 33题)

搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

分析

​ 从题目要求时间复杂度为O(logn),则可知最终必须使用二分法。然后再具体分析旋转数组的特征,假设在下标i处进行了旋转,旋转后的序列为i ~ n-1, 0 ~ i-1,由于数组本身是升序排列的,所以仔细分析mid中间结点的位置,便可以找到这题的突破口,假设mid处于i ~ n-1区间内,那么i ~ mid段就是有序的,相对地,mid ~ i-1就是无序的。当mid结点处于0 ~ i-1区间时,易知mid ~ i-1段是有序的,相对地,i ~ mid就是无序的。可以发现,无论mid在哪个位置,总能得到一个有序段,最终我们也就是利用有序段,来每次排除一半的数据。

​ 为了分析有序段,再看题目,因为数组为升序,因此我们每次只需要和最右边的结点进行比较即可。若nums[mid] < nums[right],则mid ~ right段为有序,若nums[mid] > nums[right],由于数组整体为升序,原数组中左边的数据一定是小于右边的,因此在mid ~ right存在一个旋转点,即该段是无序的,再由上文分析可知,无论mid处于哪一段,总存在有序区间,因此此时i ~ mid段是有序的。

​ 当我们得到有序段后,我们便可以根据target是否存在于有序段来取舍一半的数据,最终的代码如下:

    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = (left + right) >>> 1;
            if (nums[mid] == target) {
                return mid;
            }
            if (nums[mid] > nums[right]) {
                // target是否存在于left ~ mid有序段
                if (nums[left] <= target && nums[mid] > target) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } else {
                // target是否存在于mid ~ right有序段
                if (nums[right] >= target && nums[mid] < target) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }
        return -1;
    }
复杂度分析
  • 时间复杂度:每次取舍一半的数据,为二分查找,故为O(logn);
  • 空间复杂度:未使用额外空间,为O(1)。

寻找旋转排序数组中的最小值(LeetCode153题)

分析

​ 本题的条件和上一题相同,取别是上题要求找到一个目标值,而本题则要求找到最小值。再次回想之前的分析,假设在下标i处进行了旋转,旋转后的序列为i ~ n-1, 0 ~ i-1,对于mid结点而言,由之前的分析可知,可以由mid结点与最右边的结点的值的大小关系,判断出有序区间处于mid之前还是之后。再次分析有序区间,假设i ~ mid段为有序,那么数组一定是在mid ~ i-1段的某个位置进行了旋转,相应地,最小值也存在于这个区间,即存在于无序段。当mid ~ i-1为有序时,则根据上文的分析,最小值也一定存在于存在旋转结点的区间,即无序段。

​ 经过上文的分析,可知最小值(也就是旋转结点)一定存在于无序段,因此代码也很容易得出,如下:

    public int findMin(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        while (left < right) {
            int mid = (left + right) >>> 1;
            // mid > right, 则mid必定不为最小值, 
            // 结合left ~ mid为有序段,故可直接舍弃left ~ mid段 
            if (nums[mid] > nums[right]) {
                left = mid + 1;
            } else {
                // mid < right, mid处可能为最小值,
                // 因此对于有序段mid ~ right, 只能舍弃mid+1 ~ right
                right = mid;
            }
        }
        return nums[left];
    }
复杂度分析
  • 时间复杂度:O(logn);
  • 空间复杂度:O(1)。

存在重复值的数组

​ 以上两题均是不考虑数组有重复元素的情况,而LeetCode81题和154题,则正是以上两题存在重复元素的改变。当存在重复元素时,我们在判断midright结点的大小也将多了一种可能相等的情况,那么怎么考虑相等呢?其实我们只要每次在相等情况判断时,都只将right--即可,因为不管是寻找目标值target,还是寻找数组最小值,因为midright相等,所以每次去掉最右结点,再对剩下部分之前的判断,即可得到最终的结果。但是,在有重复值的情况下,算法的时间复杂度也有了改变,在最好的情况下(数组中不存在重复元素),仍未O(logn)。但在最坏的情况下(数组元素全部相等),由于每次只能将数组大小缩减1,因此将退化为O(n)。

猜你喜欢

转载自blog.csdn.net/qq_41698074/article/details/107549017