Hallo,大家好👋。在工作的过程中,发现算法越来越重要‼️于是笔者决定重新系统性地学习算法💪。同时,开设了个算法专栏,打算记录这个学习的过程。但由于可能时间或个人原因(懒),更新的时间可能就会很随缘......
「二分查找」的思想在生活和工作中很常见,通过不断缩小搜索区间的范围(减治思想),直到找到目标元素或者没有找到目标元素。
减治思想
减是「减少问题规模」,治是「解决」。通俗来说,就是「排除法」,即:每一轮排除掉一定「不存在目标元素」的区间,在剩下「可能存在目标元素」的区间里继续查找。使得问题的规模逐渐减少。因为问题的规模是有限的,通过有限次的操作,问题就得以解决。
例子: 「猜价格」游戏。游戏规则是:给出一个商品,告诉学生它的价格在多少元(价格为整数)以内,让学生猜,如果猜出的价格低于真正价格,老师就说少了,高于真正的价格,就说多了,看谁能在最短的时间内猜中。
假设商品为37元。
🧑🏫老师:在1~100元之间。
🧑🎓学生:50元。
🧑🏫老师:多了。
🧑🎓学生:25元。
🧑🏫老师:少了。
🧑🎓学生:37元。
🧑🏫老师:正确。
这个游戏就是应用「减治思想」完成「猜价格」任务的。老师说「多了」或者「少了」,就是给学生反馈,让学生逐渐缩小价格区间,最终猜中价格。
应用范围
二分下标
在有序数组中进行查找一个数
数组和有序是关键。
数组具有「随机访问」的特性,由于数组在内存中连续存放,因此可以通过数组的下标快速地访问到这个元素。
「有序」不一定要求目标元素所在的区间是有序数组,也就是说「有序」这个条件可以放宽,「半有序」数组或者「山脉」数组里都可以应用二分查找算法。
二分答案
在整数范围内查找一个整数
如果要找的是一个整数,并且知道这个整数的范围,就可以使用二分查找算法,逐渐缩小整数的范围。
假设要找的数最小值为0
,最大值为N
,就可以把这个整数想象成数组 [0, 1, 2,..., N]
里的一个值,这个数组的「下标」和「值」是一样的,找数组的下标就等于找数组的值。
解决思路
运用「减治思想」,一般有两种解决思路,但仅是细节上的不同。
以最基本的 leetcode 704.二分查找为例:
/**
* leetcode 704.二分查找 - 简单
*
* @remarks
* 给定一个 `n` 个元素有序的(升序)整型数组 `nums` 和一个目标值 `target` ,
* 写一个函数搜索 `nums` 中的 `target`,如果目标值存在返回下标,否则返回 `-1`。
*
* @example
* nums = [-1,0,3,5,9,12], target = 9
* 输出:4
*/
循环体中查找元素
分析:解决这个问题的思路和「猜价格」游戏是一样的。由于数组是有序且升序的数组,二分查找的思路是,先看数组的「中间元素」:
如果「中间」元素等于「目标」元素,就直接返回该元素下标; 否则就需要在「中间」元素的左边或者右边继续查找。
var search = function(nums, target) {
const len = nums.length;
let [left, right] = [0, len - 1]
while(left <= right) {
let mid = parseInt(left + (right - left) / 2);
if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1
} else {
return mid
}
}
return -1
}
- 每一次都会在一个区间里搜索目标元素。所以需要两个变量表示数组里区间的「左右边界」,分别用变量
left
和right
表示,左边界的值等于0
,右边界的值等于数组的长度减1
; - 接下来是循环体,循环继续的条件是
left <= right
,表示:在区间只有1
个元素的时候,仍需进行逻辑判断。这个逻辑是,检查区间里位于中间的那个元素的值:- 如果它是「目标」元素,则返回其下标;
- 如果「中间」元素比「目标」元素「严格小」。「中间位置」以及「中间位置的左边」的所有元素的数值一定比目标元素「小」,那么下一轮搜索区间为
[left = mid + 1, right]
; - 如果「中间」元素比「目标」元素「严格大」,「中间位置」以及「中间位置的右边」的所有元素的数值一定比目标元素「大」,那么下一轮搜索区间为
[left, right = mid - 1]
;
- 退出循环后,表示不存在目标元素,返回
−1
;
循环体中排除区间
生活中我们往往很清楚自己「不需要什么」,但是不清楚自己「需要什么」。所以,从「中间元素」在什么情况下「不是目标元素」考虑,会使得问题变得简单。
分析:题目要找== target
的元素。对这个性质取反,就是!= target
,也就是< target
或者> target
,这两个条件选择其中一个,都可以缩小问题的区间。
< target
:
var search = function(nums, target) {
const len = nums.length
let [left, right] = [0, len - 1]
let midIndex
while (right > left) {
midIndex = Math.floor(left + (right - left) / 2)
if (nums[midIndex] < target) {
left = midIndex + 1
} else {
right = midIndex
}
}
if (nums[left] === target) {
return left
}
return -1
};
> target
:
var search = function(nums, target) {
const len = nums.length
let [left, right] = [0, len - 1]
let midIndex
while (right > left) {
midIndex = Math.ceil(left + (right - left) / 2)
if (nums[midIndex] > target) {
right = midIndex - 1
} else {
left = midIndex
}
}
if (nums[left] === target) {
return left
}
return -1
};
- 循环可以继续的条件是
left < right
。在「循环体中查找元素」中,当left == right
,「左右边界」重合的时候,区间里只有一个元素时,查找继续;而在「循环体中排除区间」,在left == right
时退出了循环,表示区间里只剩下一个元素时,退出循环; - 在退出循环以后,还需要单独做一次判断(若要找的「目标元素一定落在给的区间内」,那么该判断可以省略);
实现细节
二分查找算法在实现的过程中,很容易出现「边界」问题、「中间值」问题等,因此需要注意细节的实现。
循环继续条件
- 循环体中查找元素:
while (left <= right)
; - 循环体中排除区间:
while (left < right)
;
取中间值法
取中间数的代码(left + right) / 2
,在left
和right
很大时,left + right
有可能会发生整型溢出(±2^53
)。因此可以改用以下写法,left + (right - left) / 2
或者(left + right) >> 1
。
>>
表示「位运算」中的「右移」,整型「右移」一位相当于/2
。但高级编程语言在/2
时,底层都会转化成为「位运算」。为了代码的易读性,不建议这样写。
中间值上下取整
-
向下取整:
Math.floor(left + (right - left) / 2)
;- 使用「循环体中排除区间」思路,且区间剩下两个元素时,采取「向下取整」,此时
left == mid
,若后续代码出现left = mid
,可能会陷入「死循环」。
- 使用「循环体中排除区间」思路,且区间剩下两个元素时,采取「向下取整」,此时
-
向上取整:
Math.ceil(left + (right - left) / 2)
;- 使用「循环体中排除区间」思路,且区间剩下两个元素时,采取「向下取整」,此时
right == mid
,若后续代码出现right = mid
,可能会陷入「死循环」。
- 使用「循环体中排除区间」思路,且区间剩下两个元素时,采取「向下取整」,此时
因此,在决定「向上/下取整」时,需要根据后续的逻辑而改变。
小技巧:出现
left = mid
时,中间值「向上取整」;出现right = mid
时,中间值「向下取整」。
lc例题
⚠️:以下代码并不是「官方题解」或「最优解」,仅仅是笔者基于「二分查找」思路的实现。
二分下标
指在一个有序数组(该条件可以适当放宽)中查找目标元素的下标。
35 - 简单
/**
* leetcode 35.搜索插入位置 - 简单
*
* @remarks
* 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。
* 如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
*
* @example
* 输入: nums = [1,3,5,6], target = 5
* 输出: 2
*/
const search_35 = (nums, target) => {
const len = nums.length;
if (!len) return 0;
let [left, right] = [0, len];
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
if (target > nums[mid]) {
left = mid + 1;
} else {
right = mid;
}
}
return left;
};
34 - 中等
/**
* leetcode 34.在排序数组中查找元素的第一个和最后一个位置 - 中等
*
* @remarks
* 给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。
* 请你找出给定目标值在数组中的开始位置和结束位置。
* 如果数组中不存在目标值 target,返回 [-1, -1]。
*
* @example
* 输入:nums = [5,7,7,8,8,10], target = 8
* 输出:[3,4]
*
* @example
* 输入:nums = [5,7,7,8,8,10], target = 6
* 输出:[-1,-1]
*/
const search_34 = (nums, target) => {
const len = nums.length;
if (!len) return [-1, -1];
let [left, right] = [0, len - 1];
let [round_left, round_right] = [0, len - 1];
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
if (target > nums[mid]) {
left = mid + 1;
} else {
right = mid;
}
}
if (nums[left] !== target) return [-1, -1];
while (round_left < round_right) {
let mid = Math.ceil(round_left + (round_right - round_left) / 2);
if (target < nums[mid]) {
round_right = mid - 1;
} else {
round_left = mid;
}
}
return [left, round_left];
};
153 - 中等
/**
* leetcode 153.寻找旋转排序数组中的最小值 - 中等
*
* @remarks
* 已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。
* 例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
* 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
* 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
* 注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
* 给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
*
* @example
* 输入:nums = [3,4,5,1,2]
* 输出:1
*/
const search_153 = (nums) => {
const len = nums.length;
let [left, right] = [0, len - 1];
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
if (nums[left] < nums[right]) return nums[left];
if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
};
154 - 困难
/**
* leetcode 154.寻找旋转排序数组中的最小值 II - 困难
*
* @remarks
* 已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。
* 例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到:
* 若旋转 4 次,则可以得到 [4,5,6,7,0,1,4]
* 若旋转 7 次,则可以得到 [0,1,4,4,5,6,7]
* 注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]] 。
* 给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
*
* @example
* 输入:nums = [1,3,5]
* 输出:1
*/
const search_154 = (nums) => {
const len = nums.length;
let [left, right] = [0, len - 1];
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
if (nums[left] < nums[right]) return nums[left];
if (nums[left] === nums[right]) {
right -= 1;
} else if (nums[mid] > nums[right]) {
left = mid + 1;
} else {
right = mid;
}
}
return nums[left];
};
33 - 中等
/**
* leetcode 33.搜索旋转排序数组 - 中等
*
* @remarks
* 整数数组 nums 按升序排列,数组中的值 互不相同 。
* 在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,
* 使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。
* 例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
* 给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。
*
* @examle
* 输入:nums = [4,5,6,7,0,1,2], target = 0
* 输出:4
*/
const search_33 = (nums, target) => {
const len = nums.length;
let [left, right] = [0, len - 1];
while (left + 1 < right) {
let mid = Math.floor(left + (right - left) / 2);
// 未经旋转
if (nums[right] > nums[left]) {
if (target > nums[mid]) {
left = mid + 1;
} else {
right = mid;
}
continue;
}
// 前半区有序
if (nums[mid] > nums[left]) {
// 目标在此前半区
if (nums[left] <= target && nums[mid] >= target) {
right = mid;
} else {
left = mid;
}
}
// 后半区有序
if (nums[right] > nums[mid]) {
// 目标在后区域
if (nums[mid] <= target && nums[right] >= target) {
left = mid;
} else {
right = mid;
}
}
}
if (nums[left] === target) return left;
if (nums[right] === target) return right;
return -1;
};
81 - 中等
/**
* leetcode 81.搜索旋转排序数组 II - 中等
*
* @remarks
* 已知存在一个按非降序排列的整数数组 nums ,数组中的值不必互不相同。
* 在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转 ,
* 使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标 从 0 开始 计数)。
* 例如, [0,1,2,4,4,4,5,6,6,7] 在下标 5 处经旋转后可能变为 [4,5,6,6,7,0,1,2,4,4] 。
* 给你 旋转后 的数组 nums 和一个整数 target ,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target ,则返回 true ,否则返回 false 。
*
* @expamle
* 输入:nums = [2,5,6,0,0,1,2], target = 0
* 输出:true
*
* @example
* 输入:nums = [2,5,6,0,0,1,2], target = 3
* 输出:false
*/
const search_81 = (nums, target) => {
const len = nums.length;
let [left, right] = [0, len - 1];
while (left + 1 < right) {
let mid = Math.floor(left + (right - left) / 2);
if (nums[right] === nums[left]) {
right -= 1;
continue;
}
// 有序
if (nums[right] > nums[left]) {
if (target > nums[mid]) {
left = mid + 1;
} else {
right = mid;
}
continue;
}
// 前半区有序
if (nums[mid] >= nums[left]) {
// 目标在此前半区
if (nums[left] <= target && nums[mid] >= target) {
right = mid;
} else {
left = mid;
}
}
// 后半区有序
if (nums[right] >= nums[mid]) {
// 目标在后区域
if (nums[mid] <= target && nums[right] >= target) {
left = mid;
} else {
right = mid;
}
}
}
return nums[left] === target || nums[right] === target;
};
278 - 简单
/**
* leetcode 278.第一个错误的版本 - 简单
*
* @remarks
* 你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。
* 由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
* 假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
* 你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。
* 实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。
*
* @example
* 输入:n = 5, bad = 4
* 输出:4
*/
const search_278 = (n) => {
let [left, right] = [0, n];
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
if (isBadVersion(mid)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
};
852 - 中等
/**
* leetcode 852.山脉数组的峰顶索引 - 中等
*
* @remarks
* 符合下列属性的数组 arr 称为 山脉数组 :
* arr.length >= 3
* 存在 i(0 < i < arr.length - 1)使得:
* arr[0] < arr[1] < ... arr[i-1] < arr[i]
* arr[i] > arr[i+1] > ... > arr[arr.length - 1]
* 给你由整数组成的山脉数组 arr ,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i 。
*
* @example
* 输入:arr = [0,10,5,2]
* 输出:1
*
* @example
* 输入:arr = [3,4,5,1]
* 输出:2
*
* @example
* 输入:arr = [24,69,100,99,79,78,67,36,26,19]
* 输出:2
*/
const search_852 = (arr) => {
const len = arr.length;
let [left, right] = [0, len - 1];
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
// 左山峰
if (arr[mid] < arr[mid + 1]) {
left = mid + 1;
// 右山峰
} else {
right = mid;
}
}
return left;
};
1095 - 困难
/**
* leetcode 1095.山脉数组中查找目标值 - 困难
*
* @remarks
* 给你一个 山脉数组 mountainArr,请你返回能够使得 mountainArr.get(index) 等于 target 最小 的下标 index 值。
* 如果不存在这样的下标 index,就请返回 -1。
* 你将 不能直接访问该山脉数组,必须通过 MountainArray 接口来获取数据:
* MountainArray.get(k) - 会返回数组中索引为k 的元素(下标从 0 开始)
* MountainArray.length() - 会返回该数组的长度
*
* @example
* 输入:array = [1,2,3,4,5,3,1], target = 3
* 输出:2
* 解释:3 在数组中出现了两次,下标分别为 2 和 5,我们返回最小的下标 2。
*
* @example
* 输入:array = [0,1,2,4,2,1], target = 3
* 输出:-1
* 解释:3 在数组中没有出现,返回 -1。
*/
const search_1095 = (target, mountainArr) => {
const len = mountainArr.length();
let [left, right] = [0, len - 1];
let top = 0;
// 寻找最高峰
while (left + 1 < right) {
let mid = Math.floor(left + (right - left) / 2);
// 左山峰
if (mountainArr.get(mid) < mountainArr.get(mid + 1)) {
left = mid + 1;
// 右山峰
} else {
right = mid;
}
}
top = mountainArr.get(left) > mountainArr.get(right) ? left : right;
// 左山峰查找
left = 0;
right = top;
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
if (mountainArr.get(mid) < target) {
left = mid + 1;
} else {
right = mid;
}
}
if (target === mountainArr.get(left)) return left;
// 右山峰查找
left = top + 1;
right = len - 1;
while (left < right) {
let mid = Math.ceil(left + (right - left) / 2);
if (mountainArr.get(mid) < target) {
right = mid - 1;
} else {
left = mid;
}
}
if (target === mountainArr.get(left)) return left;
return -1;
};
4 - 困难
/**
* leetcode 4.寻找两个正序数组的中位数 - 困难
*
* @remarks
* 给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
* 算法的时间复杂度应该为 O(log (m+n))
*
* @example
* 输入:nums1 = [1,3], nums2 = [2]
* 输出:2.00000
*/
const search_4 = (nums1, nums2) => {
// 确保上数组的长度要小于下数组
if (nums1.length > nums2.length) {
[nums1, nums2] = [nums2, nums1];
}
const nums1_len = nums1.length;
const nums2_len = nums2.length;
// 左半区元素个数(确保左半区元素个数等于右半区元素个数或者只多一个)
// 对于数组长度和为偶数时,由于采取向下取整,所以可以 + 1
// 对于数组长度和为奇数时,左半区比右半区多一个,所以可以 + 1
// 因此统一 + 1 处理
let total_left = Math.floor((nums1_len + nums2_len + 1) / 2);
// 在 nums1 的 [0, nums1_len]区间寻找分割线
// 该分割线满足 nums1[i - 1] <= nums2[j] && nums1[i] >= nums2[j - 1]
let [left, right] = [0, nums1_len];
while (left < right) {
let i = Math.ceil(left + (right - left) / 2);
let j = total_left - i;
if (nums1[i - 1] > nums2[j]) {
right = i - 1;
} else {
left = i;
}
}
// 确定分割线位置
let [i, j] = [left, total_left - left];
const nums1_left = i === 0 ? -1 * Number.MAX_VALUE : nums1[i - 1];
const nums1_right = i === nums1_len ? Number.MAX_VALUE : nums1[i];
const nums2_left = j === 0 ? -1 * Number.MAX_VALUE : nums2[j - 1];
const nums2_right = j === nums2_len ? Number.MAX_VALUE : nums2[j];
// 偶数
if ((nums1_len + nums2_len) % 2 === 0) {
return (
((nums1_left > nums2_left ? nums1_left : nums2_left) +
(nums1_right < nums2_right ? nums1_right : nums2_right)) /
2
);
// 奇数
} else {
return nums1_left > nums2_left ? nums1_left : nums2_left;
}
};
二分答案
要求找的是一个整数,并且知道这个整数「最小值」和「最大值」。此时,可以考虑使用二分查找算法找到这个目标值。
69 - 简单
/**
* leetcode 69.x 的平方根 - 简单
*
* @remarks
* 给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
* 由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
*
* @example
* 输入:x = 4
* 输出:2
*/
const search_69 = (x) => {
if (x === 1) return 1;
let [left, right] = [0, x];
while (left + 1 < right) {
let mid = Math.floor(left + (right - left) / 2);
let result = mid * mid;
if (result > x) {
right = mid;
} else {
left = mid;
}
}
return left;
};
287 - 中等
/**
* leetcode 287.寻找重复数 - 中等
*
* @remarks
* 给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
* 假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
* 你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间。
*
* @example
* 输入:nums = [1,3,4,2,2]
* 输出:2
*/
const search_287 = (nums) => {
const len = nums.length;
if (len === 2) return nums[0];
let [left, right, answer] = [1, len - 1, -1];
while (right >= left) {
let [mid, count] = [Math.floor(left + (right - left) / 2), 0];
for (let i = 0; i < len; i++) {
if (nums[i] <= mid) {
count++;
}
}
if (count <= mid) {
left = mid + 1;
} else {
right = mid - 1;
answer = mid;
}
}
return answer;
};
1300 - 中等
/**
* leetcode 1300.转变数组后最接近目标值的数组和 - 中等
*
* @remarks
* 给你一个整数数组 arr 和一个目标值 target ,请你返回一个整数 value ,
* 使得将数组中所有大于 value 的值变成 value 后,数组的和最接近 target (最接近表示两者之差的绝对值最小)。
* 如果有多种使得和最接近 target 的方案,请你返回这些整数中的最小值。
* 请注意,答案不一定是 arr 中的数字。
*
* @example
* 输入:arr = [4,9,3], target = 10
* 输出:3
*/
const search_1300 = (arr, target) => {
const len = arr.length;
arr.sort();
let [left, right] = [0, arr[len - 1]];
while (left < right) {
let mid = Math.ceil(left + (right - left) / 2);
const total = arr.reduce((total, num) => {
total += num >= mid ? mid : num;
return total;
}, 0);
if (total > target) {
right = mid - 1;
} else {
left = mid;
}
}
const total_left = arr.reduce((total, num) => {
total += num > left ? left : num;
return total;
}, 0);
const total_right = arr.reduce((total, num) => {
total += num > left + 1 ? left + 1 : num;
return total;
}, 0);
return Math.abs(total_left - target) > Math.abs(total_right - target)
? left + 1
: left;
};
复杂的判别条件
「目标变量」和「另一个变量」有相关关系(一般而言是线性关系),目标变量的性质不好推测,但是另一个变量的性质相对容易推测。这样的问题的判别函数通常会写成一个「函数」的形式。
875 - 中等
/**
* leetcode 875.爱吃香蕉的珂珂 - 中等
*
* @remarks
* 珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 h 小时后回来。
* 珂珂可以决定她吃香蕉的速度 k (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。
* 珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
* 返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。
*
* @example
* 输入:piles = [3,6,7,11], h = 8
* 输出:4
*
* @example
* 输入:piles = [30,11,23,4,20], h = 5
* 输出:30
*
*/
const search_875 = (piles, h) => {
const len = piles.length;
let [left, right, mid, hour] = [0, Math.max(...piles), 0, 0];
while (left < right) {
mid = Math.floor(left + (right - left) / 2);
hour = h;
for (let i = 0; i < len; i++) {
if (hour >= 0) {
hour -= Math.ceil(piles[i] / mid);
} else {
break;
}
}
if (hour >= 0) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
};
410 - 困难
/**
* leetcode 410.分割数组的最大值 - 困难(贪心 + 二分)
*
* @remarks
* 给定一个非负整数数组 nums 和一个整数 m ,你需要将这个数组分成 m 个非空的连续子数组。
* 设计一个算法使得这 m 个子数组各自和的最大值最小。
*
* @example
* 输入:nums = [7,2,5,10,8], m = 2
* 输出:18
*/
const search_410 = (nums, k) => {
let [min, max] = [Math.max(...nums), eval(nums.join("+"))];
while (min < max) {
let mid = Math.floor(min + (max - min) / 2);
if (check_410(nums, mid, k)) {
max = mid;
} else {
min = mid + 1;
}
}
return min;
};
const check_410 = (nums, mid, k) => {
let count = 1;
let sum = 0;
for (let i = 0; i < nums.length; i++) {
if (sum + nums[i] > mid) {
count++;
sum = nums[i];
} else {
sum += nums[i];
}
}
return count <= k;
};
1011 - 中等
/**
* leetcode 1011.在 D 天内送达包裹的能力 - 中等
*
* @remarks
* 传送带上的包裹必须在 days 天内从一个港口运送到另一个港口。
* 传送带上的第 i 个包裹的重量为 weights[i]。每一天,我们都会按给出重量(weights)的顺序往传送带上装载包裹。
* 我们装载的重量不会超过船的最大运载重量。
* 返回能在 days 天内将传送带上的所有包裹送达的船的最低运载能力。
*
* @example
* 输入:weights = [3,2,2,4,1,4], days = 3
* 输出:6
*/
const search_1011 = (weights, days) => {
let [left, right] = [Math.max(...weights), eval(weights.join("+"))];
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
// 以mid的运力送完,时间是否不超过days
if (check_1011(weights, mid, days)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
};
const check_1011 = (weights, mid, days) => {
let count = 1;
let sum = 0;
for (let i = 0; i < weights.length; i++) {
if (sum + weights[i] > mid) {
count++;
sum = weights[i];
} else {
sum += weights[i];
}
}
return count <= days;
};
1482 - 中等
/**
* leetcode 1482.制作 m 束花所需的最少天数 - 中等
*
* @remarks
* 给你一个整数数组 bloomDay,以及两个整数 m 和 k 。
* 现需要制作 m 束花。制作花束时,需要使用花园中 相邻的 k 朵花 。
* 花园中有 n 朵花,第 i 朵花会在 bloomDay[i] 时盛开,恰好 可以用于 一束 花中。
* 请你返回从花园中摘 m 束花需要等待的最少的天数。如果不能摘到 m 束花则返回 -1 。
*
* @example
* 输入:bloomDay = [1,10,3,10,2], m = 3, k = 1
* 输出:3
*/
const search_1482 = (bloomDay, m, k) => {
const len = bloomDay.length;
if (m * k > len) return -1;
let [left, right] = [Math.min(...bloomDay), Math.max(...bloomDay)];
while (left < right) {
// 取中间天数
const mid = Math.floor(left + (right - left) / 2);
// 在mid天时,能否制作完
if (check_3(bloomDay, mid, m, k)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
};
const check_1482 = (bloomDay, mid, m, k) => {
let count = m;
let can = 0;
for (let i = 0; i < bloomDay.length && count >= 0; i++) {
if (bloomDay[i] <= mid) {
can++;
if (can === k) {
can = 0;
count--;
}
} else {
can = 0;
}
}
return count <= 0;
};
lcp12 - 中等
/**
* leetcode lcp12.小张刷题计划 - 中等
*
* @remarks
* 为了提高自己的代码能力,小张制定了 LeetCode 刷题计划,他选中了 LeetCode 题库中的 n 道题,编号从 0 到 n-1,并计划在 m 天内按照题目编号顺序刷完所有的题目(注意,小张不能用多天完成同一题)。
* 在小张刷题计划中,小张需要用 time[i] 的时间完成编号 i 的题目。此外,小张还可以使用场外求助功能,通过询问他的好朋友小杨题目的解法,可以省去该题的做题时间。为了防止“小张刷题计划”变成“小杨刷题计划”,小张每天最多使用一次求助。
* 我们定义 m 天中做题时间最多的一天耗时为 T(小杨完成的题目不计入做题总时间)。请你帮小张求出最小的 T是多少。
*
* @example
* 输入:time = [1,2,3,3,3], m = 2
* 输出:3
*/
const search_lcp12 = (time, m) => {
if (time.length === m) return 0;
let [left, right] = [Math.min(...time), eval(time.join("+"))];
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
// 最多耗时为mid时,m天内能否完成,能完成则向左收缩,否则右收缩
if (check_4(time, mid, m)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
};
const check_lcp12 = (time, mid, m) => {
const len = time.length;
let total = 0;
let count = 1;
let max = time[0];
for (let i = 0; i < len; i++) {
max = time[i] >= max ? time[i] : max;
let whole = total + time[i] - max;
if (whole <= mid) {
total += time[i];
} else {
count++;
total = time[i];
max = time[i];
}
}
return count <= m;
};
最后
参考文章:
很感谢大家抽空读完这篇文章,希望大家能有所收获。祝大家工作顺利,身体健康💪。
下一篇:基础、高级、非比较排序算法。
「 ---------- The end ---------- 」