LeetCode 33. 搜索旋转排序数组(C++)

题目地址:力扣

这道题的意思就是会取数组中的某个位置,将其与之后的部分放到数组头部,而数组头部的部分放在这个位置之后。由于数组之前是保证有序的,因此整个数组除了那一个位置会出现非升序的情况,其他位置都是有序的。题目要求查找元素的时间复杂度是O(logn),再加上数组有序,因此可以确定这道题考察的就是二分查找的变形情况。

原先的二分查找,如果目标数小于mid,那么就往左找;目标数大于mid,就往右找。而这里的情况发生了改变,因为可能存在某个位置失序的可能。我们可以列出一个例子试试,如下所示比如在nums_1中我们需要寻找0,若按照之前的二分方法,我们可能就往左边一直找,发现没有最后返回-1。因此这道题目的关键就在于找到原来数组的头到底是位于mid的左边还是右边,来判断我们应该向哪边查找。

nums = [0, 1, 2, 3, 4, 5, 6, 7]
// 根据选取不同的旋转位置,我们可以得到以下八种情况
         left     mid         right
nums_0 = [0, 1, 2, 3, 4, 5, 6, 7]
nums_1 = [1, 2, 3, 4, 5, 6, 7, 0]  
nums_2 = [2, 3, 4, 5, 6, 7, 0, 1]
nums_3 = [3, 4, 5, 6, 7, 0, 1, 2]
nums_4 = [4, 5, 6, 7, 0, 1, 2, 3]
nums_5 = [5, 6, 7, 0, 1, 2, 3, 4]
nums_6 = [6, 7, 0, 1, 2, 3, 4, 5]
nums_7 = [7, 0, 1, 2, 3, 4, 5, 6]
         left     mid         right

我们可以发现,在所有可能的情况里,mid位置的元素和right位置的元素只有两种可能性,分别是mid位置元素 > right位置元素, mid位置元素 < right位置元素,由于题目保证了数组中没有相同元素,因此第一次二分查找不可能出现等于的情况。

我们可以观察到一个现象:

1、当mid位置元素 > right位置元素时(说明原数组的头在mid位置右边),mid元素及其左边是严格升序的,这意味着唯一可能出现失序的情况在mid右边

2、mid位置元素 < right位置元素时(说明原数组的头在mid位置或其左边),mid元素及其右边是严格升序的,这意味着唯一可能出现失序的情况在mid左边

因此,根据上面观察到的现象,我们就可以在二分查找的时候确定到底应该是往左边找还是往右边找了。

解法1:二分查找

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right = nums.size()-1;
                // 定义返回值初始为-1
        int res = -1;
        // 二分查找的边界条件是左边位置小于等于右边位置
        while (left <= right)
        {
            // mid记录当前中点
            int mid = (left+right)/2;
            // 若中点位置元素等于目标值,将中点位置赋给res,跳出循环
            if (nums[mid] == target)
            {
                res = mid;
                break;
            }
            // 若中点位置大于右边位置元素,其左边严格升序    
            if (nums[mid] > nums[right])
            {
                // 若目标值在左边范围内,则进左边查找,否则进右边
                if (target < nums[mid] && target >= nums[left])
                    right = mid-1;
                else
                    left = mid+1;
            } 
            // 若中点位置小于右边位置元素,其右边严格升序
            else {
                // 若目标值在右边范围内,则进右边查找,否则进左边
                if (target > nums[mid] && target <= nums[right])
                    left = mid+1;
                else
                    right = mid-1;
            }
        }
        return res;
    }
};

解法2:二分查找(极简)

这里是根据力扣上一位大佬的思路,我自己再做整理得出的思路总结。首先这道题之所以麻烦,之所以不能直接套用二分查找,归根结底其原因就是因为可能存在某点失序的情况,因此下一轮循环到底是向左还是向右就不好判断,所以我们只要明确了下一轮往左或往右的条件,就能写出简洁优雅的代码。根据题意,我们可以归类成以下两种情况。

这里我们考虑往左搜索的情况。左图是一种情况,也就是反转的位置在mid之后;右图是第二种情况,反转的位置在mid之前。其实还有第三种情况,就是反转的位置正好在mid,这种情况和第一种情况的处理方式其实是一样的。但是由于反转的位置我们不知道,所以讨论反转位置不好作为条件。而观察例子可以发现,我们其实可以用数组最左边的元素和中间元素的关系来判断当前子区间是否包含反转的部分。我们可以看下图:

  • 左图的情况是nums[mid] > nums[0],我们确定了mid,要往左边搜索的话,必须满足nums[0]\le target < nums[mid],如果不满足这个条件,就应该往mid右边搜索。
  • 右图的情况是nums[mid] < nums[0],我们确定了mid,要往左边搜索的话,要么满足target < nums[mid],要么满足target \ge nums[0]

综上所述,满足下列三个条件中任一就往左搜索:

1.nums[0] \le target \le nums[mid]

2.target \le nums[mid] < nums[0]

3.nums[mid] < nums[0] \le target

我们仔细观察可以发现这上面三个条件可以用下面三部分来体现,分别是target和mid的关系,0和target的关系,mid和0的关系。

1、target > nums[mid]

2、 nums[0] > target

3、nums[mid] < nums[0]

由此,因为上面三个条件是把所有的可能性都考虑到了,因此上面的三部分最多只会同时成立两个,或者同时不成立两个,不可能出现三个都成立或者三个都不成立的情况。于是我们可以通过异或运算符来捕捉两个同时为真和两个都不为真(只有一个为真)的情况。

对这三部分做异或,若三项均为真或一个为真,异或结果为真,此时向右搜索。这样我们就得到了是否应该向右的判断条件。其它情况就应该向左搜索,但是我们要注意到target=nums[mid]的情况,若出现这种情况,三部分均为假,此时应该向左,因此我们在向左的时候就不要把right设置为mid-1,而是直接设置为mid,防止这种情况被忽略。

代码如下:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        // 左指针和右指针
        int left = 0, right = nums.size() - 1;
        // 只要左指针小于右循环
        while (left < right) {
            // 设置中间指针
            int mid = (left + right) / 2;
            // 异或判断为真,说明只满足一个部分,向右走
            if ((nums[0] > target) ^ (nums[0] > nums[mid]) ^ (target > nums[mid]))
                left = mid + 1;
            // 为假,说明满足两个部分,向左走
            else
                right = mid;
        }
        // 返回的情况,需要判断两个指针是否相遇且结果等于目标值,都满足返回left,否则返回-1
        return left == right && nums[left] == target ? left : -1;
    }
};

Accepted

  • 195/195 cases passed (0 ms)
  • Your runtime beats 100 % of cpp submissions
  • Your memory usage beats 70.57 % of cpp submissions (10.7 MB)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值