二分查找

二分查找

理解

查找在算法题中是很常见的,但是怎么最大化查找的效率和写出bugfree的代码才是难的部分。一般查找方法有顺序查找、二分查找和双指针,推荐一开始可以直接用顺序查找,如果遇到TLE的情况再考虑剩下的两种,毕竟AC是最重要的。

一般二分查找的对象是有序或者由有序部分变化的(可能暂时理解不了,看例题即可),但还存在一种可以运用的地方是按值二分查找,之后会介绍。

代码模板

总体来说二分查找是比较简单的算法,网上看到的写法也很多,掌握一种就可以了。
以下是我的写法,参考C++标准库里的写法。这种写法比较好的点在于:

  • 1.即使区间为空、答案不存在、有重复元素、搜索开/闭区间的上/下界也同样适用
  • 2.±1 的位置调整只出现了一次,而且最后返回lo还是hi都是对的,无需纠结
class Solution:
    def firstBadVersion(self, arr):
        # 第一点
        lo, hi = 0, len(arr)-1
        while lo < hi:
            # 第二点
            mid = (lo+hi) // 2
            # 第三点
            if f(x):
                lo = mid + 1
            else:
                hi = mid
        return lo

解释

  • 第一点:lo和hi分别对应搜索的上界和下界,但不一定为0和arr最后一个元素的下标。
  • 第二点:因为Python没有溢出,int型不够了会自动改成long int型,所以无需担心。如果再苛求一点,可以把这一行改成
mid = lo + (hi-lo) // 2
# 之所以 //2 这部分不用位运算 >> 1 是因为会自动优化,效率不会提升
  • 第三点:
    比较重要的就是这个f(x),在带入模板的情况下,写对函数就完了。

那么我们一步一步地揭开二分查找的神秘面纱,首先来一道简单的题。

LeetCode 35. Search Insert Position

给定排序数组和目标值,如果找到目标,则返回索引。如果不是,则返回按顺序插入索引的位置的索引。 您可以假设数组中没有重复项。

Example

Example 1:
Input: [1,3,5,6], 5
Output: 2

Example 2:
Input: [1,3,5,6], 2
Output: 1

Example 3:
Input: [1,3,5,6], 7
Output: 4

Example 4:
Input: [1,3,5,6], 0
Output: 0

分析: 这里要注意的点是 high 要设置为 len(nums) 的原因是像第三个例子会超出数组的最大值,所以要让 lo 能到 这个下标。

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

LeetCode540. Single Element in a Sorted Array

您将获得一个仅由整数组成的排序数组,其中每个元素精确出现两次,但一个元素仅出现一次。 找到只出现一次的单个元素。

Example

Example 1:

Input: [1,1,2,3,3,4,4,8,8]
Output: 2

Example 2:

Input: [3,3,7,7,10,11,11]
Output: 10

分析: 异或的巧妙应用!如果mid是偶数,那么和1异或的话,那么得到的是mid+1,如果mid是奇数,得到的是mid-1。如果相等的话,那么唯一的元素还在这之后,往后找就可以了。

class Solution:
    def singleNonDuplicate(self, nums):
        lo, hi = 0, len(nums) - 1
        while lo < hi:
            mid = (lo + hi) // 2
            if nums[mid] == nums[mid ^ 1]:
                lo = mid + 1
            else:
                hi = mid
        return nums[lo]

是不是还挺简单哈哈,那我们来道HARD难度的题!

LeetCode 410. Split Array Largest Sum

给定一个由非负整数和整数m组成的数组,您可以将该数组拆分为m个非空连续子数组。编写算法以最小化这m个子数组中的最大和。

Example

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

Output:
18

Explanation:
There are four ways to split nums into two subarrays.
The best way is to split it into [7,2,5] and [10,8],
where the largest sum among the two subarrays is only 18.

分析:

  • 这其实就是二分查找里的按值二分了,可以看出这里的元素就无序了。但是我们的目标是找到一个合适的最小和,换个角度理解我们要找的值在最小值max(nums)和sum(nums)内,而这两个值中间是连续的。是不是有点难理解,那么看代码吧
  • 辅助函数的作用是判断当前的“最小和”的情况下,区间数是多少,来和m判断
  • 这里的下界是数组的最大值是因为如果比最大值小那么一个区间就装不下,数组的上界是数组和因为区间最少是一个,没必要扩大搜索的范围
class Solution:
    def splitArray(self, nums: List[int], m: int) -> int:

        def helper(mid):
            res = tmp = 0
            for num in nums:
                if tmp + num <= mid:
                    tmp += num
                else:
                    res += 1
                    tmp = num
            return res + 1

        lo, hi = max(nums), sum(nums)
        while lo < hi:
            mid = (lo + hi) // 2
            if helper(mid) > m:
                lo = mid + 1
            else:
                hi = mid
        return lo
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值