每日一题之 搜索旋转排序数组系列

题目1:
整数数组 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 。

示例 1:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

题目2:
已知存在一个按非降序排列的整数数组 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,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。

给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。

示例 1:
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array-ii
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题思路:
题目1:
方法1:
一次遍历

方法2:
区间二分
有两种具体的实现:
一种是按照分好的两个区间直接开始遍历;
第二种是第一次二分找到分割点,第二次二分根据该分割点进行遍历。

题目2:
方法:
同样是两种。
第一种是因为存在相同元素,当无法判断前后区间哪个有序时,需要继续偏移寻找有序的区间;
第二种是恢复二段性后,先找旋转点,后找目标值

技术总结:
最近被二分虐的死去活来,通过广泛阅读各种大佬的总结和模板,自己整理一下心得:

  1. 理解二分的本质。

这也是我最近学习宫水三叶大佬的文章悟出的。
二分的本质不是单调,而是两段性。 只要某一段满足某个性质,而另一段不满足,就可以使用二分来求解。
单调只是一个性质,我们从有序数组中找某个数只是二分的应用。

  1. 二分难在细节。

不同的问题写法都有区别,我们最重要的是根据题目能够写出模板,而不是背出模板。
在数组中,我们一般会遇到两种情况,这两种情况本质上是没有区别的:

  • 找到某个固定元素
  • 找到满足条件的 第一个 / 最后一个元素
  1. 二分模板:

网上有两种不同的模板,一种是闭区间,另一种是左开右闭区间。 其中,左开右闭区间的写法更加灵活,更适合情况较为复杂的题目。
「二分」模板其实有两套,主要是根据 check(mid) 函数为 true 时,需要调整的是 l 指针还是 r 指针来判断。

• 当 check(mid) == true 调整的是 l 时:计算 mid 的方式应该为 mid = l + r + 1 >> 1:

	/* check(mid)就是根据题意给出表达式:
	例如当求x ≥ target 的第一个元素时,使用模板2,check(mid)为 nums[i] >= target
	*/
	
	//模板1:求最后一个满足该条件的值
	//没有越界风险是考虑int
	long l = 0, r = n - 1;
	while (l < r) {
		//向下取整, +1 操作主要是为了避免发生「死循环」
	    long mid = l + r + 1 >> 1;
	    //推荐写法 int mid = l +( r - l) / 2 + 1;	    
	    if (check(mid)) { 
			//偏移l, 求最后一个满足该条件的值
	        l = mid;
	    } else {
	        r = mid - 1;
	    }
	}

• 当 check(mid) == true 调整的是 r 时:计算 mid 的方式应该为 mid = l + r >> 1:

	//模板2:求第一个满足该条件的值
	long l = 0, r = n - 1;
	//左开右闭区间
	while (l < r) {
	    long mid = l + r >> 1;
		//推荐写法 int mid = l +( r - l) / 2;	
	    if (check(mid)) {
		//偏移r, 求第一个满足该条件的值
	        r = mid; // 
	    } else {
	        l = mid + 1;
	    }
}

代码

//题目1
//直接按照两个不同的区间进行查找
class Solution {
    public int search(int[] nums, int target) {

        int len= nums.length;

        //special
        if (len == 0 || nums == null) {
            return -1;
        }

        //区间二分
        int l = 0;
        int r= len - 1;
        int mid = 0;

        while (l <= r) {

            mid = ( r - l) / 2 + l;

            //按照区间二分
            if (nums[mid] == target) {
                return mid;
            }

            //前半区间
            if (nums[0] <= nums[mid]) { 
                //前半段
                if (target >= nums[0] && target <= nums[mid]) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            } else {
                //后半段
                if (target <= nums[len - 1] && target >= nums[mid]) {
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
        }

        return -1;
    }
}

//第一次二分 找到分割点,第二次二分进行遍历
class Solution {
    public int search(int[] nums, int target) {

        int n = nums.length;

        //special
        if (n == 0 || nums == null) {
            return -1;
        }

        if (n == 1 && nums[0] == target) {
            return 0;
        }

        int l = 0;
        int r = n - 1;
        //normal
        //第一次二分 找到idx
        //找到分割点 因为分割点在大小交界的位置,所以如果大于首位置数值,需要继续向后偏移;小于则向前偏移
        while (l < r) {
            int mid = l + r + 1 >> 1; //偏移一个位置开始计算
            if (nums[mid] >= nums[0]) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        
        //第二次二分 找到target
        int idx = 0;
        if (target >= nums[0]) {
            idx = find (nums, 0, r, target);
        } else {
            idx = find (nums, l + 1, n - 1, target);
        }
        return idx;
    }

    public int find(int[] nums, int l, int r, int t) {
        int mid = 0;
        while (l < r) {
            mid = l + r >> 1;
            if (nums[mid] >= t) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }

        return nums[r] == t? r: -1;
    }
}
//题目二
//方法1:因为存在相同元素,当无法判断前后区间哪个有序时,需要继续偏移寻找有序的区间
class Solution {
    public boolean search(int[] nums, int target) {

        int len= nums.length;

        //special
        if (len == 0 || nums == null) {
            return false;
        }

        //区间二分
        int l = 0;
        int r= len - 1;
        int mid = 0;

        while (l <= r) {

            mid = ( r - l) / 2 + l;

            //按照区间二分
            if (nums[mid] == target) {
                return true;
            }

            //只偏移前面的
            if (nums[l] == nums[mid]) {
                l++;
                continue;
            }

            //前半区间
            if (nums[l] < nums[mid]) { 
                //前半段
                if (target >= nums[l] && target <= nums[mid]) {
                    r = mid - 1;
                } else {
                    l = mid + 1;
                }
            } else {
                //后半段
                if (target <= nums[len - 1] && target >= nums[mid]) {
                    l = mid + 1;
                } else {
                    r = mid - 1;
                }
            }
        }

        return false;
    }
}

//方法2
//恢复二段性后,先找旋转点,后找目标值
class Solution {
    public boolean search(int[] nums, int t) {
        int n = nums.length;
        int l = 0, r = n - 1;
        // 恢复二段性
        while (l < r && nums[0] == nums[r]) r--;

        // 第一次二分,找旋转点
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (nums[mid] >= nums[0]) {
                l = mid;
            } else {
                r = mid - 1;
            }
        }
        
        int idx = n;
        if (nums[r] >= nums[0] && r + 1 < n) idx = r + 1;

        // 第二次二分,找目标值
        int ans = find(nums, 0, idx - 1, t);
        if (ans != -1) return true;
        ans = find(nums, idx, n - 1, t);
        return ans != -1;
    }
    int find(int[] nums, int l, int r, int t) {
        while (l < r) {
            int mid = l + r >> 1;
            if (nums[mid] >= t) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return nums[r] == t ? r : -1;
    }
}

参考文献:
【宫水三叶】详解为何元素相同会导致 O(n),一起看清二分的本质

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值