算法背景
旋转数组是将一个原本有序的数组在某一个下标处进行旋转(即将前面部分移到后面)得到的数组。例如 [0,1,2,4,5,6,7] 在下标3处旋转后变为 [4,5,6,7,0,1,2]。
旋转数组问题的核心挑战在于:数组局部有序但整体无序,传统的二分查找需要经过改造才能适用。经典的旋转数组问题包括:寻找最小值、搜索特定值、判断数组是否由有序数组旋转得到等。
核心思路
旋转数组有一个重要性质:将数组从中间切开后,至少有一半是有序的。利用这一性质,我们可以在O(log n)时间内完成搜索。
对于寻找最小值的问题:比较中间元素与右边界元素,若 nums[mid] > nums[right],说明最小值在右半部分;若 nums[mid] < nums[right],说明最小值在左半部分(包含mid);若相等则无法判断,需要缩小右边界。
Java实现
/**
* 旋转数组相关算法实现
* 包含寻找最小值、搜索目标值、判断旋转次数等
*/
public class RotatedSortedArray {
/**
* 寻找旋转数组中的最小值
* 数组无重复元素
* 时间复杂度:O(log n)
*/
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 if (nums[mid] < nums[right]) {
// 最小值在左半部分(包含mid)
right = mid;
}
// 无重复元素时不会出现相等的情况
}
return nums[left];
}
/**
* 寻找旋转数组中的最小值(含重复元素版本)
* 最坏情况时间复杂度:O(n),平均O(log n)
*/
public int findMinWithDuplicates(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 if (nums[mid] < nums[right]) {
right = mid;
} else {
// 相等时无法判断,保守地缩小右边界
right--;
}
}
return nums[left];
}
/**
* 在旋转数组中搜索目标值
* 时间复杂度:O(log n)
*/
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
// 判断哪一半是有序的
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 -1; // 未找到
}
/**
* 计算数组的旋转次数(即最小值下标)
* 时间复杂度:O(log n)
*/
public int rotationCount(int[] nums) {
int left = 0, right = nums.length - 1;
int n = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] > nums[right]) {
left = mid + 1;
} else if (nums[mid] < nums[right]) {
right = mid;
} else {
right--;
}
}
return left;
}
/**
* 判断数组是否由有序数组旋转得到
*/
public boolean isRotatedSorted(int[] nums) {
int n = nums.length;
int count = 0; // 记录"断点"数量
for (int i = 0; i < n; i++) {
if (nums[i] > nums[(i + 1) % n]) {
count++;
}
if (count > 1) return false;
}
return true;
}
/**
* 主函数:测试与演示
*/
public static void main(String[] args) {
RotatedSortedArray rsa = new RotatedSortedArray();
int[] nums = {4, 5, 6, 7, 0, 1, 2};
System.out.println("数组: [4,5,6,7,0,1,2]");
System.out.println("最小值: " + rsa.findMin(nums));
System.out.println("旋转次数: " + rsa.rotationCount(nums));
System.out.println("搜索0的下标: " + rsa.search(nums, 0));
System.out.println("搜索3的下标: " + rsa.search(nums, 3));
System.out.println("是否为旋转有序数组: " + rsa.isRotatedSorted(nums));
int[] dupNums = {2, 2, 2, 0, 1, 2};
System.out.println("\n含重复数组: [2,2,2,0,1,2]");
System.out.println("最小值: " + rsa.findMinWithDuplicates(dupNums));
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 寻找最小值 | O(log n) | O(1) | 无重复元素 |
| 寻找最小值(含重复) | O(n) 最坏 | O(1) | 最坏情况需线性扫描 |
| 搜索目标值 | O(log n) | O(1) | 先判断哪半部分有序 |
| 判断旋转有序 | O(n) | O(1) | 统计断点数量 |
应用场景
- 日志文件分析:循环缓冲区中的时间戳搜索
- 循环队列:在循环存储结构中高效定位元素
- 音乐播放列表:循环播放模式下的歌曲索引定位
总结
旋转数组问题是二分查找的经典变种。核心思想在于利用”至少一半有序”的性质,每次迭代都能确定目标所在的区间。本文提供了寻找最小值、搜索目标值、判断旋转次数等常见操作的Java实现,帮助读者掌握处理局部有序数据结构的二分查找技巧。