本文始发于个人公众号:TechFlow,原创不易,求个关注

链接

Search in Rotated Sorted Array

难度

Medium

描述

给定一个升序排列的数组,它被分成两部分之后交换了顺序。要求给定一个元素,返回这个元素的在数组当中的下标,如果不存在返回-1. 时间复杂度必须是\(O(logn)\)

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 ).

样例 1:

Input: nums = [4,5,6,7,0,1,2], target = 0
Output: 4

样例 2:

Input: nums = [4,5,6,7,0,1,2], target = 3
Output: -1

题解

这题非常明显,虽然看似限制了时间复杂度在提升难度,但其实是在给大家提示。一个数组查找元素,时间复杂度还是\(log(n)\),这显然是在暗示大家什么。我想大家作为一个成熟的程序员,异性的暗示未必读的懂,但来自出题人的暗示一定能get到【狗头】。

显然,这题需要使用二分法,但是如何使用二分呢,这才是本题的重点。

两次二分

第一种方法非常简单粗暴,因为我们已经知道要二分查找了,然后我们还知道数组分成了两截之后又交换了顺序。所以数组的情况应该是这样的:

对不起,我画得有些抽象,但是精髓传达到了。什么意思呢?就是原本升序的数组分成了两截之后交换顺序,那么现在的数组应该是两端递增的序列拼接构成的。既然是两端序列,那么中间显然有一个断点。我们只要找到这个断点的位置,就容易了,这个问题就变成了在两个有序的数组里查找元素了,那就没有难度了。

但是这个断点怎么找呢?

显然不是打开QQ音乐,而是要使用二分法。因为前面我们已经说了,我们整体的复杂度是log级的,显然我们不可能用遍历来寻找断点。留给我们的就只有一条路,就是二分。

设计这里的二分需要对二分法有一定深入的了解,因为在这个问题当中其实是不满足二分法的使用条件的,因为数组被分成了两个部分,已经不是整体有序了。但是虽然不是整体有序,但仍然是有办法的。

我们先来根据上面的图来分析一下,很容易发现,断点的位置就是全局最小值。我们可以找到左侧部分的最大值,或者是右侧部分的最小值,就是断点的位置。

我们再来分析一下二分时候可能出现的情况,一开始的时候l在最左侧,r在最右侧,m则是两侧都有可能。如果m在左侧部分,那么m位置的值一定大于l,否则一定小于l。所以我们通过比较m和l位置元素的大小关系可以判断m在左侧还是右侧。

如果说我们最终的搜索目标是寻找左侧部分的最大值,那么当m处的值大于l时,则舍弃左侧部分,因为左侧部分已经不可能是答案了。同理,如果m处的值小于l,那么应该舍弃m右侧,因为m右侧都在右边部分,当中是不可能有左侧部分的最大值的,通过这种方法我们也可以使用二分查找,只不过条件和我们常用的二分不太一样。

def binary_search(arr):
l, r = 0, len(arr)
while r - l > 1:
m = (l + r) >> 1
if arr[m] > arr[l]:
l = m
else:
r = m
return l

找到断点之后就容易了,就是简单的无脑二分了。我们来看代码:

class Solution:
def search(self, nums: List[int], target: int) -> int:
# 找到断点
l, r = 0, len(nums)
if r == 0:
return -1 while r - l > 1:
m = (l + r) >> 1
if nums[m] > nums[l]:
l = m
else:
r = m # 根据target和第0个元素比较大小判断target在左还是右
if target < nums[0]:
l, r = r, len(nums)
else:
l, r = 0, l+1 if l == len(nums):
return -1 # 二分查找
while r - l > 1:
m = (l + r) >> 1
if nums[m] <= target:
l = m
else:
r = m
return l if nums[l] == target else -1

一次二分

虽然我们三下五除二地解决了问题,但是我仍然不够满意,总觉得这种做法不够巧妙,应该还能继续优化。比如说我们能不能不先找断点的位置直接二分呢?

