33. 搜索旋转排序数组 Search in Rotated Sorted Array

题目

整数数组 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:

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

示例 2:

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

示例 3:

  [plaintext]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
输入:nums = [1], target = 0 输出:-1 ```  提示: 1 <= nums.length <= 5000 -10^4 <= nums[i] <= 10^4 nums 中的每个值都 独一无二 题目数据保证 nums 在预先未知的某个下标上进行了旋转 -10^4 <= target <= 10^4 ## v1-二分法青春版 ### 思路 [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。 我们首先应该找到这个数组旋转的位置 k,然后把数组分为 2 个部分,这样依然是有序的。 然后采用分别二分法查询即可。 ### 实现 ```java /** * Input: nums = [4,5,6,7,0,1,2], target = 0 * Output: 4 * @param nums * @param target * @return */ public int search(int[] nums, int target) { // 没有旋转,或者全部旋转 int randomK = getRandomK(nums); if(-1 == randomK) { return binarySearch(nums, target, 0, nums.length-1); } // 将数组拆成2个部分 // 4 5 6 7 0 1 2 => [4 5 6 7] [0 1 2] // 3 1 ==> [3] [1] int leftIndex = binarySearch(nums, target, 0, randomK); if(leftIndex != -1) { return leftIndex; } // 右边寻找 int rightIndex = binarySearch(nums, target, randomK+1, nums.length-1); if(rightIndex != -1) { return rightIndex; } // 如果不存在 return -1; } /** * 获取随机数 * * 寻找 k > k+i 的位置 * * [4,5,6,7,0,1,2] * @param nums 数组 * @return 变化的长度 * @since v33 */ private int getRandomK(final int[] nums) { for(int i = 0; i < nums.length-1; i++) { if(nums[i] > nums[i+1]) { return i; } } // 根据顺序找到即可 return -1; } /** * 二分查询 * <p> * 备注:ASC * * @param nums 原始数组 * @param target 目标值 * @return 结果 * @since v33 */ private static int binarySearch(int[] nums, int target, int low, int high) { while (low <= high) { int mid = (high+low)/2; int midVal = nums[mid]; // 刚好相等 if (target == midVal) { return mid; } else if (target > midVal) { // 当前信息偏小 low = mid+1; } else { // 数据偏大 high = mid-1; } } //NOT FOUND return -1; }

评价

这个算法思路没有问题。

但是在计算 random-k 的时候,最差的复杂度为 O(N)。所以值得进一步优化。

v2-二分法

思路

对于有序数组,可以使用二分查找的方法查找元素。

但是这道题中,数组本身不是有序的,进行旋转后只保证了数组的局部是有序的,这还能进行二分查找吗?答案是可以的。

可以发现的是,我们将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的。

拿示例来看,我们从 6 这个位置分开以后数组变成了 [4, 5, 6] 和 [7, 0, 1, 2] 两个部分,其中左边 [4, 5, 6] 这个部分的数组是有序的,其他也是如此。

这启示我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:

如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l], nums[mid]],则我们应该将搜索范围缩小至 [l, mid-1],否则在 [mid + 1, r] 中寻找。

