二分查找
二分查找也称为折半查找,时间复杂度O(logn),是一种较为快速的查找算法。
【使。。最大值尽可能小】【最小值尽可能大】等是二分搜索中常见的问法,想要应用二分查找,前提条件是必须具备如下特征:
1、数据在数组中
2、数据有序排列
特殊场景:寻找峰值---》可以将nums数组中任意给定的序列认为是交替的降序和升序,这样看似无需的排列就变成了有序排列。
算法关键点:
关键变量:low , high, mid 三个元素,根据目标值target 和 mid所在的索引的数值进行对比,根据[mid]中间值的大小,重新确定查找区间。
下面给出了两种常用的二分查找模板:
注意循环跳出条件,以及如何修改边界条件。
代码段1 public int binarySearch(int[] nums, int target) { int low = 0; int high = nums.length - 1; //high没有超出数组边界 while (low <= high) { // 循环条件,此处有等号 int mid = low + (high - low) / 2; // 避免整数相加溢出 if (nums[mid] == target) { return mid; //找到值就返回mid } else if (nums[mid] > target) { high = mid - 1; //调整边界 } else { low = mid + 1; //调整边界 } } return -1; //未查到对应值,返回-1 }
代码段2 public int binarySearch1(int[] nums, int target) { int low = 0; int high = nums.length; //high超出了边界 -----说明high一定不是查找目标 while (low < high) { //这里是小于号,因为high超过了边界 int mid = low + (high - low) / 2; if (nums[mid] == target) { return mid; } else if (nums[mid] > target) { high = mid; //最外层没有减一,内层就不减一 // --- mid 已经确定不是查找目标了,所有把mid变成high,一定不是查找目标 } else { low = mid + 1; } } return -1; }
题目:
给定一个非负整数数组
nums
和一个整数m
,你需要将这个数组分成m
个非空的连续子数组。设计一个算法使得这m
个子数组各自和的最大值最小。nums = [7,2,5,10,8], m = 2 ;nums = [1,2,3,4,5], m = 2; nums = [1,4,4], m = 3
分析:注意题干关键词,子数组和最大值最小,因此可以考虑使用二分法进行求解。
但是二分查找的前提条件:数组、升序 ,这两个怎么满足?(实际上,没有条件是可以创建条件的,如当前要找的子数组的和的最小值,那么什么时候最小?什么时候最大?把从最小值到最大值之间所有的数据放到一个数组中不就满足这个条件了吗?)
题目解析:
1、寻找边界:
low:最小值应该是该数组中最大的值
high:最大值应该是改数组中所有值的和
2、二分查找:
如何把mid 和 target联系起来?常规的二分法num[mid] == target 即可,但是显然不适用于此题。因为给出的m,是分割出来的数组的个数。-------->我们可以想到,分割出来的数组越多,那么最大的子数组和可能就越小,分割的数组越少,对应的最大子数组和可能就越大。借助letcode题解中的一个解释,比较明白的解释了这个问题:
代码:
public class Test410 { public int splitArray(int[] nums, int m) { // 0 <= nums[i] <= 10^6 在此条件下,可以取最小值,left=0; int left = 0; int right = 0; //对数组进行遍历,最小值为数组中值最大的一个,最大值为数组中所有值相加。 // 这个就是边界的寻找 for (int i = 0; i < nums.length; i++) { left = Math.max(left, nums[i]); right += nums[i]; } //二分法,先取一个最大值和最小值中间的一个值,假定这个值是满足要求的最小值。 int mid = left + (right - left) / 2; //经典二分法模板,循环调用,确保得到想要的值。 while (left < right) { if (check(nums, m, mid)) { //满足条件,就尝试在取一个较小的值试一下 right = mid; } else { // 不满足条件,需要取一个较大的值 left = mid + 1; } // 把mid的值改变放到这里 mid = left + (right - left) / 2; } return mid; } // 二分法中的判断方法。 public boolean check(int[] nums, int m, int mid) { //count为分割出来的子数组的数量,1 <= nums.length <= 1000,因此count最小值为1,可以初始化定义这个数量; int count = 1; //sum记录每个子数组的和,初始化值为0 int sum = 0; for (int i = 0; i < nums.length; i++) { // 如果在循环中遇到了sum + nums[i]>mid,那么说明 nums[0]~nums[i-1]之间的和是小于或者等于mid的 if (sum + nums[i] > mid) { // 这种情况下,可以定义nums[0]~nums[i-1]为一个子数组,后面继续遍历,因此子数组数量会++; count++; // 后续新遍历子数组是,第一个数字为 nums[i] sum = nums[i]; } else { // sum + nums[i]<=mid,子数组仍可以继续扩大。 sum += nums[i]; } } // 遍历结束时, 如果子数组的数量小于等于要求的m值,说明mid的值取大了,因此可以缩小right的值;否则mid值可能取小了,可以增大left的值。 // 可以看上图,等于也要包含 return count <= m; } }
class Solution: def splitArray(self, nums: List[int], k: int) -> int: left, right = max(nums), sum(nums) def check(mid): count, n = 1, 0 for i in nums: if n + i > mid: count += 1 n = i else: n += i return count <= k while left < right: mid = (left + right) // 2 if check(mid): right = mid else: left = mid + 1 return left
题解
1、直接暴力搜索,忽略不写
2、使用二分法:
由题目意思,数组其实是由两段升序子数组排列的。升序的数组,可以考虑二分法求解,且复杂度符合O(logn)
代码:
public static int search(int[] nums, int target) { int len = nums.length; if (len == 1) { return nums[0] == target ? 0 : -1; } int left = 0; int right = len - 1; int mid; // 二分循环查找条件 while (left <= right) { // 求中间值 mid = left + (right - left) / 2; if (nums[mid] == target) { return mid; } // 代表左侧为升序数组 if (nums[0] <= nums[mid]) { // 目标值落在了左侧升序数组里面,注意等号的使用 // 进入此段代码,截下来就是常规二分法 if (target >= nums[0] && target < nums[mid]) { right = mid - 1; } // 目标值不在左侧升序数组中,那么左边界重新划定,下次仍然是选择数组 else { left = mid + 1; } } // 左侧非升序数组 else { // 右侧是正常的升序数组 // 目标值在右侧数组内,从新划分左边界,接下来就是常规二分 if (target > nums[mid] && target <= nums[len - 1]) { left = mid + 1; } // 重新划分右边界,下次循环仍然是一个旋转数组 else { right = mid - 1; } } } return -1; }
class Solution: def search(self, nums: List[int], target: int) -> int: length = len(nums) if length == 1: return 0 if target == nums[0] else -1 left, right = 0, len(nums) - 1 while left <= right: mid = (left + right) // 2 if nums[mid] == target: return mid if nums[mid] >= nums[0]: if nums[0] <= target < nums[mid]: right = mid - 1 else: left = mid + 1 else: if nums[length - 1] >= target > nums[mid]: left = mid + 1 else: right = mid - 1 return -1