LeetCode Notes_#33#153#154

LeetCode Notes_#33#153#154

Contents

#33 搜索旋转排序数组

题目

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。

你可以假设数组中不存在重复的元素。

你的算法时间复杂度必须是 O(log n) 级别。

示例 1:

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

示例 2:

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

官方解答

//这个解法就是二分查找的应用,首先找到翻转的位置,就可以缩小一半的范围,然后再进行基本的二分查找,妙啊
class Solution {
    int[] nums;
    int target;
    public int find_rotate_index(int left,int right){
        //如果nums[0]<nums[n-1],说明没有被翻转,rotate_index为0
        if(nums[left] < nums[right])
            return 0;
        //否则就寻找pivot的位置,注意这也是二分查找,反正就是查找一个数,刚好小于前一个数字,也就是第一次出现逆序的数
        while(left <= right){
            int pivot = (left + right)/2;
            //如果直接找到满足条件的pivot,直接返回这个值
            if(nums[pivot] > nums[pivot+1])
                return pivot+1;
            //否则
            else{
            //如果中间点小于最左侧点,说明在pivot位置之前就发生了逆序,调整右边界到pivot前一个位置
                if(nums[pivot] < nums[left])
                    right = pivot - 1;
                else    
                    left = pivot + 1;
            }
        }
        return 0;
    }
    //这里写了两个同名的search()函数,区别在于参数表不相同
    public int search(int left,int right){
        //不断地缩小查找的范围,直到左右边界相遇
        while(left <= right){
            //取中间点的方法就是直接求左右index的均值
            int pivot = (left + right)/2;
            if(nums[pivot] == target)
                return pivot;
            else{
                //如果目标值小于中间点的值,那么将查找范围变成中间点的左边
                if(target < nums[pivot])
                    right = pivot - 1;
                //否则,将查找范围变成中间点的右边
                else 
                    left = pivot + 1;
            }
        }
        //如果左右边界相遇后,还是没有返回值,说明找不到target,返回-1
        return -1;
    }

    public int search(int[] nums, int target) {
        this.nums = nums;
        this.target = target;
        int n = nums.length;
        //首先考虑数组长度为0或者为1的情况
        if(n == 0)
            return -1;
        if(n == 1)
            return this.nums[0] == target?0:-1;
        //left,right分别取为0,n-1,寻找最小的元素(翻转点)
        int rotate_index = find_rotate_index(0,n-1);
        //如果target就是最小的元素,直接返回这个位置的索引
        if(nums[rotate_index] == target)
            return rotate_index;
        //如果没有被翻转(翻转位置为0)
        if(rotate_index == 0)
            return search(0,n-1);
        //如果target比第一个数字小,说明一定在后半段,在后半段进行查找
        if(target < nums[0])
            return search(rotate_index,n-1);
        return search(0,rotate_index);
    }
}

题解Review

  1. 题目所谓的旋转,其实是找到一个位置,将这个位置之前的几个数平移到数组的最后。对于题目中的例子来说这个位置就是4所在的位置。这样做的效果就是,有且只有一个数组下标为pivot的地方,满足nums[pivot]>nums[pivot+1]
  2. 所以第一步要找到这个pivot的位置。最朴素的想法就是从头到尾遍历,去找到满足条件的pivot,但是这样的复杂度就是O(n),不满足要求,O(log n)的复杂度应该是需要进行二分查找的。
    • 首先考虑corner case:没有被翻转的情况,直接认为pivot = 0
    • 二分查找算是一种模板型的代码吧,整体是while循环,循环体内部首先找到中间位置,然后判断目标位置和中间位置的关系,缩小一半的范围,继续循环。
  3. 找到pivot的位置后,比较target和nums[0],就可以知道target位于左半部分还是右半部分,分别在左半部分和右半部分进行二分搜索,即可找到target的索引,且整体的时间复杂度是O(log n)。
    • 先考虑corner case:rotate_index=0或者rotate_index=1
    • 二分查找

官方解答复现

class Solution {
    int[] nums;
    int target;
    public int find_rotate_index(int left,int right){
        //没有翻转的情况
        if(nums[left] < nums[right])
            return 0;
        while(left <= right){
            int pivot = (left + right)/2;
            if(nums[pivot] > nums[pivot+1]){
                //将最小的数字作为rotate_index
                return pivot+1;
            }else{
                if(nums[pivot] >=nums[left]){
                    left = pivot + 1;
                }else{
                    right = pivot - 1;
                }
//                if(nums[pivot] < nums[left])
//                    right = pivot - 1;
//                else
//                    left = pivot + 1;
//注意这里两个条件是不可以互换位置的,因为在大于和小于之间还有一种等于的情况。
            }
        }
        //循环结束还没有找到,也返回0
        return 0;
    }

    public int search(int left,int right){
        //到了调用这一步的时候,left和right之间的数组必然是顺序的,那么直接普通的二分查找就可以了
        while(left <= right){
            int pivot = (left + right)/2;
            if(target == nums[pivot])
                return pivot;
            if(target > nums[pivot]){
                left = pivot + 1;
            }else
                right = pivot - 1;
        }
        return -1;
    }

    public int search(int[] nums, int target) {
        //为什么要定义两个类变量?因为定义完之后就可以直接被前面的两个函数访问到了
        this.nums = nums;
        this.target = target;
        int len = nums.length;
        if(len == 0)
            return -1;
        if(len == 1)
            return target == nums[0]?0:-1;
        int rotate_index = find_rotate_index(0, len-1);
        if(nums[rotate_index] == target)
            return rotate_index;
        if(rotate_index == 0)
            return search(0, len-1);
        if(target >= nums[0])
            return search(0, rotate_index);
        return search(rotate_index, len-1);
    }
}