如果 [mid, r] 是有序数组,且 target 的大小满足 [nums[mid+1], nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。

二分法

java 实现

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public int search(int[] nums, int target) { int n = nums.length; if (n == 0) { return -1; } if (n == 1) { return nums[0] == target ? 0 : -1; } int l = 0, r = n - 1; while (l <= r) { int mid = (l + r) / 2; if (nums[mid] == target) { return mid; } if (nums[0] <= nums[mid]) { if (nums[0] <= target && target < nums[mid]) { r = mid - 1; } else { l = mid + 1; } } else { if (nums[mid] < target && target <= nums[n - 1]) { l = mid + 1; } else { r = mid - 1; } } } return -1; }

复杂度

时间复杂度: O(logn),其中 n 为 nums 数组的大小。整个算法时间复杂度即为二分查找的时间复杂度 O(logn)。

空间复杂度: O(1)。我们只需要常数级别的空间存放变量。

81. 搜索旋转排序数组 II Search in Rotated Sorted Array II

题目

已知存在一个按非降序排列的整数数组 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,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

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

例子

示例 1:

  [plaintext]
1
2
输入:nums = [2,5,6,0,0,1,2], target = 0 输出:true

示例 2:

  [plaintext]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
输入:nums = [2,5,6,0,0,1,2], target = 3 输出:false ```  进阶: 这是 搜索旋转排序数组 的延伸题目,本题中的 nums  可能包含重复元素。 这会影响到程序的时间复杂度吗?会有怎样的影响,为什么? ## 思路 整体和 T33 类似,但是因为包含重复元素,所以略微复杂些。 对于数组中有重复元素的情况,二分查找时可能会有 `a[l]=a[mid]=a[r]`,此时无法判断区间 [l,mid] 和区间 [mid+1,r] 哪个是有序的。 例如 nums=[3,1,2,3,3,3,3],target=2,首次二分时无法判断区间 [0,3] 和区间 [4,6] 哪个是有序的。 对于这种情况,我们只能将当前二分区间的左边界加一,右边界减一,然后在新区间上继续二分查找。 ## java 实现 ```java public boolean search(int[] nums, int target) { int n = nums.length; if (n == 0) { return false; } if (n == 1) { return nums[0] == target; } int l = 0, r = n - 1; while (l <= r) { int mid = (l + r) / 2; if (nums[mid] == target) { return true; } // 处理一下 if (nums[l] == nums[mid] && nums[mid] == nums[r]) { ++l; --r; } else if (nums[l] <= nums[mid]) { if (nums[l] <= target && target < nums[mid]) { r = mid - 1; } else { l = mid + 1; } } else { if (nums[mid] < target && target <= nums[n - 1]) { l = mid + 1; } else { r = mid - 1; } } } return false; }

复杂度

时间复杂度: O(logn),其中 n 为 nums 数组的大小。整个算法时间复杂度即为二分查找的时间复杂度 O(logn)。

空间复杂度: O(1)。我们只需要常数级别的空间存放变量。

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) 的算法解决此问题。

例子

示例 1:

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

示例 2:

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

示例 3:

  [plaintext]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
输入:nums = [11,13,15,17] 输出:11 解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。 ```  提示: n == nums.length 1 <= n <= 5000 -5000 <= nums[i] <= 5000 nums 中的所有整数 互不相同 nums 原来是一个升序排序的数组,并进行了 1 至 n 次旋转 ## 思路 还是那句话,有序的数组,查找元素使用二分法! 但是问题是,已经被旋转了,还能使用吗?

The minimum element must satisfy one of two conditions:

[4,5,6,7,0,1,2]

1) If rotate, A[min] < A[min - 1];

2) If not, A[0].

Therefore, we can use binary search: check the middle element, if it is less than previous one, then it is minimum.

If not, there are 2 conditions as well: If it is greater than both left and right element, then minimum element should be on its right, otherwise on its left.

  [plaintext]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
## java 实现 ```java public int findMin(int[] nums) { int start = 0; int end = nums.length - 1; while (start < end) { int mid = (start +end) / 2; // 如果是旋转的场景。不旋转的话,一定大于前面 // [4,5,6,7,0,1,2] if(mid > 0 && nums[mid] < nums[mid-1]) { return nums[mid]; } // 如果当前元素比2边都大,那就是右边。 if(nums[mid] >= nums[start] && nums[mid] >= nums[end]) { start = mid+1; } else { end = mid-1; } } return nums[start]; }

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

题目

和 153 类似,编程数组中的元素可能重复。

思路

首先,我们取 low = 0, high = nums.length-1

0 1 2 3 4

1)默认情况下,如果 nums[lo] < nums[hi] 那么我们返回 nums[lo] 因为数组从未旋转过,或者旋转过 n 次。

2)进入while循环后,我们检查

if nums[mid] > nums[hi] => lo = mid + 1 因为最小元素在数组的右半部分

2 3 4 0 1

else if nums[mid] < nums[hi] => hi = mid 因为最小元素在数组的左半部分

7 0 1 2 3 4 5 6

else => hi-- 处理重复值

然后我们返回 nums[hi]

java 实现

  [java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int findMin(int[] nums) { int low = 0, high = nums.length-1; // default if(nums[low] < nums[high]) { return nums[low]; } while (low < high) { int mid = (low + high) / 2; //1. 大于最大,在右边 if(nums[mid] > nums[high]) { low = mid+1; } else if(nums[mid] < nums[high]) { //2. 小于最大,则在左边 high = mid; } else { // 重复 high--; } } return nums[high]; }

小结

可以看到有序的数组,再处理一个元素的时候,我们首先应该使用二分法。

只不过会有一些限制,但是核心思路不会变化。

开源地址

为了便于大家学习,所有实现均已开源。欢迎 fork + star~

https://github.com/houbb/leetcode

参考资料

https://leetcode.cn/problems/search-in-rotated-sorted-array/

https://leetcode.cn/problems/find-minimum-in-rotated-sorted-array/