Lihang Liu's Homepage

LeetCode 笔记 5:Search in Rotated Sorted Array

今天要讲的题目都使用到了二分查找(或者和二分查找紧密相关),同时又都是基于一个 Rotated Sorted Array。那么先从 Rotated Sorted Array 的特点讲起吧。

Rotated Sorted Array 的定义是这样子的:

There is an integer array nums sorted in ascending order (with distinct values). nums is possibly rotated at an unknown pivot index k (1 <= k < nums.length) such that the resulting array is [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]] (0-indexed).

就拿 int[] nums = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 举例好了,如果 numsnums[4] 位置进行旋转,那么它将变成: {4, 5, 6, 7, 8, 9, 10, 0, 1, 2, 3} ,那么我们能够观察到几个特性:

  1. nums 被分成了两个区间,两个区间都是排好序的(严格上升)
  2. nums 第二个区间的所有元素小于第一个区间的最小元素

{4, 5, 6, 7, 8, 9, 10, 0, 1, 2, 3} 举例来说,从 nums[0]nums[5] 是第一个区间( {4, 5, 6, 7, 8, 9, 10} ),这个区间是排好序的,从 nums[6]nums[9] 是第二个区间,第二个区间也是排好序的。同时第二个区间的所有值均小于第一个区间的所有值。

根据这两个特性,我们就能够在子区间中使用二分查找来缩小查找范围了。如何在 rotated sorted array 中使用二分查找算法,请看题:

Search in Rotated Sorted Array

题目 1

33. Search in Rotated Sorted Array

算法思路是这样子的:

  1. 定义 left, right, mid ,计算 mid ,如果 nums[mid] == target ,那么就返回结果。
  2. 否则,我们需要缩小我们的查找空间。如何做呢?我们需要比较 nums[left] , nums[mid]nums[right] 的大小来判断当前的子区间是否是排好序的(即判断 [left, mid][mid, right] 这两个区间是否是排好序的):
    • 如果 nums[left] <= nums[mid] ,那么说明 [left, mid] 这个子区间是有序的;
    • 如果 nums[mid] <= nums[right] ,那么说明 [mid, right] 这个子区间是有序的;
  3. 在子区间是有序的前提下,我们再来判断 target 是否在该子区间中,以及它和 nums[mid] 的大小关系。之所以要在有序的子区间内进行比较的原因是,二分查找算法只能使用在排好序的数据集合当中。 根据 nums[mid]target 的大小关系比较,来移动左端点和右端点,就像寻常的二分查找那样:
    • 如果 nums[left] <= nums[mid] ,那么可能存在两种情况:
      1. nums[left] <= target && target < nums[mid] ,说明 target[left, mid] 这个区间内,我们只需要将 right 左移到 mid - 1 处即可;
      2. 否则,说明 target 不在 [left, mid] 区间内,我们右移 left 节点即可。
    • 如果是右半边子区间是有序的 (即 nums[mid] <= nums[right] ),同样可能存在两种情况:
      1. nums[mid] < target && target <= nums[right] :说明 target[mid, right] 这个区间内,右移 left 左端点;
      2. 否则,说明 target 不在这个区间内,我们左移 right
  4. 重复以上步骤直到找到了 target ,或 left > right

代码如下:


class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1, mid;

        while (left <= right) {
            mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                return mid;
            }

            if (nums[left] <= nums[mid]) {
                if (nums[left] <= target && target < nums[mid]) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } else {
                if (nums[mid] < target && target <= nums[right]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }

        return -1;
    }
}

复杂度分析:

  • 时间复杂度: O(logN)
  • 空间复杂度: O(1)

笔者认为,这类题目的关键点在于如何在找出可以使用二分查找的子区间上。因为 Rotated Sorted Array 的特性,导致我们无法确定 [left, right] 这个区间是排好序的,因此我们只能去分析更小范围的子区间的情况,也就是 [left, mid][mid, right] 这两个更小的子区间。一旦我们找到了排好序的区间,那么就可以使用二分查找算法的思想,来缩小搜索范围。

题目 2

这道题目在上一道题目的基础上只有一个不同,那就是 nums 可能存在重复元素,而这将影响我们的算法,举个例子:

int[] nums = new int[]{1, 1, 1, 1, 1, 0, 1, 1, 1, 1}

