zoukankan      html  css  js  c++  java
  • 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. 搜索旋转排序数组
  • 相关阅读:
    HTML+CSS学习笔记(九)
    HTML+CSS学习笔记(八)
    HTML+CSS学习笔记(七)
    HTML+CSS学习笔记(六)
    HTML+CSS学习笔记(五)
    Numpy学习笔记(五)
    图片和文字放在一行对齐的方法
    CSS控制文字,超出部分显示省略号
    Stylus基本使用
    什么是HTML语义化标签?常见HTML语义化标签大全
  • 原文地址:https://www.cnblogs.com/Howfars/p/11992273.html
Copyright © 2011-2022 走看看