旋转排序数组题目的解法一般都是二分查找。大家都知道二分查找是细节狂魔,下面我们来总结一下leetcode上5题关于旋转排序数组题目,来得到一些解题经验。题目如下:
文章首发于👉团灭5题leetcode旋转排序数组
第一题【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 。
对于有序数组,我们可以使用二分查找来找到元素。本题中,数组本身不是有序的,进行旋转后只保证了数组的局部是有序的。其实这道题的关键就是局部有序,将数组从中间分开成左右两部分的时候,一定有一部分的数组是有序的,我们就可以从有序的那一部分入手。下面举两个情况,以题目中的例子来说。
- [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2],那么此时左边边界 left数值4 到 mid数值7 是保持升序的,我们可以先判断target是不是在此区间,以此来收缩right边界或者left边界。
- [0,1,2,4,5,6,7] 在下标 4 处经旋转后可能变为 [5,6,7,0,1,2,4],那么此时左边边界 mid数值0 到 right数值4 是保持升序的,我们可以先判断target是不是在此区间,以此来收缩left边界或者right边界
最终的代码如下,细节点已经注释:
var search = function (nums, target) {
let left = 0;
let right = nums.length - 1;
// 细节点1
// 如果是left<right, 那么当left===right时结束循环
// 此时有可能nums[left]===nums[right] === target, 错过答案
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] === target) return mid;
// 细节点2
// 当中间值与左边界left对应的元素相等时,将左边界left右移
// 因为此时arr[left]===arr[mid]但是arr[mid] !== target,即arr[left] !== target
// 所以target一定落在[l+1,r]区间内
if (nums[mid] === nums[left]) {
left++;
} else if (nums[mid] > nums[left]) {
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return -1;
};
第二题【81. 搜索旋转排序数组 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 。
你必须尽可能减少整个操作步骤。
对于数组中有重复数值的情况,二分查找时可能会有 nums[l]===nums[mid]===nums[r]的情况,此时无法判断区间 [l,mid] 和区间 [mid+1,r] 哪个是有序的。直接上题解:
var search = function (nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] === target) return true;
// 当中间值与左边界left对应的元素相等时,将左边界left右移
// 因为此时arr[left]==arr[mid]但是arr[mid] != target,即arr[left] != target
// 所以target一定落在[l+1:r]区间内
if (nums[left] === nums[mid]) {
left++;
// 同理
} else if (nums[right] === nums[mid]) {
right--;
}
// 后面的处理跟第一题一样
else if (nums[left] < nums[mid]) {
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
if (target > nums[mid] && target <= nums[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return false;
};
第三题【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[mid] = nums[left] 此时mid和left重合,更新min最小值,left++
- nums[mid] > nums[left] 此时left到mid保持升序,此段区间内最小值为left,更新min最小值,需要看mid到right段,left = mid+1
- nums[mid] < nums[left] 此时mid到right保持升序,此段区间内最小值为mid,更新min最小值,需要看left到mid段,right = mid-1
其中由于元素的值互不相同,1和2的情况可以合并。代码如下:
var findMin = function (nums) {
let left = 0;
let right = nums.length - 1;
let min = Number.MAX_SAFE_INTEGER;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] >= nums[left]) {
min = Math.min(min, nums[left]);
left = mid + 1;
} else {
min = Math.min(min, nums[mid]);
right = mid - 1;
}
}
return min;
};
第四题【154.寻找旋转排序数组中的最小值-ii】
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。
例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
若旋转 7 次,则可以得到 [0,1,4,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 ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。
请你找出并返回数组中的 最小元素 。
你必须尽可能减少整个过程的操作步骤。
参考第三题的题解,不合并1和2的情况,因为本题中元素的值可以重复,nums[left] === nums[mid] 时left!==mid, left++而不是 left = mid + 1。代码如下:
var findMin = function (nums) {
let left = 0
let right = nums.length - 1
let min = Number.MAX_SAFE_INTEGER
while (left <= right) {
const mid = Math.floor((left + right) / 2)
if (nums[left] === nums[mid]) {
// [1,1,1,1,1,1,1,1]
min = Math.min(min, nums[left])
left++
} else if (nums[left] < nums[mid]) {
min = Math.min(min, nums[left])
left = mid + 1
} else if (nums[left] > nums[mid]) {
min = Math.min(min, nums[mid])
right = mid - 1
}
}
return min
};
第五题【面试题 10.03. 搜索旋转数组】
搜索旋转数组。给定一个排序后的数组,包含n个整数,但这个数组已被旋转过很多次了,次数不详。
请编写代码找出数组中的某个元素,假设数组元素原先是按升序排列的。若有多个相同元素,返回索引值最小的一个。
示例1:
输入: arr = [15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14], target = 5
输出: 8(元素5在该数组中的索引)
示例2:
输入:arr = [15, 16, 19, 20, 25, 1, 3, 4, 5, 7, 10, 14], target = 11
输出:-1 (没有找到)
这道题目是上面几题的加强版本,注意关键字眼:【多个相同元素】,【返回索引值最小的一个】
大体上的思路是有多个相同元素,我们可以逐步向左逼近,直到arr[left] === target,所以当target === arr[mid]时,我们需要更新right边界,即right = mid。其余思路跟上面几题相同。
代码如下:
var search = function (arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
// 当左边界l对应元素为target时直接返回l,因为题目要求返回最小索引
if (arr[left] === target) return left;
const mid = Math.floor((left + right) / 2);
// 当中间值等于target时,将右边界r左移到mid,因为mid左边可能还有等于target的元素
if (target === arr[mid]) right = mid;
else if (arr[left] > arr[mid]) {
if (target > arr[mid] && target <= arr[right]) {
left = mid + 1;
} else {
right = mid - 1;
}
} else if (arr[left] < arr[mid]) {
if (target >= arr[left] && target < arr[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
} else {
// 当中间值与左边界l对应的元素相等时,将左边界l右移
// 因为此时arr[l]==arr[mid]但是arr[mid] != target,即arr[l] != target
// 所以target一定落在[l+1:r]区间内
left = left + 1;
}
}
return -1;
};
总结
最后我们可以总结一下旋转排序数组带重复元素的代码思路:
var search = function (nums, target) {
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (nums[mid] === target) return true;
// 当中间值与左边界left对应的元素相等时,将左边界left右移
// 因为此时arr[left]==arr[mid]但是arr[mid] != target,即arr[left] != target
// 所以target一定落在[l+1:r]区间内
if (nums[left] === nums[mid]) {
left++;
// 同理
} else if (nums[right] === nums[mid]) {
right--;
}
// 左边边界 left 到 mid 是保持升序的
else if (nums[left] < nums[mid]) {
// 我们可以先判断target是不是在此区间, 此来收缩right边界
if (target >= nums[left] && target < nums[mid]) {
right = mid - 1;
} else {
// left边界
left = mid + 1;
}
} else {
// 左边边界 mid 到 right 是保持升序
if (target > nums[mid] && target <= nums[right]) {
// 收缩left边界
left = mid + 1;
} else {
// right边界
right = mid - 1;
}
}
}
return false;
};