更加简短的解法

class Solution {
    public int search(int[] nums, int target) {
        int len = nums.length;
        int start = 0;
        int end = len - 1;

        if(nums == null || len == 0) return -1;

        while(start + 1 < end){
            int mid = start + (end - start) / 2;
            if (nums[mid] == target) return mid;
            else{
                //因为题目强调了没有重复元素,所以不需要考虑等号
                //其实这里也可以和end比较
                if (nums[mid] > nums[start]){
                    if (target >= nums[start] && target < nums[mid]) end = mid;
                    else start = mid; 
                //Q:为什么这里不把mid排除掉,写成start = mid + 1
                //1.start + 1有可能越界
                //2.如果遇到start和right相邻的情况,nums[mid] == nums[start],边界应该向右,start = mid + 1,但是脑子乱了就容易想错,所以不如留着mid,防止脑抽把边界左移了
                //3.综上,不把mid排除,可能会多出几轮循环,但是好处是防止出错,不需要去考虑边界情况,直接这么写就行。
                }else if(nums[mid] < nums[start]){
                    if (target > nums[mid] && target <= nums[end]) start = mid;
                    else end = mid;
                }
            }
        }

        if(nums[start] == target) return start;
        if(nums[end] == target) return end;
        return -1;
    }
}

参考了basketwangCoding这位大佬的视频,感觉讲的很清晰,感觉逻辑捋清楚以后,实现代码并没有那么困难,这里边有几个Tips,我一并在LeetCode Notes_#81 搜索旋转排序数组II中整理。

学习方法

我感觉自己去想这些套路确实效率很低,自己想很久想不出来,无法通过,一是费时间,二是打击信心,时间又并不是很充分,是一种难以持续的方式。不如先用简单点的流程,看解答抄代码->review思路->自己尝试写一遍。这样速度相对快些,可以一天看两三道类似的题目,反复学习揣摩一个基本算法和它的变形(比如这题就是二分搜索算法的变形),反而会效率比较高。

#153 寻找旋转排序数组中的最小值

题目

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

请找出其中最小的元素。

你可以假设数组中不存在重复元素。

示例 1:

输入: [3,4,5,1,2]
输出: 1

示例 2:

输入: [4,5,6,7,0,1,2]
输出: 0

解答

这一题就是33题里边的find_rotate_index()这个函数...就不再分析了,直接贴解答。

class Solution {
    public int findMin(int[] nums) {
        int len = nums.length;
        int start = 0;
        int end = len - 1;
        if (len == 0) return -1;//?空数组要怎么处理
        if (len == 1) return nums[0];
        if (nums[end] > nums[start]) return nums[start];
        while (start + 1 < end){
            int mid = start + (end - start) / 2;
            if (nums [mid] < nums[mid - 1]) return nums[mid];
            else {
                if (nums[start] < nums[mid]) start = mid;
                else end = mid;
            }
        }
        //如果最后start和end相邻,那么直接返回小的那个
        return nums[start] < nums[end]?nums[start]:nums[end];
        
    }
}

#154 寻找旋转排序数组中的最小值 II

题目

假设按照升序排序的数组在预先未知的某个点上进行了旋转。

( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。

请找出其中最小的元素。

注意数组中可能存在重复的元素。

示例 1:

输入: [1,3,5]
输出: 1

示例 2:

输入: [2,2,2,0,1]
输出: 0

解答

这个跟#153类似,同样只是多了一个可以重复的条件而已,遇到nums[mid] == nums[start],直接start++即可,不过还是犯了错误,做过的题一定要避免再次做错。

class Solution {
    public int findMin(int[] nums) {
        int start = 0;
        int end = nums.length - 1;
        if (nums.length == 1) return nums[0];
        if (nums.length == 0) return -1;
        while(start + 1 < end){
            //corner case:输入就是有序数组
            //ERROR:这个判断不能写到循环外,每次循环都要判断一下,因为可能start++之后就变成有序数组了
            if(nums[end] > nums[start]) return nums[start];
            int mid = start + (end - start) / 2;
            if (nums[mid] < nums[mid-1]) return nums[mid];
            if (nums[mid] == nums[start]) start++;
            else{
                //ERROR:这里缩紧边界的方向写错了一次,每次写完先自己在纸上或者脑子里过一遍流程比较保险
                if(nums[mid] > nums[start]) start = mid;
                else end = mid;
            }
        }
        //有可能left,right都是最小,所以考虑等号
        return nums[start] <= nums[end]?nums[start]:nums[end];
    }
}

旋转数组搜索类题目总结

流程

  1. 初始化start,end指针。
  2. 处理corner case
    • 循环外:
      • 输入空数组:返回一个错误值例如-1
      • 输入数组长度为1:直接返回第一个元素
    • 循环内:
      • 输入数组有序:直接返回第一个元素
  3. 进入循环
    • 注意循环条件是start + 1 < end,并且在循环结束后单独判断startend
    • 求中间位置索引mid = start + (end - start)/2
    • 判断nums[mid]是否满足条件,满足就返回,否则缩紧边界
      • 根据情况把start或者end移动到mid
  4. 循环结束后,单独判断left,right

体会

  • hard题一般就是medium的变形或者组合,所以要从相关的medium开始做,而不要一来就做hard
  • 集中做同类型题目,刷起来也快,留下的印象也很深刻,直接做每道题下面推荐的相似题目就行

参考

  1. [Leetcode 33]Search In Rotated Sorted Array - YouTube
  2. 搜索旋转排序数组
posted @ 2019-12-05 21:58  Howfar's  阅读(295)  评论(0编辑  收藏  举报