LeetCode---二分查找

二分查找

二分查找也称折半查找(Binary Search),是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

基本代码框架

# 基本代码框架
def binarySearch(nums, target):
    left = 0
    
    # right = len(nums) - 1 搭配 while left <= right使用;  
    # right = len(nums) 搭配 while left < right使用。
    
    right = ...

    while ...: 
        # 防溢出整型范围
        mid = left + (right - left) // 2
        if (nums[mid] == target):
            ...
        elif (nums[mid] < target):
            left = ...
        elif (nums[mid] > target):
            right = ...

    return ...

注意:分析二分查找的一个技巧是:不要出现 else,而是把所有情况用 else if 写清楚,这样可以清楚地展现所有细节。 上面代码只是最基础的框架,细节部分根据不同的题目会有变化。

基本应用

寻找一个数(最基本的二分查找)

def binary_search(nums, target):
    left = 0
    # right = len(nums) - 1 对应搜索区间 [left, right]
    right = len(nums) - 1

    # left <= right 代表 left==right+1时退出循环,这里使用left<=right,搭配right=len(nums)-1,
    # 即[right+1, right]退出,可以保证遍历了所有元素;
    # 如果使用left<right 代表 left=right时退出循环,即[right, right]时退出,这会导致right元素没有遍历到
    while left <= right:
        # 防溢出整型范围
        mid = left + (right - left) // 2
        if nums[mid] == target:
            return mid

        elif nums[mid] < target:
            left = mid + 1

        elif nums[mid] > target:
            right = mid - 1

    return -1

寻找左边界

要求:列表中存在重复数字,找到左边界的下标。

def binary_search(nums, target):
    left = 0
    # right = len(nums) 对应搜索区间 [left, right)
    right = len(nums)

    # left < right 代表 left==right时退出循环,这里使用left<right,搭配right=len(nums),
    # 即[right, right)退出,可以保证遍历了所有元素
    while left < right:
        mid = left + (right - left) // 2
        if nums[mid] == target:
            # 因为是找左边界,当前mid可能就是左边界,所以向左收缩空间并且不能漏掉当前mid
            right = mid

        elif nums[mid] < target:
            left = mid + 1

        elif nums[mid] > target:
            right = mid
    
    # 判断找不到的情况
    if right >= length or nums[right] != target:
        return -1

    return right

注意:

  1. if nums[mid] == target —> right = mid
    因为是找左边界,当前mid可能就是左边界,所以向左收缩空间并且不能漏掉当前mid,即 right=mid;

  2. 因为向左收缩时right = mid;而mid=left+(right-left)//2 —> 2mid=left+right —> 2mid=left+mid —> mid=left —> mid=left=right,如果使用while left<=right,则会导致left=right时陷入死循环。因此,不能使用while left <= right,应该选择right=len(nums)搭配while left<right。

  3. 由于选择了right=len(nums)和while left<right,即搜索空间是[left, right),如果找到nums[mid] > target,下一步搜索空间应该是[left,mid),即right=mid。

  4. 由于选择了right=len(nums)和while left<right,即搜索空间是[left, right),如果找到nums[mid] < target,下一步搜索空间应该是[mid+1,righjt),即left=mid+1。

寻找右边界

要求:列表中存在重复数字,找到右边界的下标。

def binary_search(nums, target):
    length = len(nums)
    left = 0
    # 对应搜索区间 [left, right)
    right = length

    # left < right 代表 left==right时退出循环,这里使用left<right,搭配right=len(nums),
    # 即[right, right)退出,可以保证遍历了所有元素
    while left < right:
        mid = left + (right - left) // 2
        if nums[mid] == target:
            # 因为搜索区间是[left, right),如果当前nums[mid] == target,要想向右收缩搜索区间,因为mid已经选过了,所以left=mid+1,即[mid+1, right)
            # 如果nums[mid]就是右边界,那么mid+1就会错过右边界,所以在最后left-1
            left = mid + 1

        elif nums[mid] < target:
            left = mid + 1

        elif nums[mid] > target:
        	# 收缩搜索区间,因为是左闭右开区间,所以right=mid,不需要mid-1
            right = mid

	if left >= length or nums[left-1] != target:
        return -1

    return left - 1

LeetCode

一文带你搞定二分查找及其多个变种

69. x 的平方根

题目链接

题解

def mySqrt(x):
    if x <= 1:
        return x
    left = 0
    # 这里缩小搜索范围,(x//2)^2平方小于x^2,所以最大值选择x//2
    right = x // 2
	
	# 因为搜索范围是[left, right],所以判断条件为left<right,
	# 跳出条件为left==right,即[right,right],可以保证每个元素都被遍历过
    while left < right:
        mid = left + (right - left) // 2

        if mid * mid == x:
            return mid
        
        elif mid * mid < x:
        	# 小于x时,判断mid+1平方是否大于x,如果满足则说明平方根在mid和mid+1之间,取整型后就是mid
            if (mid+1) * (mid+1) > x:
                return mid
            # 走到else说明 mid+1也小于x,则向右缩减搜索空间,即left = mid + 1
            else:
                left = mid + 1
        elif mid * mid > x:
            right = mid - 1

	# 退出循环时 left == right,这里返回left和right均可
    return left

