二分查找典型题-绝对差值和
二分查找可以以 O(logn) 的时间复杂度完成查找,最典型的二分查找问题是在一个排序数组中,查找特定的值,返回其下标。实际中,二分查找问题有着很多的变形,这里以一道典型的题目展示二分查找的一些典型变化。
这里分析的题目是 leetcode 中的 1818. 绝对差值和, 题目的要求如下:
给你两个正整数数组
nums1
和nums2
,数组的长度都是 n 。数组
nums1
和nums2
的 绝对差值和 定义为所有|nums1[i] - nums2[i]|
(0 <= i < n)的 总和(下标从 0 开始)。你可以选用
nums1
中的 任意一个 元素来替换nums1
中的 至多 一个元素,以 最小化 绝对差值和。在替换数组
nums1
中最多一个元素 之后 ,返回最小绝对差值和。因为答案可能很大,所以需要对 1e9 + 7 取余 后返回。
分析
上面的这道题目,允许在 nums1
中替换一个数字,来减小与 nums2
数组中对应元素之间的距离,以实现 绝对差值和 的最小化。初看来,这道题目有点无从入手,我们可以将问题进行分解,实际上要求总的绝对差值和 最小化,可以分解为求解 nums2 中的每一位元素,nums1 中与该元素的最小差值。然后将该差值与原差值比较,得到差值可以减少的最大值 - maximumReduction
,从而 最小的绝对差值 就等于 原绝对差值和 减去 maximumReduction
。
因此,问题可以转换为遍历数组 nums2 中的每个元素,寻找 nums1 中与对应元素最近的元素,并计算差值。标准的二分查找问题是查找等于对应值的元素,而与对应元素最近的元素该如何查找呢?实际上,这里可以将该问题分为两类来处理,与 元素 target 最近的元素有两种可能性,一种是 最近的元素小于等于 target,一种是大于等于 target。求解小于等于 target 的最右边元素 和 大于等于 target 的最左边元素的代码如下所示:
1 | // 寻找小于等于 target 的最大值 |
由于我们的查找一定有解,所以 while
中使用 <
而未用 <=
。上述两个函数主要的不同之处在于 mid 值的选取,对于寻找 最大值 的 flooring 函数,mid 值是中间右偏的,这样,如果有相等的值,指针会不断的右移,从而找到的是 最右边的值。类似的,ceiliing 函数寻找的是最小值,mid 则是中间左偏。需要注意的是,findFlooring
返回的值不一定在 findCeiling
的左边,当 数组中有等于 target 的值时,findFlooring
最终的指针指向最右边的一个 target,而 findCeiling
最终的指针指向的是最左边的一个 target。如下图所示:
最后,如果数组中所有的元素都大于 target,findFlooring()
会返回数组中最左边的元素,类似的,如果都小于 target,findCeiling()
会返回数组中最右边的元素,都是最靠近 target 的元素。
完成上述两个函数后,最近的元素为 两个函数返回值中距离 target 更小的那一个。我们可以通过一个函数来实现这个功能:
1 | int findClosest(int[] nums, int target){ |
完成上述准备工作后,原问题的处理就相对简单了。具体的主函数的代码如下:
1 | public int minAbsoluteSumDiff(int[] nums1, int[] nums2) { |