leetcode二分查找算法总结

二分查找(binary search)是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。二分查找算法在情况下的复杂度是对数时间,进行O(log n)次比较操作。

二分查找有很多等价写法,判断搜索循环条件,左右边界更新方式的选择需要配合,否则容易出现死循环或者遗漏区域。在这里给出两个框架,根据具体问题选择适当的框架解决。

常见的二分查找通常在左闭右闭区间[left,right]范围内进行搜索,循环条件为left\leq right,左边界更新为left = mid + 1,右边界更新为right = mid - 1。

class Solution:
    def searchIndex(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2    # 防止(left+right)太大而溢出
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                left = mid + 1
            else:
                right = mid - 1
        return -1

上面的做法当mid没有找到恰好满足条件的解,需要对左右边界进行判断时,左右边界不等,使判断变得复杂。而使用左闭右开区间[left,right)时,使用下述算法框架可保证left==right,只需判断一个位置即可。

class Solution:
    def searchIndex(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums)
        while left < right:
            mid = left + (right - left) // 2    # 防止(left+right)太大而溢出
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                left = mid + 1
            else:
                right = mid
        return -1

35.搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

你可以假设数组中无重复元素。

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

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

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums)
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return mid
            elif nums[mid] < target:
                left = mid + 1
            else:
                right = mid
        return left

69.x的平方根

实现 int sqrt(int x) 函数。

计算并返回 x 的平方根,其中 x 是非负整数。

由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

输入: 4
输出: 2

输入: 8
输出: 2
说明: 8 的平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

x的平方根不会超过x的一半,所以将右边界设置为x / 2 + 1

class Solution:
    def mySqrt(self, x: int) -> int:
        if x==0 or x==1:
            return x
        left = 1
        right = x // 2 + 1
        while left < right:
            mid = left + (right - left) // 2
            if mid * mid == x:
                return mid
            elif mid * mid < x:
                left = mid + 1
            else:
                right = mid
        return left - 1

34.在排序数组中查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

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

如果数组中不存在目标值,返回 [-1, -1]。

输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]

输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]

一开始的想法使用二分查找找出目标值所在的任意一个索引,再向前后分别遍历。但这种算法在最坏情况下时间复杂度为O(n),即整个数组内值全为target。因此使用两个二分查找,分别查找目标值的左边界和右边界。

class Solution:
    def searchRange(self, nums: List[int], target: int) -> List[int]:
        ret = [-1,-1]
        ret[0] = self.searchLeft(nums,target)
        ret[1] = self.searchRight(nums,target)
        return ret


    def searchLeft(self,nums,target):
        left = 0
        right = len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                if mid == 0 or nums[mid-1] != target:
                    return mid
                else:
                    right = mid - 1  # 不是左边界,继续二分查找
            elif target > nums[mid]:
                left = mid + 1
            else:
                right = mid - 1 
        return -1

    def searchRight(self,nums,target):
        left = 0
        right = len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                if mid == len(nums) - 1 or nums[mid+1] != target:
                    return mid
                else:
                    left = mid + 1  # 不是右边界,继续二分查找
            elif target > nums[mid]:
                left = mid + 1
            else:
                right = mid - 1
        return -1

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

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

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

请找出其中最小的元素。

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

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

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

如果mid处值小于前一个值,则mid处值为解返回,若mid==0,则判断其与nums[1]值的大小关系。判断区间内数组是否为有序数组,若有序,返回区间的第一个值nums[left],若仍为旋转数组,通过mid处的值与left,right处值比较,判断继续在左区间搜索or右区间搜索。

class Solution:
    def findMin(self, nums: List[int]) -> int:
        if len(nums)==1:
            return nums[0]
        left = 0
        right = len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if mid == 0 and nums[1] < nums[0]:
                return nums[1]
            elif mid != 0 and nums[mid-1] > nums[mid]:
                return nums[mid]
            else:
                if nums[left] > nums[right]:
                    if nums[mid] > nums[right]:
                        left = mid + 1
                    else:
                        right = mid - 1
                else:
                    return nums[left]

33.搜索旋转排序数组

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

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

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

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

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

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

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

此题为部分有序的数组,且要求复杂度为O(log n),暗示需使用二分搜索算法。由于数组部分有序,需尤其注意如何更新左右边界。有序数组当target>nums[mid]时,应继续搜索右半部分,即更新左边界。但有一种情况需要在左半部分进行搜索,把当前的数组看作两个有序数组的拼接,当mid落在第二个有序数组内,且target比第二个有序数组的最大值还大,此时要查找的值在第一个有序数组内,此时,更新右边界。当target

注:当然也可先使用上一题的方式找到旋转位置,再在两个有序的数组内进行二分查找

