左中右,取整数,我叫「二分」你记住!🫵

1,004 阅读13分钟

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
}
  • 每一次都会在一个区间里搜索目标元素。所以需要两个变量表示数组里区间的「左右边界」,分别用变量leftright表示,左边界的值等于 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,在leftright很大时,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 - 困难

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

/**
 * 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 - 中等

搜索旋转排序数组 II

/**
 * 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 - 简单

x 的平方根

/**
 * 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 - 中等

在 D 天内送达包裹的能力

/**
 * 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 - 中等

制作 m 束花所需的最少天数

/**
 * 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 ---------- 」