leetcode刷题笔记-数组-二分法

一、二分法的基本题型

  • 题目

​ 给定一个有序无重复数组nums,给一目标数target,要求查找nums数组中是否存在有target,若存在则返回target所在的下标位置,若不存在则返回-1。 704. 二分查找

  • 二分法的的基本条件

    • 有序
    • 无重复

    当题目中出现需要在一组有序且无重复数据中找到一个数时,优先考虑使用二分法

  • 解决方法

    二分法思路比较简单,难点在于确定区间,不同的区间确定方法,二分法的代码也不想同,下面分别给出两种不同的二分法写法。

    • 方法一:左闭右闭区间

      • 若定义每次的查询区间为[left,right],则外层循环的条件应该为while(left<=right),因为left,right都能取到并进行查询
      • nums[mid] > target时,说明nums[mid]以及mid位置右边的所有数的比taget大,下一次查询的右边界应该定义在mid-1处,因为mid处已经查询比较过了
      • 同理,当nums[mid] < target时,说明nums[mid]以及mid位置左边的所有数的比taget小,下一次查询的左边界应该定义在mid+1处,因为mid处已经查询比较过了
      func binarySearch(nums []int,target int)int{
          left,right := 0 , len(nums)-1 // 由于是左闭右闭区间的,右边是需要进行比较的,因此右边界是len(nums)-1
          mid := 0
          for left <= right{
              mid = (left + right) / 2
              if nums[mid] == target{
                  return mid
              }else if nums[mid] > target{
                  right = mid - 1
              }else if nums[mid] < target{
                  left = mid + 1
              }
          }
          return -1 // 找不到则返回-1
      }
      
    • 方法二:左闭右开区间

    • 若定义每次的查询区间为[left,right),则外层循环的条件应该为while(left,每次查询只会涉及到左边界,而右边界是没有查询到的

    • nums[mid] > target时,说明nums[mid]以及mid位置右边的所有数的比taget大,下一次查询的右边界应该定义在mid处,因为mid处已经查询比较过了,将其设置为右边界后下一次就不会再去比较mid这一位置的值

    • 同理,当nums[mid] < target时,说明nums[mid]以及mid位置左边的所有数的比taget小,下一次查询的左边界应该定义在mid+1处,因为mid处已经查询比较过了

      func binarySearch(nums []int,target int)int{
          left,right := 0 , len(nums) // 左闭右开,右边界取不到,因此右边界不能是len(nums)-1,否则会漏掉最后一个元素
          mid := 0
          for left < right{
              mid = (left + right) / 2          
              if nums[mid] == target{
                  return mid
              }else if nums[mid] > target{
                  right = mid
              }else if nums[mid] < target{
                  left = mid + 1
              }
          }
          return -1 // 找不到则返回-1
      }
      

二、二分法的进阶问题一:查找合适的插入位置

  • 问题:给定有序无重复元素数组nums和目标数target,要求查询target在nums中的下标位置,如果nums中不存在target,则返回target在nums中应该插入的位置。35. 搜索插入位置

  • 分析:

    • 如果target存在nums中,那么有target == num[pos]
    • 如果target不存在nums中,假设target应该在的位置为pos,那么nums[pos-1]必然成立
    • 联合上述两个条件,即nums[pos-1],我们只要找到第一个大于或等于target的数的下标,即为最终结果
  • 解决方案

    func binarySearchIndex(nums []int,target int)int{
        left,right := 0,len(nums)-1
        mid := 0
        ans := len(nums)  //存放最终结果
        for left <= right{
            mid = (left + right) / 2
            if nums[mid] >= target{ // 只要找到第一个大于等于target的数的下标,就可以得到ans的值
                ans = mid 
                right = mid - 1
            }else if nums[mid] < target{ 
                left = mid + 1
            }
        }
        return ans
    }
    

(难点)三、二分法的进阶问题二:有重复元素时查找某个元素的索引范围

  • 问题:给定一个有序有重复元素的数组nums和目标值target,要求给出target在nums数组中的下标索引范围,若不存在则返回[-1,-1]。34. 在排序数组中查找元素的第一个和最后一个位置

  • 问题分析:

    • 难点:数组nums中有重复元素,使用寻常的二分查找法找到的target索引不唯一
    • 在上一个问题中,我们找到了一种方法,可以找到一个target值在nums中应该存在的位置,我们可以利用这个方法
    • 首先,我们应用上一题的方法寻找target在nums中应该存在的位置,我们可以分析得到
      • 如果target不存在,返回的是target应该插入的位置pos,但此时的nums[pos]!=target
      • 如果target存在一个或者多个,由于上一题的思路是找到第一个大于或等于target的位置,所以其返回的肯定是最左边的target的位置,即target的左边界。
    • 因此,通过上一题的方法我们就可以先判断target是否存在于nums中,如果不存在,则返回[-1,-1],如果存在,其返回值即为答案的左边界
    • 右边界如何寻找呢,我们同样可以使用上一题的方法!我们只要寻找第一个比target值大的位置即可。找到该位置后,将其减一,即为target索引范围的右边界。
  • 解决方案

    // binarySearchIndex为上一题的实现
    func searchRange(nums []int, target int) []int {
        left := binarySearchIndex(nums,target)
        if left == len(nums) || nums[left] != target{
            return []int{-1,-1}
        }
        return []int{left,binarySearchIndex(nums,target+1) - 1}
    }
    

四、二分法的其它题型:求一个数的平方根问题

  • 问题:给定一个整数target,求其算术平方根,如果结果是小数,要求返回其整数部分69. x 的平方根

  • 问题分析:

    • 假设返回值是ans,那么一定有ans * ans <= target成立
    • 我们从0开始,一直到target,取整数,可以发现是一个升序无重复元素的序列,符合是用二分法的条件
    • 综上所述,我们可以使用二分法查找第一个ans * ans <= target的值,就是我们想要的结果
  • 解决方案

    func mySqrt(target int)int{
        left,right := 0,target
        mid := 0
        ans := 0
        squre := mid * mid
        for left <= right{
            mid = left+(right-left) / 2
            squre = mid * mid
            if squre <= target{
                ans = mid
                left = mid + 1
            }else{
                right = mid - 1
            }
        }
        return ans
    }
    
  • 相似问题:367. 有效的完全平方数

五、旋转数组

5.1 无重复元素的旋转数组

  • 问题:33. 搜索旋转排序数组 - 力扣(Leetcode)

  • 分析

    • 首先要注意,本题是升序无重复元素进行旋转
    • 其次,我们要明确,即使该数组经过了旋转,但是,如果我们沿着某一个位置将数组一份为2,一定存在着一个有序的区间。
      • 如果我们的target在这个有序区间内,我们就继续使用二分查找法查找这个target
      • 如果我们的target不在这个有序区间内,我们就到无序的区间去,继续寻找一个更小的有序区间进行寻找,直到我们的区间只剩一个数为止
      • 理解,例如现在有一个旋转后的数组[8 9 1 2 5 6 7],我们在数组的中间切上一刀,得到了一个有序的区间[ 2 5 6 7]和一个无序区间[8 9 1],如果我们的target存在于有序区间,我们就可以在有序区间使用二分法,如果在无序区间,我们就继续将无序区间一分为2,重复步骤直到找到target或者切到只剩一个数为止
    • 那么,我们找哪个位置切割数组比较好呢,由于我们寻找target时使用的是二分法,因此,我们同样可以以二分法去切割数组,此时,我们可以通过以下方法判断区间是否有序
      • 如果nums[mid]<=nums[right],那么说明从mid到right这个区间内都是有序的,这里为什么有个等号,元素不是不重复的吗?这是因为随着切割持续进行,有可能会切到最后只剩下一个元素的区间,此时,mid和right指向同一个元素,因此我们要加上这个等号,否则可能会漏掉这个区间没去判断
      • 同理,如果nums[mid]>=nums[left],说明此时从left到mid的区间都是有序的。
  • 解决方案

    func search(nums []int,target int)int{
        left,right,mid := 0,len(nums)-1,0
        for left <= right{
            mid = left + (right-left) / 2
            if nums[mid] == target{ // 如果mid位置恰好就是要寻找的,直接返回
                return mid
            }
            // 在mid位置将数组切开,找出有序的区间
            if nums[mid] <= nums[right]{//判断右区间是否有序
                //右区间有序,判断target是否在右区间
                if target > nums[mid] && target <= nums[right]{ // 如果在右区间,那么就调整左边界,接下来进行的都是二分法查找
                    left = mid + 1
                }else{ // 如果不在右区间,那么就将右边界调整到mid左边,接下来会继续切割数组寻找有序区间
                    right = mid - 1
                }
            }else if nums[mid] >= nums[left]{//判断左区间是否有序
                //左区间有序,判断target是否在左区间
                if target >= nums[left] && target < nums[mid]{ // 如果在左区间,那么就调整右边界,接下来进行的都是二分法查找
                    right = mid - 1
                }else{ // 如果不在左区间,那么就将左边界调整到mid右边,接下来会继续切割数组寻找有序区间
                    left = mid + 1
                }
            }
        }
        return -1
    }
    

5.2 有重复元素的旋转数组

  • 问题:81. 搜索旋转排序数组 II - 力扣(Leetcode)

  • 分析:

    • 此题与上一题的区别在于,数组中存在着重复元素
    • 数组中存在着重复元素,将会带来多一个判断条件,即当mid的值与端点的值相同时的情况
      • 当数组中不存在重复元素时,mid的值与端点的值相同的情况只有一种,即mid端点指向的是同一个位置
      • 当数组中存在重复元素时,mid的值与端点的值相同的情况就有3种可能,当这种情况发生时,我们无法判断此时到底是左区间的值全部相同,还是右区间的值全部相同,或者是区间只剩相同的值了
    • 为了寻找到有序区间,我们必须避免出现上述情况,即当遇到mid的值与端点的值相同时,我们可以让left++,让端点移动一位,此时的mid也会跟着移动一位,如果左端点的值仍然与mid的值相同,那么我们就知道左区间的值全是一样的,反之,我们就可以判断是右区间的值全部一样的,我们就可以跳过这些区间了
  • 解决方案

    func search(nums []int,target int)bool{
        left,right,mid := left,len(nums)-1,0
        for left <= right{
            mid = left+(right-left)/2
            if nums[mid] == target{
                return true
            }
            
            if nums[mid] == nums[left]{ //用于避免出现混淆的情况
                left++
            }else if nums[mid] <= nums[right]{ // 这里用等号是因为上面只用了左边界进行判断
                if target > nums[mid] && target <= nums[right]{
                    left = mid + 1
                }else{
                    right = mid - 1
                }
            }else if nums[mid] > nums[left]{// 这里不需要用等号了
                if target < nums[mid] && target >= nums[left]{
                    right = mid - 1
                } else{
                    left = mid + 1
                }
            }
        }
        return false
    }
    

5.3 旋转数组的最小值

  • 问题:153. 寻找旋转排序数组中的最小值

  • 分析:

    • 首先,这道题也是有重复元素的,因此,基本可以套上一题的框架
    • 这里要找的是最小值,我们如何去找呢?我们通过二分法,将一个旋转数组一分为二,一定可以找到一个有序数组,由于是升序的,那么,这个有序数组的左端点,就是当前有序区间的最小值
      • 当右区间为有序区间时,mid所在的位置即为当前有序区间最小值
      • 当左区间为有序区间时,left所在的位置即为当前有序区间的最小值
    • 找到一个最小值后,我们继续去切割无序区间,寻找下一个有序区间,然后获得下一个有序区间的最小值,然后比较获取更小的值,直到切割完所有区间为止
    • 还有一个情况,在上一题中,我们加了一个端点判断,为的就是避免掉重复元素的那段区间,这里我们同样需要避免这段区间,但是,我们要找的最小值却有可能就在这段重复区间内,因此,我们在逃避之前,还要先比较一下当前最小值和区间里的值谁更小
  • 解决方案

    func findMin(nums []int)int{
        left,right,mid := 0,len(nums)-1,0
        ans := 5001 //这里是因为题目写了最大值是5000,只要比5000大都行,可以直接设置为math.MaxInt64
        for left <= right{
            mid = left + (right-left)/2
            if nums[mid] == nums[left]{ // 避免重复区间,但是要判断最小值是否在这个区间内
                ans = min(ans,nums[left])
                left++
            }else if nums[mid] <= nums[right]{ //右区间有序,就比较mid的值,然后调整到无序区间进行下次切割
                ans = min(ans,nums[mid])
                right = mid - 1
            }else{ //左区间有序,就比较left的值,然后调整到无序区间进行下次切割
                ans = min(ans,nums[left])
                left = mid + 1
            }
        }
        return ans
    }
    
    func min(a,b int)int{
        if a < b{
            return a
        }
        return b
    }
    

六、 总结

  • 二分法常用于解决数组中查找元素的问题
  • 使用二分法的前提条件:有序,无重复元素的序列
  • 二分法有两种不同的区间,区间不同,代码细节不同
    • 左闭右闭区间:循环条件为left<=right,当nums[mid]>target时,right = mid - 1
    • 左闭右开区间:循环条件为lefttarget时,right=mid
  • 当题目出现重复元素时,使用二分法寻找插入位置的算法(第二题),能够找到第一个大于等于target的数的索引位置,利用这个思路,可以寻找重复元素的边界范围
  • 遇到旋转数组,首先要看题目是否存在重复元素,然后使用二分法切割寻找有序区间即可
  • 此外,出现寻找平方数,开根等题目时,也可以利用二分法巧妙解题。

你可能感兴趣的:(leetcode算法刷题笔记,leetcode,算法,数据结构,golang)