假设 nums 是这个样子的,我们只需要演算一下我们算法的第一次迭代即可看出问题:

  • left = 0 , right = 9 , mid = 0 + (9 - 0) / 2 = 4
  • nums[left] <= nums[mid] ,同时, nums[mid] <= nums[right]

我们发现 nums[mid] <= nums[right] ,那么按照我们之前的想法, [mid, right] 这个子区间应该是排好序的,但是 {1, 0, 1, 1, 1, 1} 却不是一个有序的数组。问题就出在重复元素上。

解决方法其实也很简单,我们只需要略过这些重复的、影响对我们数组是否有序判断的元素即可,整体算法不变:


class Solution {
    public boolean search(int[] nums, int target) {
        int left = 0, right = nums.length - 1, mid;

        while (left <= right) {
            while (left + 1 <= right && nums[left + 1] == nums[left]) {
                ++left;
            }
            while (right - 1 >= left && nums[right - 1] == nums[right]) {
                --right;
            }
            
            mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                return true;
            }

            if (nums[left] <= nums[mid]) {
                if (nums[left] <= target && target < nums[mid]) {
                    right = mid - 1;
                } else {
                    left = mid + 1;
                }
            } else {
                if (nums[mid] < target && target <= nums[right]) {
                    left = mid + 1;
                } else {
                    right = mid - 1;
                }
            }
        }

        return false;
    }
}

这两道题都是笔者之前博客 LeetCode 笔记 4:Binary Search 的5种变体应用 | caffcen’s blog 中提到的二分查找的第一种应用 Contains(即用二分查找判断排序数组是否包含目标)。

Search the Minimum in Rotated Sorted Array

题目 1

这道题目和之前题目的区别在于,之前题目给出了 target ,而这两个题目的 targetnums 中最小的元素。解决方法也很简单,只需要用到我们开篇提到的 rotated sorted array 的一个特性即可:

Rotated sorted array 的第一个子区间的最小值,大于第二个子区间的所有值。

既然第一个子区间的最小值都大于第二个子区间的所有值,那么整个 nums 中的最小值就是第二个子区间的第一个值了。那么目标就是去寻找第二个子区间的第一个值。

那么如何去寻找这个值呢?其实很简单,我们只需要做一定的转换即可:根据 rotated sorted array 的特性我们知道,第一个子区间的所有值均大于第二个子区间的所有值,那么我们二分查找时定义的右端点,一定小于左端点,即 nums[left] < nums[right] ,同时,第一个子区间中的所有值均大于 nums[right] ,它们也都不是我们要找的解。而题目的解即第一个值小于 nums[right] 的元素。

为了更好理解,我们定义这样一个有序集合 S = [a1, a2, a3, ..., an] ,同时 S 中的元素 a 表示 nums 的下标,且 a1 < a2 < a3 < ... < an 。同时,它们都满足 nums[a] < nums[right] 这一要求,也就是说 S 是一个合法的解集。那么,『求 nums 最小元素』这个问题,就可以转换成『求 S 这一解集中的第一个元素』这样一个问题。

经过这么一转换,可以看出这道题目和 LeetCode 笔记 4:Binary Search 的5种变体应用 | caffcen’s blog 中的 Index of least element greater than (or equal) to key最小的大于(或大于等于)目标的下标算法有异曲同工之妙:它们都是使用二分查找去求解集的第一个元素。


class Solution {
    public int findMin(int[] nums) {
        int left = 0, right = nums.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[right]) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return nums[left];
    }
}

题目 2

算法和上一题几乎一样,只是多了过滤重复元素的步骤,因此不再做过多赘述:


class Solution {
    public int findMin(int[] nums) {
        int left = 0, right = nums.length - 1;
        while (left < right) {
            while (left + 1 < right && nums[left + 1] == nums[left]) {
                ++left;
            }
            while (right - 1 > left && nums[right - 1] == nums[right]) {
                --right;
            }
            
            int mid = left + (right - left) / 2;
            if (nums[mid] > nums[right]) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return nums[left];
    }
}

虽然不好理解,但实际上这两道题目都是用 Binary Search 去求解集中的第一个解,也就是 LeetCode 笔记 4:Binary Search 的5种变体应用 | caffcen’s blog 中的 Index of least element greater than (or equal) to key,最小的大于(或大于等于)目标的下标 这一算法。

Reference

  1. 33. Search in Rotated Sorted Array
  2. 81. Search in Rotated Sorted Array II
  3. 153. Find Minimum in Rotated Sorted Array
  4. 154. Find Minimum in Rotated Sorted Array II