class Solution:
    def search(self, nums: List[int], target: int) -> int:
        left = 0
        right = len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return mid
            elif target > nums[mid]:
                # 在左边搜索,需要满足两个条件
                if nums[mid] < nums[left] and nums[right] < target: 
                    right = mid - 1
                else:
                    left = mid + 1
            else:
                # 在右边搜索,需要满足两个条件
                if nums[mid] > nums[right] and target < nums[left]: 
                    left = mid + 1
                else:
                    right = mid - 1
        return -1

81.搜索旋转排序数组2

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

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

编写一个函数来判断给定的目标值是否存在于数组中。若存在返回 true,否则返回 false。

这是搜索旋转排序数组的延伸题目,本题中的nums可能包含重复元素。

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

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

继续按照上题的思路解决本题,由于有重复数字,使得target在第一个有序数组还是第二个的边界判断更加复杂。在边界处跳过所有重复数字,然后进行判断。最坏情况下有很多重复数字,遍历数组,时间复杂度为O(n)

class Solution:
    def search(self, nums: List[int], target: int) -> bool:
        left = 0
        right = len(nums) - 1
        while left <= right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return True
            elif target > nums[mid]:
                while nums[mid] == nums[left] and mid > left: # 不超过mid
                    left += 1
                if nums[mid] < nums[left] and target >= nums[left]: # 在左边搜索
                    right = mid - 1
                else:
                    left = mid + 1
            else:
                while nums[mid] == nums[right] and mid < right:
                    right -= 1
                if nums[mid] > nums[right] and target <= nums[right]: # 在右边搜索
                    left = mid + 1
                else:
                    right = mid - 1
        return False

74.搜索二维矩阵

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:

每行中的整数从左到右按升序排列。
每行的第一个整数大于前一行的最后一个整数。

输入:
matrix = [
  [1,   3,  5,  7],
  [10, 11, 16, 20],
  [23, 30, 34, 50]
]
target = 3
输出: true
matrix = [
  [1,   3,  5,  7],
  [10, 11, 16, 20],
  [23, 30, 34, 50]
]
target = 13
输出: false

两个二分搜索,先找出target可能在的list,再在这个list中查找是否存在target。

class Solution:
    def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
        if len(matrix) == 0 or len(matrix[0]) == 0: 
            return False
        left = 0
        right = len(matrix)
        while left < right:
            mid = left + (right - left) // 2
            if matrix[mid][0] == target:
                return True
            elif target < matrix[mid][0]:
                right = mid
            else:
                left = mid + 1
        nums = matrix[left-1]
        left = 0
        right = len(nums)
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] == target:
                return True
            elif target < nums[mid]:
                right = mid
            else:
                left = mid + 1
        return False

162.寻找峰值

峰值元素是指其值大于左右相邻值的元素。

给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。

数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞。

输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。

输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5 
解释: 你的函数可以返回索引 1,其峰值元素为 2;或者返回索引 5, 其峰值元素为 6。
说明:你的解法应该是 O(logN) 时间复杂度的。

解法比较简单,主要是需要想到只要数组中存在一个元素比相邻元素大,那么沿着它一定可以找到一个峰值(想象山峰的形状)。比较mid与mid+1处值,如果mid较大,则左侧存在峰值,如果 mid + 1 较大,则右侧存在峰值。

class Solution:
    def findPeakElement(self, nums: List[int]) -> int:
        left = 0
        right = len(nums) - 1
        while left < right:
            mid = left + (right - left) // 2
            if nums[mid] < nums[mid+1]:
                left = mid + 1
            else:
                right = mid
        return left

410.分割数组的最大值

给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。

注意:
数组长度 n 满足以下条件:

1 ≤ n ≤ 1000
1 ≤ m ≤ min(50, n)

输入:
nums = [7,2,5,10,8]
m = 2

输出:
18

解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。

在max(nums)和sum(nums)之间进行二分查找,子数组和的范围在这二者之间。对于一个mid,计算根据这个mid能将数组分为几个子数组。如小于m,则还可以继续分组,mid大了,在左侧继续搜索。若大于m证明分组多了,mid小了,在右侧继续搜索。最后返回交点。

class Solution {
    public int splitArray(int[] nums, int m) {
        // 在 max(nums) 和 sum(nums)中二分 子数组最大值是有范围的
        int left = 0;
        int right = 0;
        for(int num:nums) {
            right += num;
            left = num > left ? num : left;
        }
        while(left < right) {
            int mid = left + (right - left) / 2;
            int cnt = 1; //记录分成的组数
            int sum = 0;
            for(int num:nums) {
                if(sum + num > mid) {
                    sum = num;
                    cnt++;
                } else {
                    sum += num;
                }
            }
            if(cnt > m) {  //组数大于规定组数,证明mid小了,在右侧继续搜索
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left;
    }
}

 

 

 

 

 

你可能感兴趣的:(leetcode)