【leetcode】33.搜索旋转排序数组
这道题是快手面经中提到的,在此记录:https://www.nowcoder.com/feed/main/detail/b17a674ca2ba4327b3105ffacd1f60b4
题目
整数数组 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 | 示例 1: |
提示:
1 | 1 <= nums.length <= 5000 |
思路
本题要求用O(log n)
的算法解决这个问题,自然没有办法通过暴力遍历来解决了(不然就太简单了)。
看到log就要想到二分法,同时要想到二分法的前提是遍历的序列已经有序,但这道题并非是一个严格有序的数组,它会进行一定的旋转。
仔细观察旋转的方式,当选用下标k的时候,会将k和k以后的元素全部移动到数组开头,之前的元素移动到数组末尾。
可以拿纸笔多试试几次,就能得出来一个结论,不管数组的长度是奇数还是偶数,也不管k选择那一个地方,被旋转的部分(k和k以后)和没有被旋转的部分(k以前)的序列长度肯定相等或有一个更长!
这里假设二分法计算mid的公式是left+(right-left)/2
,其中left初始值为0,right初始值是nums.size()-1
,二者都是闭区间。因为上文提到的特性(被旋转的部分和没有被旋转的部分长度相等或有一个更长),使用这个公式计算出来的第一个mid值肯定是在某个有序序列之中!
这里举几个具体的例子:
- 数组长度7(奇数),k=3,即在下标3处旋转,此时k之前还有下标
0,1,2
三个数字,k和k以后还有3,4,5,6
四个数字;k=4时同理,k之前有四个数字,k以后有三个数字。- 此时计算
mid=0+(6-0)/2=3
,不管k选择什么,下标3肯定是在某个有序序列之中的,这个有序序列要么在下标3之前,要么在下标3之后。
- 此时计算
- 数组长度8(偶数),k=4,此时k之前有
0,1,2,3
四个数字,k和k之后有下标4,5,6,7
四个数字,被旋转和没有被旋转的部分一样长;k=5时没有被旋转的部分更长,k=3时被旋转的部分更长。- 此时计算
mid=0+(7-0)/2=3
,不管k选择哪一个,下标3还是肯定在某个有序序列之中!
- 此时计算
现在我们能确认这个特性了,也就能用二分法解决这个问题了!思路就是通过判断mid左侧还是右侧有序,来确认left/right边界,让下一轮的mid计算在有序序列中进行。
- 如果nums[left]小于等于nums[mid],则说明左侧有序(这里的等于判断是避免mid=left的情况)
- 其他情况都可以归于右侧有序
在循环体内,每一次都需要判断mid左侧还是右侧有序的,确认有序序列的位置之后,找target的操作就是在有序数组中通过二分法查找的思想了,时间复杂度是O(log(N))
。
- 为什么每一次都需要判断?
因为target可能不在第一次找到的有序序列中,如下示例,第一次计算出来的mid=3,虽然能确认mid的左侧是有序的,但target=1的时候,我们需要找的目标数是在mid的右侧。此时右侧的序列是[7,0,1,2]
,这还不是一个有序序列,我们还是需要通过计算mid判断左侧还是右侧有序,来再次确认第二个有序的子序列位置。
1 | [3,4,5,6,7,0,1,2] |
代码
思路理清楚了,代码就不难写了
1 | class Solution { |