• LeetCode 33,在不满足二分的数组内使用二分的方法


    本文始发于个人公众号: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
    

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

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

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

  • 相关阅读:
    Nginx 缓存解决方案
    Chrome去水印、自由复制的解决方法
    简单介绍正向代理和反向代理
    Lambda表达式
    项目集成hystrix-dashboard
    hystrix 添加turbine
    GsonUtil 工具类
    idea 去除import * 变成具体引入文件
    eureka 创建服务消费者
    eureka 创建注册服务提供方
  • 原文地址:https://www.cnblogs.com/techflow/p/12441002.html
Copyright © 2020-2023  润新知