【LeetCode】详解搜索旋转排序数组33. Search in Rotated Sorted Array Suppose an array sorted in ascending order is


前言

最近忙着期末考试,刚好考完了两科,今天抽出时间继续刷题!


正文

原题:

链接:搜索旋转排序数组

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的位置。

思路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%的人,那我们从减少时间复杂度的角度切入。
在这里插入图片描述

思路2:

由于原数组是一个旋转排序数组,以某个元素为分界点,将数组分为左数组跟右数组,如下图所示:
在这里插入图片描述
为什么要分为两个数组出来,这是为了减少搜索无关项浪费的时间,假设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个变量,分别为

  • mid:指向数组的中间位置
  • left:数组的左指针,这里的数组是指左数组或者右数组
  • right:数组的右指针,这里的数组是指左数组或者右数组
  • leftLeft:左数组的左指针,默认为0,即指向第0个元素
  • leftRight:左数组的右指针
  • rightLeft:右数组的左指针
  • rightRight:右数组的右指针

首先我们需要将原数组分为左数组跟右数组,划分依据就是左数组的左指针指向的元素小于右数组的右指针,这是什么意思呢?若出现红字部分的情况,则左右数组已经可以确定了,看图说明吧:
在这里插入图片描述
一开始左数组左指针在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步骤,时间自然也快了不少,提交代码,舒服!
在这里插入图片描述

思路3:

这个思路是我在讨论区看到的,跟思路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的代码讲解
若感兴趣的同学可以自己动手在纸上模拟编译,很容易就懂了。


总结

有感而发,我觉得影响学习效率的一个很重要的因素是气氛,如果能够找到适合自己学习的气氛,比如在安静的图书馆学习、又或者自己带着耳机学习、又或者到咖啡厅中学习,并一直沉浸于这种学习的氛围中,想必你的学习效率能有不错的提高。
当然我本人还达不到非常自律的境界,主要问题还是我对身边诱惑的抵制力不够,导致有时候浪费了很多时间,一个良好的学习气氛或许可以让自己专注于正确的事。
希望每个人都能找到适合自己学习的气氛,不断实现自己的梦想!

发布了57 篇原创文章 · 获赞 282 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/weixin_41463193/article/details/92802738
今日推荐