仔细想的话是可以的,尤其是我们已经有了上面这种做法的情况下。

在我们上面查找断点的时候,已经有了一个重要的关键信息,就是我们可以通过m和l位置比大小来确定断点在什么位置。既然我们可以确定断点的位置,那么我们同样可以确定target的位置。

说起来很抽象,我们来看图:

这是一种情况,即m的位置在断点右侧,也就是右侧。那么我们通过判断target和l处的大小关系可以判断target可能在哪个部分。

如果target > nums[l],那么显然target在左侧,所以我们应该让r=m,即抛弃右部分。

如果target < nums[l],这个时候还不能确定范围。因为m左侧可能还有比m小的元素,所以我们还需要判断一下target和nums[m]的关系,来判断抛弃哪个部分。如果target < nums[m],那么抛弃右部分,否则抛弃左部分。

下面来看第二种情况:

同样我们判断target和nums[l]的关系,如果target > nums[l],那么我们需要继续判断它和m的关系,来决定抛弃哪个部分。如果target < nums[l],那么说明我们需要抛弃m左侧的部分。

情况虽然有点复杂,但是我们画个图还是很容易理清楚的。一旦理清楚了,我们就可以一次二分搞定全局。

来看代码:

class Solution:
def search(self, nums: List[int], target: int) -> int:
if len(nums) == 0:
return -1 l, r = 0, len(nums) while r - l > 1:
m = (l + r) >> 1
# 如果m出现做左侧部分
if nums[m] >= nums[l]:
if target >= nums[l]:
if target < nums[m]:
r = m
else:
l = m
# 如果target小于nums[l],需要让l=m+1,因为m这个点也是非法点
else:
l = m+1
else:
if target >= nums[l]:
r = m
else:
if target >= nums[m]:
l = m
else:
r = m
return l if l < len(nums) and nums[l] == target else -1

到这里整个题目就分析完了,但是比较搞笑的是虽然我们第二种算法看起来牛哄哄,减少了一次二分,但是提交上去之后的结果反而耗时要长一些。而且相信大家也感觉到了,这种方法实现起来要复杂得多,边界条件很容易写错。所以这点告诉我们一个道理,厉害的方法不一定效果好。

这道题虽然不难,但是挺有意思,打破了我们一直以来对于二分法的认识,不知道会不会给你们带来脑洞大开的感觉。

今天的文章就是这些,如果觉得有所收获,请顺手点个关注吧,你们的举手之劳对我来说很重要。

最新文章

  1. Sql Server 中锁的概念
  2. &lt;meta&gt;元素
  3. php json_encode中文unicode问题
  4. scrollview做定时来回滚动时,总出现错位的情况。
  5. android 读中文文本文件
  6. Apache Kafka源码分析 - KafkaApis
  7. 动态加载JS(css)文件
  8. js获取光标位置例子
  9. vs2010中iostream.h出错
  10. MySQL中Procedure事务编写基础笔记
  11. VS2013中Python学习环境搭建
  12. Linux kernel中网络设备的管理
  13. Hololens 开发环境配置
  14. 接口测试——httpclient介绍与请求方式详解
  15. jquery开关按钮效果
  16. puppeteer(五)chrome启动参数列表API
  17. 1.7Oob对象的创建局部变量
  18. Ubuntu 12.10 Tty (字符终端) 显示中文,和字体大小设置
  19. MySQL:数据表基本操作
  20. union与union all的用法给区别

热门文章

  1. linux系统用户管理(二)
  2. 读写分离(AMOEBA)
  3. 点分治练习——BZOJ 2152
  4. 转载【docker】CMD ENTRYPOINT 的使用方法
  5. 每个月执行一次任务,保存90天的mongo日志数据
  6. 使用zxing生成和解析二维码
  7. pressure to compete|listen to sb do|have sb do|felt like|shouldn&#39;t have done|spring up|in honour of|not more than|much as|
  8. CF-1110C-Meaningless Operations
  9. jstl之核心标签
  10. Java JDBC调用inout类型参数的存储过程