最近忙着期末考试,刚好考完了两科,今天抽出时间继续刷题!
链接:搜索旋转排序数组
Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand.
(i.e., [0,1,2,4,5,6,7] might become [4,5,6,7,0,1,2]).
You are given a target value to search. If found in the array return its index, otherwise return -1.
You may assume no duplicate exists in the array.
Your algorithm’s runtime complexity must be in the order of O(log n).
Example 1:
Input: nums = [4,5,6,7,0,1,2], target = 0
Output: 4
Example 2:
Input: nums = [4,5,6,7,0,1,2], target = 3
Output: -1
题目大意
给定一个“有序数组”,该数组在未知的某个点上进行了旋转,
比如一个有序数组[0,1,2,4,5,6,7],根据某点进行了旋转,变成了[4,5,6,7,0,1,2],即一个局部有序的数组,题目要求我们根据target的值返回它的索引,比如target为5,则返回1,表示5在1的位置。
一开始我没按照题目要求的复杂度,首先使用了一个map辅助存储每个元素的值以及下标(时间复杂度为O(N)),并将其原数组排序(快排,时间复杂度为O(log(N) * N)),由于数组已经全部有序了,可以直接使用二分查找(时间复杂度为O(log(N))),并返回map中的元素下标,看一下代码吧:
代码
public int search(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<>();
// 将原数组的值跟下标放进map中
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], i);
}
// 排序原数组
Arrays.sort(nums);
// 二分查找中左指针,右指针跟指向中间值的指针
int left = 0;
int right = nums.length - 1;
int mid;
// 二分查找元素是否存在
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
return map.get(nums[mid]);
}
}
// 若查找不到则返回-1
return -1;
}
代码讲解
首先使用HashMap存储每个元素的值以及下标,值为key,下标为value,如以下代码所示:
Map<Integer, Integer> map = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
map.put(nums[i], i);
}
接着将原数组排序,排序的目的是为了进行二分查找,因为二分查找的前提是数组有序,如以下代码所示:
Arrays.sort(nums);
然后就是直接二分查找元素是否存在了,若找到元素target,则返回当前元素nums[mid]在map中的value,即元素的原始下标,如以下代码所示:
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
return map.get(nums[mid]);
}
}
提交之后发现,时间很慢,才打败了22.75%的人,那我们从减少时间复杂度的角度切入。
由于原数组是一个旋转排序数组,以某个元素为分界点,将数组分为左数组跟右数组,如下图所示:
为什么要分为两个数组出来,这是为了减少搜索无关项浪费的时间,假设target为0,我们只需要查右数组即可,同理target为6时,我们只需要查询左数组即可。
那怎么确认我们要查的是左数组还是右数组呢?希望大家好好理解下面的文字:
我们可以发现有个规律,左数组的第0个元素永远≥右数组的最后一个元素,这里面是4>2,
如果target≥左数组的第0个元素,那我们只需要查左数组即可
反之,target≤右数组的最后一个元素,那么只需要查右数组即可
因为左数组最小的元素就是第0个,比第0个元素还小的元素肯定在右数组,
右数组最大的元素就是最后一个,若target比它还大,那只能去左数组查找,代码如下:
代码
public int search(int[] nums, int target) {
if (nums == null || nums.length < 1) {
return -1;
}
// 指向数组中间的位置
int mid = 0;
// (左或右)数组的左指针,默认为第0个
int left = 0;
// (左或右)数组的右指针,默认为最后一个
int right = nums.length - 1;
// 这里指向的是左数组的左指针,默认为第0个
int leftLeft = 0;
// 这里指向的是左数组的右指针
int leftRight = 0;
// 这里指向的是右数组的左指针
int rightLeft = 0;
// 这里指向的是右数组的右指针,默认为最后一个
int rightRight = nums.length - 1;
// 先确认在哪里发生了划分,即先分左右数组
while (leftLeft <= rightRight) {
if (nums[rightRight] <= nums[leftLeft]) {
rightRight = rightRight - 1;
rightLeft = leftRight + 1;
} else {
leftLeft = 0;
leftRight = rightRight;
rightLeft = rightRight + 1;
rightRight = nums.length - 1;
break;
}
}
// 取具体的某个区间,确定左右指针的位置
if (target >= nums[0])
right = leftRight;
else if (target <= nums[nums.length - 1])
left = rightLeft;
// 此时left到right是一个排好序的数组,可能是左数组也可能是右数组
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
return mid;
}
}
return -1;
}
代码讲解
这里面定义了7个变量,分别为
首先我们需要将原数组分为左数组跟右数组,划分依据就是左数组的左指针指向的元素小于右数组的右指针,这是什么意思呢?若出现红字部分的情况,则左右数组已经可以确定了,看图说明吧:
一开始左数组左指针在0的位置,右数组右指针在最后的位置,通过比较大小,不断改变右数组右指针的位置,直到左数组的左指针指向的元素小于右数组的右指针
这时候左右数组已经可以确定了,将左右数组的其它指针指向左右数组的边界,如下图所示:
下面这段代码就是实现了上面的功能,即划分左右数组:
while (leftLeft <= rightRight) {
if (nums[rightRight] <= nums[leftLeft]) {
rightRight = rightRight - 1;
rightLeft = leftRight + 1;
} else {
leftLeft = 0;
leftRight = rightRight;
rightLeft = rightRight + 1;
rightRight = nums.length - 1;
break;
}
}
接着就是从左右数组中取出一个符合我们需求的数组,若target≥第0个数,则取左数组;若target≤最后一个数,则取右数组,如以下代码所示:
if (target >= nums[0])
right = leftRight;
else if (target <= nums[nums.length - 1])
left = rightLeft;
这时候left和right就指向我们想要的数组了,使用二分查找数组即可,如以下代码所示:
while (left <= right) {
mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
return mid;
}
}
由于思路2少了思路1中的遍历存储到map步骤,时间自然也快了不少,提交代码,舒服!
这个思路是我在讨论区看到的,跟思路2有点类似,只不过思路2是先将数组分为左右数组,再从其中一个数组中进行二分查找,而这个思路是直接二分查找,同时通过判断target的值与数组边界的关系并在其中一部分的数组(类似左右数组)进行查找,直接看代码吧:
代码
public int search(int[] nums, int target) {
int start = 0;
int end = nums.length - 1;
while (start <= end){
int mid = (start + end) / 2;
if (nums[mid] == target)
return mid;
if (nums[start] <= nums[mid]){
if (target < nums[mid] && target >= nums[start])
end = mid - 1;
else
start = mid + 1;
}
if (nums[mid] <= nums[end]){
if (target > nums[mid] && target <= nums[end])
start = mid + 1;
else
end = mid - 1;
}
}
}
return -1;
代码讲解
代码量比较少,但是思路都是差不多的,可以参考思路2的代码讲解
若感兴趣的同学可以自己动手在纸上模拟编译,很容易就懂了。
有感而发,我觉得影响学习效率的一个很重要的因素是气氛,如果能够找到适合自己学习的气氛,比如在安静的图书馆学习、又或者自己带着耳机学习、又或者到咖啡厅中学习,并一直沉浸于这种学习的氛围中,想必你的学习效率能有不错的提高。
当然我本人还达不到非常自律的境界,主要问题还是我对身边诱惑的抵制力不够,导致有时候浪费了很多时间,一个良好的学习气氛或许可以让自己专注于正确的事。
希望每个人都能找到适合自己学习的气氛,不断实现自己的梦想!