744. 寻找比目标字母大的最小字母

题目链接

题解

    length = len(letters)
    left = 0
    # 搜索区间[left, right]
    right = length - 1

    # 退出条件left = right + 1,即[right+1, right]
    while left <= right:
        mid = left + (right - left) // 2

        if letters[mid] == target:
            left = mid + 1
        elif letters[mid] < target:
            left = mid + 1
        elif letters[mid] > target:
            if letters[mid-1] < target:
                return letters[mid]
            else:
                right = mid - 1

    # 因为循环退出条件是left=right+1, 即执行elif letters[mid]<target: ---> left=mid+1 
    # 或 elif letters[mid]>target:--->right=mid-1后,导致left>right跳出循环,
    # 这时right指向的是小于target的字母,left指向的是大于target的首字母,所以这里选择letters[left]
    return letters[0] if left == length else letters[left]

540. 有序数组中的单一元素

题目链接

题解

def singleNonDuplicate(nums):
    length = len(nums)
    if length == 1:
        return nums[0]
    left = 0
    # 搜索区间[left, right]
    right = length - 1

    while left <= right:
        mid = left + (right - left) // 2

        # mid为列表边界元素时直接返回
        if mid == length-1 or mid == 0:
            return nums[mid]

        # 已经列表中只有一个数时单独存在的,那么判断nums[mid]==nums[mid+1]或nums[mid]==nums[mid-1],
        # 根据当前mid分割,存在单独数字的子列表长度肯定为奇数
        if nums[mid] == nums[mid+1]:
            if mid % 2 == 0:
                left = mid + 2
            else:
                right = mid - 1
        elif nums[mid] == nums[mid-1]:
            if (mid-1) % 2 == 0:
                left = mid + 1
            else:
                right = mid + 1
        else:
            return nums[mid]

    return -1

278. 第一个错误的版本

题目链接

题解

def firstBadVersion(n):
    left = 1
    # 搜索区间[left, right]
    right = n

    # 退出条件 left==right 即 [right, right]
    while left < right:
        mid = left + (right - left) // 2

        # 返回true时,向左收缩搜索区间
        if isBadVersion(mid):
            right = mid
        else:
            left = mid + 1
	
	# 1. 退出循环时left==right;
	# 2. 假如走到了else---> left=mid+1导致了left==right,那么能走到else说明当前left指向的是好的版本,left+1后才是错误的版本
    return left

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

题目链接

题解

    length = len(nums)
    left = 0
    # 搜索区间[left, right]
    right = length - 1

    if nums[0] <= nums[-1]:
        return nums[0]

    # 退出条件 left==right+1 即 [right+1, right]
    while left <= right:
        mid = left + (right - left) // 2

        # 如果mid > mid+1,说明mid+1就是反转点,即最小值
        if nums[mid] > nums[mid+1]:
            return nums[mid+1]

        # nums[mid]>nums[0]时,说明最小值在右边,向右收缩搜索区间
        if nums[mid] > nums[0]:
            left = mid + 1
        # nums[mid]<=num[0]时,说明最小值是当前mid或在左边
        elif nums[mid] <= nums[0]:
            # 判断当前节点值是否小于前一个节点,如果小于,说明当前节点就是反转节点
            if nums[mid] < nums[mid-1]:
                return nums[mid]
            # 向左收缩搜索区间
            else:
                right = mid - 1

    # 1. 走到这里说明left == right+1,
    # 2. 假如是走到 nums[mid]>nums[0] ---> left=mid+1,则left原来的值是大于nums[0],即下一个节点有可能是反转点,所以是mid+1,即nums[left] 有可能是反转点
    # 3. 假如是走到 nums[mid]<=nums[0] ---> right=mid-1,则right原来的值小于那等于nums[0],且前一个值肯定小于当前mid,所以这里不会触发退出循环
    return nums[left]

6. 查找区间

题目链接

题解

def searchRange(nums, target):
    length = len(nums)
    left = 0
    # 搜索区间[left, right]
    right = length - 1

    while left <= right:
        mid = left + (right - left) // 2

        if nums[mid] == target:
            min_index = mid
            max_index = mid
            # 找到之后向前和向后遍历直到找到不是target的停止
            # 如果整个数组都是target,则这种方法时间复杂度会是O(n)
            # 所以最好的办法还是分别找左边界和右边界,时间复杂度是logN级别的
            while min_index >= 0 and nums[min_index] == target:
                min_index -= 1
            while max_index < length and nums[max_index] == target:
                max_index += 1
            return [min_index+1, max_index-1]
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1

    return [-1, -1]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值